在介绍递归算法时,递归发挥了两个作用,一个是递,另一个是归。递的意思是不断向下延伸,归的意思是返回上一个步骤。这两个操作为我们提供了无限的可能。其中最重要的一个应用就是尝试。关于尝试的概念,使用迷宫是好理解的。当我们身处一个迷宫中,我们可以做的事只有一个,不断的尝试,并在尝试过的位置做好标记。走过的路,我们不会再重复进行,这让我们可以快速通过迷宫。
递归回溯的功能正如身处迷宫中的我们——不断尝试,并对走过的路进行标记。我们以一个简单的实例来说明问题。
小明跳楼梯,小明每次可以跳1格或2格。问小明跳n阶楼梯一共有多少种,并且每一种是如何跳的呢?
对于一共有多少种这个问题,我们已经解答过了,它符合斐波那契数列。我们可以很快地写出代码。但对于每一种如何跳的?对于现在的我们来说,是棘手的。但递归给我们提供了思路。
我们了解一点,小明跳楼梯,每次要么跳1格,要么2格。总有一种他会选择。这是我们需要使用到的地方。我们如何使用呢?我们尝试!我们假设他跳1格,那么这一次跳过1格,下一次就是针对剩下的楼梯n-1跳了。这时,他仍然可以选择跳1格或2格。每次他都可以。那么我们可以使用一个循环,让他把每一种跳法都跳一遍好了。也就是说,第一次跳的时候,我们先让他跳1格,一直跳到最高层n阶。然后在把最后一次跳的楼梯数变成2格,就这样一遍一遍地尝试。最终给出我们的结果。对于结果,我们可以存放在一个全局变量的数组中。这是可行的。我们在这里使用伪代码简单描述一下过程。
//这是伪代码
int arr[100] = {}; //用来存放每一次的跳法。跳1格或跳2格,其中保存1或者2
//定义一个递归函数
//n 表示现在小明需要跳的楼梯总数为n
//index 用来存放第index次跳楼梯的方式 index 从1开始
void f(int n, int index)
{
if(n < 0)
return;
//楼梯数不能为负数,负数情况直接排除
if(n == 0) //小明跳的楼梯数为0,说明没有楼梯跳或者已经跳到最高层了
{//直接结束,结束之前可以输出跳的过程
//输出过程
for(int i = 1; i < index; ++i)
cout << arr[i] << " "; //从第一次跳,到index 前一次跳的楼梯数
cout << endl;
return; //没有楼梯数跳的话,直接结束程序
}
//我们并不知道小明这次跳1格还是2格,那么我们将1格或2格都试一遍
for(int i = 1; i <= 2; ++i)
{
arr[index] = i; //保存小明跳楼梯的方式
//小明这次跳了i格,那么只剩下n-i格楼梯了
//下一次跳的情况保存在index + 1的位置
f(n - i, index + 1);
}
}
int main()
{
f(3, 1); //假设楼梯数为3, 1 为第一次跳
}
-
已经使用程序描述清楚了,但程序具体的细节我们并不了解。这是不利的。我们尝试进行分析。
-
1、主函数中调用
f(3,1)。楼梯数为3,第1次跳楼梯。很显然,n的值为3,并不为0。因此,程序会直接进入for循环。循环语句的i开始初始化,i的值为1。也就是说第1次跳的是1格。我们将1格存放进arr[index]的位置,index此时也为1,我们得到arr[1]=1。接下来程序会进入递归f(3-1,2)。 -
2、很显然,
n此时是2并不等于0,程序会继续进入循环。循环中的i被重新定义。i的值为1。也就是说,小明第2次也跳了1格。我们将1存放进arr[2]的位置。接着再次进入循环。f(2-1,3)被调用。 -
3、
2-1的结果也没有得到0。程序会继续进入循环。然而,循环还是从1开始的。因此,和之前一样,程序会将1保存到arr[3]的位置,并再次以f(1-1,4)的形式进入下一次函数调用。 -
4、终于在这一次,
n的值变成了0。程序开始输出arr中的内容,我们知道小明实际跳的次数是3次,而index是数字4,我们在循环中使用小于index来表示没有取到4。输出结束之后,程序执行return表明程序结束。 -
5、程序结束表明
f(1-1,4)的函数调用结束了。但这个函数处于一个循环中,循环的i此时值为1。下一次循环会照常进行,i的值为2。执行下面这行语句arr[index] = i,这句写出来是有原因的。我们需要注意此时index的值。这个循环处于f(2-1,3这个函数中,因此index的值为3。所以赋值语句的具体表达是arr[3] = 2。程序执行下一句,调用f(1-2,4)。 -
6、进入这个函数之后,程序就会发现,
n的值为-1,小于0,程序直接结束,而什么也不做。它没办法做任何事情,他跳楼梯不能跳负数格的。 -
7、那么
f(1-2,4)的函数快速结束。而这个函数所处的循环也到头了。i等于3时,循环跳出。这也就意味着f(2-1,3)这个函数的结束。我们需要了解到的是,f(2-1,3)这个函数是在f(3-1,2)这个函数中调用的。因此,程序会回到f(3-1,2)这个函数的循环中。此时i的值还是1,下一次循环,i的值将变成2。arr[index] = i会正常保存2这个数字,并作为小明这次跳楼梯的格数。进行下一步,f(2-2,3)的函数调用开始了。 -
8、
n的值为0。程序会输出结果。
程序按照这样的步骤一步一步进行着,直到最开始的那个循环也结束。这样函数就完成它的使命。这是给出跳楼梯跳法的程序,那么既然每一次如何跳的都已经知晓了,那么可以直接通过这个代码获得有多少种吗?当然是没问题的。我们只需要添加一个全局变量。当n的值为0时,也就是一种跳法完成了,我们也就可以将跳法增加1了。具体的代码描述如下:
int arr[100] = {};
int cnt = 0;
void f(int n, int index)
{
if(n < 0)
return;
if(n == 0)
{
++cnt; //此时,跳法的数量增加1
for(int i = 1; i < index; ++i)
cout << arr[i] << " ";
cout << endl;
return;
}
for(int i = 1; i <= 2; ++i)
{
arr[index] = i;
f(n - i, index + 1);
}
}
从程序结果我们可以了解到,当小明跳3阶楼梯时,有一种方式是先跳1格,然后在跳2格。还有一种是先跳2格,再跳1格。如果我们把先跳1格,再跳2格的方式记作1+2,把先跳2格,再跳1格的方式记作2+1,并规定这两种方式为一种时,一共有多种跳法呢?
我们知道,楼梯数为3时,按照没有规定时的状态来看,答案为3种,分别为1+1+1,1+2,2+1。当按照规定时,变成2种,也就是1+1+1和1+2,当然你也可以看做1+1+1和2+1这两种。这是可以的。为了方便表达,我们常常按照从小到大排列查看,这样有利于找到对应的规律。
那么,3阶楼梯的跳法有2种,分别为1+1+1和1+2。4阶楼梯不按规定查看的话是5种,分别是1+1+1+1,1+1+2,1+2+1,2+1+1,2+2。按照规定来看的话是3种,分别是1+1+1+1,1+1+2,2+2。从这两种情况来看,我们保证的是前一次跳的楼梯数需要小于或等于后一次的楼梯数,但至少需要跳1格,至多只能跳2格。因此,我们可以将程序简单修改一下。
int arr[100] = {};
int cnt = 0;
void f(int n, int index)
{
if(n < 0)
return;
if(n == 0)
{
++cnt; //此时,跳法的数量增加1
for(int i = 1; i < index; ++i)
cout << arr[i] << " ";
cout << endl;
return;
}
//i 的值需要从前一个跳法中得到
//第一次跳时,需要确保从1格开始跳,因此需要保证arr[index - 1] = 1;
//第一次跳时,index = 1
//也就是说,arr[1 - 1] = 1;
for(int i = arr[index - 1]; i <= 2; ++i)
{
arr[index] = i;
f(n - i, index + 1);
}
}
int main()
{
arr[0] = 1; //保证第一次跳的时候从1格开始
f(5,1);
//假设跳的楼梯数为5
cout << cnt << endl; //输出种类
}
最后
以上就是曾经鼠标最近收集整理的关于算法之递归回溯(一)的全部内容,更多相关算法之递归回溯(一)内容请搜索靠谱客的其他文章。
发表评论 取消回复