Re:大牛们,你们怎么学的递归啊??求救,太难了(转载)

[复制链接]
查看11 | 回复1 | 2021-1-29 12:37:54 | 显示全部楼层 |阅读模式
原文由 EasyHard 发表在 EECS 版
我也来凑个热闹^_^
我觉得理解递归在一开始并不需要按照实现严格地手动模拟,而是要在高层次上理解递归的本质是将问题分解成更小的子问题.
举最简单的例子,大家在数学上非常熟悉的fib数列,在数学上定义为fib(n)=fib(n-1)+fib(n-2),边界条件是fib(0)=0,fib(1)=1.
为了定义fib数列的第n项,我们需要知道该数列的第n-1项和第n-2项.定义好fib数列的第n项相当于定义了整个fib数列.但是,反过来说,只有将整个fib数列定义好之后,我们才能对任意的n,给出fib数列的第n项.那么,我们又怎么能在没有定义好fib数列的时候就能使用fib数列的第n-1项和第n-2项呢?
考虑到大家都在bdwm灌水,相信大家都能明白,这个式子可以这样理解:为了求fib数列的第n项,我们需要求出fib数列的第n-1项和第n-2项,并把它们加起来.为了求第n-1项,我们又可以根据定义先求第n-2项和第n-3项......最后总会求到fib(0)和fib(1)(也就是边界条件上去)
需要大家明白的不是fib数列的求解步骤,而是要大家注意这个的解法特点:为了解决某个问题(求fib数列的第n项,这里的n是任意的),我们可以假装我们已经有了可以求解这个问题的办法(可以求出fib数列中的任意一项),然后,将要解决的问题分解为另外的几个同类的问题(求fib数列的第n-1项和第n-2项),由于这些分解出来的问题和原来的问题是同一类问题,我们可以用假装得到的办法来求解这些分解出来的问题.只要分解的新问题的规模比原问题小(n-1和n-2都比n小),我们就总会求到边界条件上,那么,只要再给出边界条件就可以了.
在编程时也是同样的.例如,要编写函数function_fib(n)返回fib数列的第n项.我们就在编写时假装function_fib(n)已经写好了,那么,只要我调用function_fib(a),它就会给我返回fib数列的第a项.那么,我们的代码应该写成int function_fib(n) {

if (n == 0) return 0;

if (n == 1) return 1;//给出边界条件

int fib_1 = function_fib(n - 1); //假装function_fib已经写好了, fib_1是fib
数列的第n-1项

int fib_2 = function_fib(n - 2); //假装function_fib已经写好了, fib_2是fib
数列的第n-2项

return fib_1 + fib_2 // function_fib(n) 应该返回fib数列的第n项
}

这实际上是一种思考的模式:为了解决某个问题,我们考虑能不能将这个问题分解成几个子问题,将这些子问题解决之后,将它们的解通过某种方式合並,得到原问题的解.如果存在这样的方法,我们会说这个问题是一个递归的问题,这样的方法会被叫做递归的.因为这些问题都可以用同样的方式实现.
再举另一个例子:n皇后判定问题.n皇后问题即给出一个n*n的棋盘,判定棋盘中能不能放下n个互相不能攻击的皇后.n*n的棋盘能不能放下n个互相不能攻击的皇后很难转化为几个k*k的棋盘放下k个互相不能攻击的皇后,这里k是任意的正整数.(不信你试试^_^)这是因为我们的问题太大了,即使是最小的问题,也没办法整除别的问题,自然就没办法分解了.这时,如果还要继续考虑用分解子问题的手段来解决,就要先将问题分解成新的形式,在新的形式里,问题的数目变多,这样,最小的问题就有可能整除别的问题了.在这个例子中,我们考虑一个新的问题.把一个n*n的棋盘,和放置累在这个棋盘上的一定数目皇后看成一个整体,叫做一个=棋局=.我们来判定一个棋局能不能再接着放下一些皇后,使得这个n*n的棋盘可以一共放下n个皇后.不妨把假装出来可以解决这个问题的方法看做函数f.也就是说,函数f返回真,如果给它的棋局可以再放下一些皇后,使得皇后的数目到达n个,否则返回假.例如f(一个8*8的空棋盘)=true;而-----|Q||-----
回复

使用道具 举报

