概念
- 递归:从已知问题的结果出发,用迭代表达式逐步推算出问题的开始的条件,即顺推法的逆过程,称为递归。
- 递推:递推算法是一种用若干步可重复运算来描述复杂问题的方法。递推是序列计算中的一种常用算法。通常是通过计算机前面的一些项来得出序列中的指定象的值。
- 递归与递推区别:相对于递归算法,递推算法免除了数据进出栈的过程,也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值。递推是一种关系,由小推大。将递推这种关系用某种计算机语言实现出来为递归。
算法举例
- 斐波那契数列:已知f(1) = 1 , f(2) = 1 , 且满足关系式f(n) = f(n-1) +
f(n-2),则f(50)等于多少? - 分析:根据初始条件f(1) = 1 , f(2) = 1 和关系式f(n) = f(n-1) + f(n-2),可知,f(3) =
f(2) + f(1) , f(3) = f(2) + f(1) …….
编写代码(递归)
int fib(n){
return n < 2 ? 1 : fib(n-1)+f(n-2);
}
递推求解
int fib(int n){
int fn = 1;
int fn_1 = 0;
for(int i=0; i<n; i++) {
int t = fn
fn = fn + fn_1;
fn_1 = t;
}
return fn;
}
经典例题
1.简单斐波那契
ACWing717. 简单斐波那契
以下数列0 1 1 2 3 5 8 13 21 …被称为斐波纳契数列。
这个数列从第3项开始,每一项都等于前两项之和。
输入一个整数N,请你输出这个序列的前N项。
输入格式
一个整数N。
输出格式
在一行中输出斐波那契数列的前N项,数字之间用空格隔开。
数据范围
0<N<46
输入样例:
5
输出样例:
0 1 1 2 3
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
int n;
cin >> n;
int f[46];
f[1] = 0, f[2] = 1;
for(int i = 3; i <= n; i++)
f[i] = f[i-1]+f[i-2];
for(int i = 1; i <= n; i++) cout << f[i] << " ";
return 0;
}
2.递归实现指数型枚举
ACWing92. 递归实现指数型枚举
从 1~n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好1个空格隔开。
对于没有选任何数的方案,输出空行。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 16;
int n;
int st[N];//状态,记录木诶个位置当前状态,0表示还没考虑,1表示选,2表示不选
void dfs(int u)
{
if(u > n)
{
for(int i = 1; i <= n; i++)
if(st[i] == 1)
printf("%d ", i);
printf("\n");
return;
}
st[u] = 2;
dfs(u+1);//第一个分支,不选
st[u] = 0;//恢复现场
st[u] = 1;
dfs(u+1);//第二个分支,选
st[u] = 0;//恢复现场
}
int main()
{
cin >> n;
//从1~n依次考虑每个数选或不选
dfs(1);
return 0;
}
3.递归实现组合型枚举
ACWing93. 递归实现组合型枚举
从 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
不能出现重复
解决办法:限制大小,只需新加的一个数大于前面的一个数
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 30;
int n,m;
int way[N];
void dfs(int u, int start)
{
if(u+n-start<m) return;//剪枝 如果把后面所有数都选上都不够m个,当前分支就一定无解
if(u > m)
{
for(int i = 1; i <= m; i++)
printf("%d ", way[i]);//打印方案
printf("\n");
return;
}
//依次枚举每个分支,即当前位置可以填哪些数
for(int i = start; i <=n; i++)
{
way[u] = i;
dfs(u+1, i+1);
//恢复现场
way[u] = 0;
}
}
int main()
{
cin >> n>>m;
//从1~n依次考虑每个数选或不选
dfs(1,1);
return 0;
}
4.递归实现排列型枚举
ACWing94. 递归实现排列型枚举
把 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<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 10;
int n;
int st[N];//状态 , 0表示还没放数,1~n表示放了数
bool used[N];//true表示用过,false表示没有用过
void dfs(int u)
{
if(u > n)
{
for(int i = 1; i <= n; i++)
printf("%d ", st[i]);//打印方案
printf("\n");
return;
}
//依次枚举每个分支,即当前位置可以填哪些数
for(int i = 1; i <=n; i++)
if(!used[i])
{
st[u] = i;
used[i] = true;
dfs(u+1);
//恢复现场
st[u] = 0;
used[i] = false;
}
}
int main()
{
cin >> n;
//从1~n依次考虑每个数选或不选
dfs(1);
return 0;
}
5.费解的开关
ACWing95. 费解的开关
你玩过“拉灯”游戏吗?25盏灯排成一个5x5的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字“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
- 顺序可以任意
- 每个格子最多按一次
- 每一行开关的操作,完全被前一行的灯的亮灭状态决定
#include<iostream>
#include<cstdio>
#include<cstring>
#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)
{
for(int i= 0; i < 5; i++)
{
int a = x + dx[i], b = y+dy[i];
if(a <0 || a >= 5 || b <0 || b >= 5) continue;//在边界外直接忽略
g[a][b] ^= 1;
}
}
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++)
{
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][j] == '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, sizeof g);
}
if(res > 6) res = -1;
cout << res<< endl;
}
return 0;
}
6.翻硬币
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:**oo***oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:
**********
o****o****
输出样例1:
5
输入样例2:
*o**o***o***
*o***o**o***
输出样例2:
1
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int n;
char start[N], last[N];
void turn(int i)
{
if(start[i] == '*') start[i] = 'o';
else start[i] = '*';
}
int main()
{
cin >> start >> last;
n = strlen(start);
int res = 0;
for(int i =0; i <n; i++)
if(start[i] != last[i])
{
turn(i), turn(i + 1);
res++;
}
cout << res << endl;
return 0;
}
7.飞行员兄弟
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有16个把手的冰箱。
已知每个把手可以处于以下两种状态之一:打开或关闭。
只有当所有把手都打开时,冰箱才会打开。
把手可以表示为一个4х4的矩阵,您可以改变任何一个位置[i,j]上把手的状态。
但是,这也会使得第i行和第j列上的所有把手的状态也随着改变。
请你求出打开冰箱所需的切换把手的次数最小值是多少。
输入格式
输入一共包含四行,每行包含四个把手的初始状态。
符号“+”表示把手处于闭合状态,而符号“-”表示把手处于打开状态。
至少一个手柄的初始状态是关闭的。
输出格式
第一行输出一个整数N,表示所需的最小切换把手次数。
接下来N行描述切换顺序,每行输入两个整数,代表被切换状态的把手的行号和列号,数字之间用空格隔开。
注意:如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开。
数据范围
1≤i,j≤4
输入样例:
-+--
----
----
-+--
输出样例:
6
1 1
1 3
1 4
4 1
4 3
4 4
按的顺序无关,每个开关按一次
最终全部变为‘-’
暴力搜索:
- 枚举所有方案 0~216-1 看成16位的二进制数
- 按照该方案对所有灯进行操作
- 判断灯是否全亮,记录方案
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 5;
char g[N][N];
vector<PII> ans, tmp;
void turn_one(int x, int y)
{
if (g[x][y] == '+') g[x][y] = '-';
else g[x][y] = '+';
}
void turn_all(int x, int y)
{
for (int i = 0; i < 4; i++)
{
turn_one(x, i);
turn_one(i, y);
}
turn_one(x, y);
}
void dfs(int x, int y)
{
// 如果说所有的把手都操作完了就看看冰箱能否打开
if (x == 3 && y == 4)
{
bool success = true;
for (int i = 0; i < 4; i++)
for (int j = 0; j <4; j++)
if (g[i][j] == '+')
{
success = false;
goto end;
}
end:
if (success)
if (ans.empty() || tmp.size() < ans.size())
ans = tmp;
return;
}
// 判断边界,如果y出界了就往下一行移动
if (y == 4) x++, y = 0;
// 操作把手(x, y)
turn_all(x, y);
tmp.push_back({ x, y });
dfs(x, y + 1);
// 恢复现场
tmp.pop_back();
turn_all(x, y);
// 不操作把手(x, y)
dfs(x, y + 1);
}
int main()
{
for (int i = 0; i < 4; i++) scanf("%s", g[i]);
// 从(0, 0)开始DFS
dfs(0, 0);
cout << ans.size() << endl;
for (int i = 0; i < ans.size(); i++)
printf("%d %d\n", ans[i].first + 1, ans[i].second + 1);
return 0;
}