如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。
———Leigh Caldwell
在学习递归之前,需要有一定的函数的基础,可以看看这篇博客:C++:函数详解
目录
递归常见的实现形式:
一、递归基础
本质上来讲就是 自己调用自己
主要是将一个大问题分为n个子问题去求解,因此必须要理解题目的具体细节,才能知道如何将一个大问题转化为n个子问题,并递归的求解这n个子问题。
而递归的理解需要结合栈,因为递归本身是栈的另外一种实践
什么是栈?
就是只允许在一端插入和删除的线性表
二、引例
对应代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int f(int n)
{
if(n==1) return 1;
if(n==2) return 2;
return f(n-1)+f(n-2);
}
int main(){
int n;
cin>>n;
cout<<f(n)<<endl;
return 0;
}
三、DFS
所有递归都能画出 递归搜索树
这也就是DFS,递归本质上是一颗树,重点是dfs里面需要哪些参数 ( eg. 用u表示当前枚举到哪个位置)
DFS我们称之为深搜,通常解决一些最大最长或者所有可能的问题,一般用递归来实现。因为深搜基本上会遍历每一个结果,但暴力法不同在于,深搜可以通过递归中不满足条件,实现剪枝。
总结:
(1)先想好递归方程
(2)处理好枚举数据,对已遍历的数据要标记
以下是递归常见的实现形式:
四、递归实现指数型枚举
从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数 n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好 1 个空格隔开。
对于没有选任何数的方案,输出空行。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
解析:
思路:从1~n依次考虑每个数选还是不选
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=15;
int n;
//记录每个位置的状态:0表示还没考虑,1表示选它,2表示不选
int st[N];
void dfs(int u) //u表示当前在第几位
{
//确定边界
if(u>n)
{
for(int i=1;i<=n;i++)
{
if(st[i]==1) cout<<i<<" ";
}
cout<<endl;
return;
}
st[u]=2;
dfs(u+1); //第一个分支:不选
st[u]=0; //恢复现场
st[u]=1;
dfs(u+1); //第二个分支:选
st[u]=0;
}
int main()
{
cin>>n;
dfs(1); //从1开始
return 0;
}
五、 递归实现排列型枚举
把 1∼n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。
输入格式
一个整数 n。
输出格式
按照从小到大的顺序输出所有方案,每行 1个。
首先,同一行相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。
数据范围
1≤n≤9
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
解析:
思路:依次枚举每个位置放哪些数
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10;
int n;
int state[N]; // 0 表示还没放数,1~n表示放了哪个数
bool used[N]; // true表示用过,false表示还未用过
void dfs(int u) //表示当前枚举到第u位
{
if(u>n) //边界
{
for(int i=1;i<=n;i++) cout<<state[i];
puts(" ");
return;
}
//依此枚举每个分支,即当前位置可以填哪些数
for(int i=1;i<=n;i++)
{
if(!used[i]) //默认值为true
{
state[u]=i;
used[i]=true;
dfs(u+1);
//恢复现场
statu[u]=0;
used[i]=false;
}
}
}
int main()
{
cin>>n;
dfs(1); //表示从1开始枚举
return 0;
}
六、递归实现组合型枚举
从 1∼n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案。
输入格式
两个整数 n,m,在同一行用空格隔开。
输出格式
按照从小到大的顺序输出所有方案,每行 1 个。
首先,同一行内的数升序排列,相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如 1 3 5 7
排在 1 3 6 8
前面)。
数据范围
n>0
0≤m≤n
n+(n−m)≤25
输入样例:
5 3
输出样例:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
解析:
思路:依此枚举每个位置上的数
重要参数:way[N] 记录三个位置
u 当前该枚举哪个位
start 当前最小可以从哪个数枚举
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=30;
int n,m;
int way[N]; //记录三个位置
//u 当前该枚举哪个位置,start 当前最小可以从哪个数枚举
void dfs(int u,int start)
{
if(u>m)
{
for(int i=1;i<=m;i++) cout<<way[i]<<" ";
cout<<endl;
return;
}
for(int i=start;i<=n;i++)
{
if(i>=start)
{
way[u]=i;
dfs(u+1,i+1);
way[u]=0;
}
}
}
int main()
{
cin>>n>>m;
dfs(1,1);
return 0;
}
七、练习题
费解的开关
25 盏灯排成一个 5×5 的方形。
每一个灯都有一个开关,游戏者可以改变它的状态。
每一步,游戏者可以改变某一个灯的状态。
游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字 1 表示一盏开着的灯,用数字 0 表示关着的灯。
下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6步以内使所有的灯都变亮。
输入格式
第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。
以下若干行数据分为 n 组,每组数据有 5 行,每行 5 个字符。
每组数据描述了一个游戏的初始状态。
各组数据间用一个空行分隔。
输出格式
一共输出 n 行数据,每行有一个小于等于 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若 6 步以内无法使所有灯变亮,则输出 −1。
数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
输出样例:
3
2
-1
解析:
思路:1.变化两次相当于没变,所以每个格子最多按一次
2.一行一行的进行判断和控制,前一行灯的状态决定这一行的操作,保证在最后一行前全 都是亮的,若最后一行有灯没亮,则该方案不可行
疑难点:1.如何枚举第一行的操作
2.如何记录和这个灯上下左右相邻的灯的位置
解决:1.递归实现指数型枚举
2.采用坐标系,记录偏移量(如图)
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=6;
char g[N][N],backup[N][N];
int dx[5]={-1,0,1,0,0},dy[5]={0,1,0,-1,0}
void turn(int x,int y) //改变5个位置的值
{
for(int i=0;i<5;i++)
{
int a=dx[i],b=y+dy[i]; //a b表示偏移之后的位置
//判断是否出界
if(a<0||a>=5||b<0||b>5) continue; //在边界外,直接忽略
if(g[a][b]='0') g[a][b]='1';
else g[a][b]='0';
}
}
int main()
{
int T;
cin>>T;
while(T--)
{
for(int i=0;i<5;i++) cin>>g[i]; //第一行
int res = 10; //最大步数
for(int op=0;op<32;op++) //op代表第一行
{
memcpy(backup,g,sizeof g); //备份一下
int step=0;
for(int i=0;i<5;i++)
{
if(op>>i & 1)
{
step++;
turn(0,i);
}
}
for(int i=0; i<4; i++){
for(int j=0; j<5; j++){
if(g[i][i]=='0')
{
step++;
turn(i+1,j);
}
}
}
//判断最后一行
bool dark = false;
for(int i=0; i<5; i++)
{
if(g[4][i]=='0')
{
dark=true;
break;
}
if(!dark) res=min(res,step);
memcpy(g,backup,sizaof g); //把原数组复制过来
}
if(res>6) res==-1;
cout<<res<<endl;
}
}
带分数(扩展:两层dfs)
100可以表示为带分数的形式:100=3+69258 / 714
还可以表示为:100=82+3546 / 197
注意特征:带分数中,数字 1∼9分别出现且只出现一次(不包含 0)。
类似这样的带分数,100有 11 种表示法。
输入格式
一个正整数。
输出格式
输出输入数字用数码 1∼9 不重复不遗漏地组成带分数表示的全部种数。
数据范围
1≤N<106
输入样例1:
100
输出样例1:
11
输入样例2:
105
输出样例2:
6
解析:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10;
int n;
bool st[N], backup[N];
int ans;
bool check(int a, int c)
{
long long b = n * (long long)c - a * c;
if (!a || !b || !c) return false;
memcpy(backup, st, sizeof st);
while (b)
{
int x = b % 10; // 取个位
b /= 10; // 个位删掉
if (!x || backup[x]) return false;
backup[x] = true;
}
for (int i = 1; i <= 9; i ++ )
if (!backup[i])
return false;
return true;
}
void dfs_c(int u, int a, int c)
{
if (u > 9) return;
if (check(a, c)) ans ++ ;
for (int i = 1; i <= 9; i ++ )
if (!st[i])
{
st[i] = true;
dfs_c(u + 1, a, c * 10 + i);
st[i] = false;
}
}
void dfs_a(int u, int a)
{
if (a >= n) return;
if (a) dfs_c(u, a, 0);
for (int i = 1; i <= 9; i ++ )
if (!st[i])
{
st[i] = true;
dfs_a(u + 1, a * 10 + i);
st[i] = false;
}
}
int main()
{
cin >> n;
dfs_a(0, 0);
cout << ans << endl;
return 0;
}