千问 | 2021-1-29 12:37:54 | 显示全部楼层
f(-----)=false
显然,对于任意一个棋局,我们生成其所有的多放一个皇后之后的棋局.如果这些新生成的棋局中有任何一个可以放到n个皇后,那么,该棋局可以放到n个皇后.而新生成的棋局可不可以放到n个皇后可以用我们假装出来的解决方案(函数f)来解决.如果f对某个棋局返回true,我们说这个棋局是可行的.否则说它是不可行的.再给出边界条件:对存在n个互不攻击的皇后的棋局返回true,对存在可以互相攻击的皇后的棋局返回false.所以,这个问题的递归代码为:bool f(棋局) {
判断边界条件, 如果满足, 返回true或者false;
for_each (棋局中所有的空位) {

在空位上放上皇后, 生成新的棋局;

if ( f(新的棋局) )

return true;
}
return false;
}
当然,为了实现这个算法,我们还有一些问题,第一个问题就是棋局的表示.当然,我们可以用一个n*n的数组来保存整个棋局.不过,如果注意到对于任意合法的方案,每一行中有且只有一个皇后,那么,我们只要一个大小为n的int数组保存每一行中那个皇后所在的位置就可以了.另外,在函数中的for循环上,我们也不用枚举所有的空位,而只要在现在棋局的某一个空行中找一个空位就可以了.
还可以进一步的优化:只要找到行号最小的空行,并枚举其中的空位就可以了.这是因为在在一个解中,每行必然存在一个皇后.所以,如果某个棋局最后是可行的,在其行号最小的空行的空行中必然能够找到一个位置放下皇后,使得新的棋局最后是可行的.也就是说,一个棋局是可行的,等价于在其行号最小的空行中的所有位置放下皇后得到的新棋局中,至少有一个是可行的.
经过了优化之后,我们的代码变成了:bool f( int* 每行皇后所在位置,未放皇后的做特殊标记(棋局的一种表示方式) ) {
判断边界条件, 如果满足, 返回true或者false;
int row = 棋局中最小空行的行号
for_each (第row行的空位) {

在空位上放上皇后, 生成新的棋局;

if ( f(新的棋局) )

return true;
}
return false;
}

如果把f的int*参数叫做board,那么,即bool f( int* board ) {
判断边界条件, 如果满足, 返回true或者false;
int row = 棋局中最小空行的行号
for_each (第row行的空位) {

在空位上放上皇后, 生成新的棋局 new_board;

if ( f(new_board) )

return true;
}
return false;
}
你会发现,如果在第三行中求出的row是x的话,那么,对于第6行中的f,它求出来的row肯定是x+1,我们也不要为难它,让它再算一次了.我们告诉它好了.而且这样的话,就有人帮我自己算了.与人为善确实是有好处的.bool f( int* board, int row ) {
判断边界条件, 如果满足, 返回true或者false;
for_each (第row行的空位) {

在空位上放上皇后, 生成新的棋局 new_board;

if ( f(new_board, row+1) )

return true;
}
return false;
}

注意,如果这个时候再考虑f的话,f的含义已经改变了.f现在的意义是,对于一个从第0行到第row-1行都已经放上一个皇后的棋局board,我来判定它是不是可行的.这相当于在第row行放上新的皇后,产生新的棋局,如果新的棋局中有一个是可行的,那么我现在的board就是可行的.要不然就是不可行的.
另一个技巧是不用显式地生成new_board,只要修改board,最后再把它恢复就好了.bool f( int* board, int row ) {
判断边界条件, 如果满足, 返回true或者false;
for_each (第row行的空位pos) {

board[row] = pos;

if ( f(board, row+1) )

return true;

board[row] = EMPTY;
}
return false;
}
这样的话,其实每个f的board都是一样的,我们可以把它弄成全局变量.我说的只是=可以=哦.intboard[n];bool f(int row ) {
判断边界条件, 如果满足, 返回true或者false;
for_each (第row行的空位pos) {

board[row] = pos;

if ( f(row+1) )

return true;

board[row] = EMPTY;
}
return false;
}
这一次,虽然f的参数表变了,不过它的含义没有变,因为我们只是把一个函数变成了隐含的.
我就不继续写了,如果你继续优化,最后会得到书上那个解法哦^_^.
最后是三个小小的,有些生硬的练习:1.N皇后问题,即给出可行的方案数,而不仅仅是是否存在可行方案.
2.用递归实现后缀表达式的求值后缀表达式的形式为算符后缀表达式_1后缀表达式_2.一个后缀表达式的值为,将后缀表达式_1的值和后缀表达式_2的值进行算符所指的操作.
在这个练习里,递归的方式是直截了当的.但是,有一个小小的问题:我们不知道后缀表达式_1和后缀表达式_2在哪里分隔开来.努力一下吧^_^要是老想不出也不要慌张....我也许出错题了_
3.在我们上面的讨论中,没有考虑那里才是合适的边界.事实上,确定边界也是非常重要的,合适的边界可以使解决问题变得更简单,也更容易保证递归的可终止性.考虑一下上面几个例子和你的练习中,是不是所有的边界都是必要的,有没有可能用新的边界代替旧的.并给出上面这些递归方案一定会终止的证明.
最后补充一下:TheLittleScheme确实是一本好书!

wdswds (wds2006sdo) 在 ta 的帖子中提到:RT
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

主题

0

回帖

4882万

积分

论坛元老

Rank: 8Rank: 8

积分
48824836
热门排行