程序调用自身的编程技巧称为递归( recursion)。递归作为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
简单来说,递归就是自己调用自己,通过复杂问题自身调用的层层展开,利用子问题求解归并为大问题求解
递归的思想在深度优先搜索(DFS)上有着很好的应用,因为深搜的核心本身就是通过递归来遍历搜索的过程。
深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.
下面以一个例题来引入深度优先搜索算法。
给定一个整数n,将数字1~n排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式: 一个整数n
输出格式:
按照字典序排列所有排列方案,每个方案占一行
数据范围: 1≤n≤7
输入样例:3
解题思路:
排列数字说白了就是求1~n的全排列,并输出。
那就要一个一个枚举搜索,这里采取dfs回溯的方法,可以简化枚举的时间复杂度
操作如图所示:
我们可以认为1~n这n个数字分别有两种状态:已经被选择和还未被选择。
我们此题想要实现的无非就是选择这1~n个数字,去填入一个盒子里面,问有哪几种选择的先后顺序的方法。
所以刚开始可以选1、2、3.
然后如果第一次选的1,下一次再从1~3遍历一边,发现可以选2或3这两个还没有被选过的,再然后如果第二次选了2,那么第三次再遍历一边发现就只能选3了;同理在第二次如果选的是3,那么第三次遍历一边发现只有2没有被选,就只能选2了。
由此概括,总结一个1~n(n无穷大)的通解
开始:遍历1~n
第一层:在1~n任选一个选出来,标记已被选择
第二层:在第一层基础上再遍历1~n,在没有被选择的再选择一个,标记已选择
第三层:在第二层… …
第n层:在第n-1层…
结束:发现全部被选完了,开始输出~~
代码如下:
/*
DFS实现排列组合,假设n=3;
1 2 3
2 3 1 3 1 2
3 2 3 1 2 1
*/
#include<iostream>
using namespace std;
int a[10],n,book[10];//a[i]这个数组用来记录已经第i个被选进去的是哪个数字
//book[i]这个数组用来记录数字i是否被选择了
void dfs(int now,int num)//now表示现在选择的是哪个数字,num表示已经选择了多少个数字
{
if(num==n)//已经选择的数已经到n了,说明选完了,开始输出
{
for(int i=1;i<=n;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return;//输出完了,结束void函数,直接return,不用执行后面的了
}
for(int i=1;i<=n;i++)//再从1~n遍历
{
if(book[i]==0)//i这个数字没有选过,那就选
{
book[i]=1;
a[++num]=i;//注意是++num而不是num++
//可以改写为num++;a[num]=i;
dfs(i,num);//继续回溯,因为前面num已经++了,这里就直接回溯num就行
book[i]=0;//和前面一样,恢复初值
a[num--]=0;//注意是num--,不能是--num!
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)//第一层这是,从1~n,都给他来一遍
{
book[i]=1;//标记i为已选择
a[1]=i;//这是第一个选的数字,所以a[1]=i
dfs(i,1);//开始进入递归函数,回溯dfs~~
book[i]=0;//42-43这两行是很重要的,但是同样也是不太好理解的关键点
a[1]=0;//这里把book和a又重新弄回0了,给恢复原值,为什么呢?最后细讲。
}
return 0;
}
/*
手动模拟上面注释的操作,假设输入的是n=3.执行代码,从1~3遍历,然后
执行dfs(1,1)--->这是第一次选择1的情况
在dfs(1,1)中又开始选择其他的~~~,好,我们现在假设第一次选1的全都执行完了,已经输出完第一次选1的了
那么该回去了,第一次要选择2了,但是注意,book[]和a[]可是有值的,并不是初始化的0噢,所以这就是为什么
我们在执行一次回溯后后面要恢复初值,是为了方便下一次回溯~
*/
图解n=3的全排列过程:
为了方便理解,手动模拟n=3的情况如下:
输入n=3->dfs(1,1)->dfs(2,2)->dfs(3,3)->输出“1 2 3”->回溯dfs(2,2)->回溯dfs(1,1)->dfs(3,2)-> dfs(2,3)->输出“1 3 2”->回溯dfs(1,1)->dfs(2,1)->dfs(1,2)->dfs(3,3)->输出“2 1 3”->回溯dfs(1,2)->dfs(3,2)->dfs(1,3)->输出“2 3 1”->回溯dfs(1,1)->dfs(3,1)->dfs
(1,2)->dfs(2,3)->输出“3 1 2”->回溯dfs(1,2)->dfs(2,2)->dfs(1,3)->输出“3 2 1”->结束bingo!
趁热打铁,再来一个n皇后问题
n-皇后问题是指将n个皇后放在n*n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同—列或同一斜线上。
输入格式:
一个整数n
输出格式:
所有满足要求的布局方案,方案之间一个空格间隔开
数据范围:1≤n≤9
输入样例:4
输出样例:
代码如下:
#include<iostream>
using namespace std;
int n,l1[1000],l2[1000],row[1000];//l1---lean1->左上右下对角线(差为0) l2---lean2->左下右上对角线(和为定值) row->列
char map[11][11];
void dfs(int now)
{
if(now==n+1)//满足条件就输出√
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cout<<map[i][j];
}
cout<<endl;
}
cout<<endl;
return ;
}
for(int i=1;i<=n;i++)//i代表列,now代表行
{
if(row[i]==0&&l1[now-i+100]==0&&l2[now+i]==0)//满足列、两条对角线上面都没有皇后
{
row[i]=1;
l1[now-i+100]=1;
l2[now+i]=1;
map[now][i]='Q';
dfs(now+1);//标记,回溯,恢复初值
row[i]=0;
l1[now-i+100]=0;
l2[now+i]=0;
map[now][i]='.';
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)//先全部给初始化成点,放皇后的再给弄成Q
for(int j=1;j<=n;j++)
map[i][j]='.';
dfs(1);//这里我们枚举的是行,一行一行往下搜索,所以刚开始dfs(1)
return 0;
}
常见的递归模板
1.递归求组合数
#include<iostream>
#include<iomanip>
using namespace std;
int book[30]={0},a[30]={0},n,m;
void dfs(int now_num,int now,int ans)
{
if(now_num>n+1)
return ;
if(now>ans)
{
for(int i=1;i<=ans;i++)
{
cout<<setw(3)<<a[i];
}
cout<<endl;
return;
}
book[now_num]=1;
a[now]=now_num;
now++;
dfs(now_num+1,now,ans);
now--;
a[now]=0;
book[now_num]=0;
dfs(now_num+1,now,ans);
}
int main()
{
cin>>n>>m;
dfs(1,1,m);
}
2.递归求全排列
#include<iostream>
#include<cmath>
using namespace std;
int a[27];
bool b[27];
int n,k;
void dfs(int now)// 1
{
if(now==n)
{
for(int i=1;i<=n;i++)
printf("%5d",a[i]);
cout<<endl;
return ;
}
for(int i=1;i<=n;i++)//下一层选i
{
if(b[i]==false)
{
b[i]=true;
a[++now]=i;
dfs(now);// 1 2
b[i]=false;
a[now--]=0;// 1 i=2
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)//第一个数选的是i
{
b[i]=true;
a[1]=i;
dfs(1);
b[i]=false;
a[1]=0;
}
}
3.递归求汉诺塔
对于汉诺塔问题:
假设只有两个木块,那么需要进行的操作是:把X第一个放到Y,把X剩下的一个放到Z,把Y剩下的放到Z
假设只有三个木块,那么需要进行的操作是:把X第一个放到Z,X第二个放到Y,然后把Z的那个放到Y,这个时候是把X种的(3-1)个借助Z全部放到了Y上面,
然后把X上面最后一个放到Z上面,然后需要进行的操作就是把Y上的两个通过X放到Z上,所以就重复了假设只有两个木块的情况。
#include<iostream>
using namespace std;
int cnt=0;
void move(int id,char from,char to)
{
cout<<"step-"<<++cnt<<" : "<<id<<" from "<<from<<" to "<<to<<endl;
}
void hanoi(int n,char x,char y,char z)
{
if(n==0)
return ;
hanoi(n-1,x,z,y);
move(n,x,z);
hanoi(n-1,y,x,z);
}
int main()
{
int x;
cin>>x;
hanoi(x,'A','B','C');
}