目录
递归是指在函数的定义中使用函数自身的方法。它通过不断地将问题分解为更小的子问题,直到达到基本情况,然后再逐层返回结果。
递推则是通过已知的前一个或前几个状态来推导出后续状态的方法。它是按照一定的规律逐步计算出后续结果的过程。
从宏观上来看,递归是将一个大问题分解为多个相同类型的小问题,不断重复;递推是根据前一个或前几个状态逐步推出后续状态,也就是从小问题解决到大问题。
本文是递归与递推的算法题练习,题目都源于网站Acwing
1、递归实现指数型枚举
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
当每个状态的可能情况都是相同的,那么此时就是指数型枚举
在这种枚举方式中,通常会以某种特定的顺序或规则来遍历所有可能的情况
比如本题,枚举1~n的每个数,每个数都有选或不选两种情况,共有2^n种结果
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
int n;
bool st[N];//每个数的状态,默认为都不选
void dfs(int x)
{
if(x > n)
{
for(int i=1;i<=n;i++)
if(st[i]) cout<<i<<" ";
cout<<endl;
return ;
}
//不选
st[x] = false;
dfs(x+1);
//选
st[x] = true;
dfs(x+1);
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
2、递归实现排列型枚举
输入样例:
3
输出样例:
bash
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
所谓排列,就是对给定的元素,按照不同顺序,排列出所有可能的情况,每个元素不重不漏
特点:有序、不重不漏
代码实现时一般会为排列元素开一个状态数组,来判定是否选过
本题就是对1~n的全排列
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N =20;
bool st[N];//状态数组,是否选过,默认没选
int ans[N];
int n;
void dfs(int x)
{
if(x > n)
{
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
cout<<endl;
}
for(int i=1;i<=n;i++)
{
if(!st[i])
{
st[i] = true;
ans[x] = i;
dfs(x+1);
st[i] = false;//还原现场
}
}
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
3、递归实现组合型枚举
输入样例:
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
组合是从给定的元素集合中,选取若干个元素组成一组,而不考虑元素的排列顺序。
组合只关注选取哪些元素,而不关心这些元素的具体排列方式。
在代码实现中,组合型枚举区别于排列型、指数型枚举,它需要传参来标记枚举顺序(每次枚举的起始位置),防止枚举重复,同样的,因为每次标记了起始位置,所以从起始位置开始都是未选取的元素,不再像排列型枚举那样开状态数组
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 30;
int n,m;
int ans[N];
void dfs(int x,int start)
{
if(x-1 + n-start + 1 < m) return;
if(x > m)
{
for(int i=1;i<=m;i++) cout<<ans[i]<<" ";
cout<<endl;
return ;
}
for(int i=start;i<=n;i++)
{
ans[x] = i;
dfs(x+1,i+1);
ans[x] = 0;//还原现场,本题其实可以不用,会自动覆盖
}
}
int main()
{
cin>>n>>m;
dfs(1,1);
return 0;
}
4、带分数
题意中的除法,没有特别指出时,一般都指正常的除法,而不是C++中的整除
题意表示,输入n,使得 n = a + b/c ,其中a、b、c包含的数字恰好为1~9各自出现一次
为了在编程时规避整除问题,我们一般把问题转化为乘法:n*c = a*c + b
方法一
每个元素恰好出现一次,正好对应全排列的性质,可以这么做:
- 对1~9做全排列,共有 9!种情况
- 枚举a、b、c分别有多少位(枚举了a、b就确定了c,两重循环即可)
- 判断等式 n***c = a*c + b**是否相等
代码:
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10;
bool st[N];
int ans[N];
int n,cnt;
int to_nums(int l, int r)
{
int res = 0;
for(int i = l; i <= r; i++)
{
res = res*10 + ans[i];
}
return res;
}
void dfs(int x)
{
if(x == 10)
{
for(int i = 1; i <= 7; i++)
{
for(int j = i+1; j<= 8; j++)
{
int a = to_nums(1,i);
int b = to_nums(i+1,j);
int c = to_nums(j+1,9);
if(c*n == c*a + b) cnt++;
}
}
return;
}
for(int i=1;i<=9;i++)
{
if(!st[i])
{
st[i] = true;
ans[x] = i;
dfs(x+1);
st[i] = false;
}
}
}
int main()
{
cin>>n;
dfs(1);
cout<<cnt<<endl;
return 0;
}
方法一的做法虽然能够AC,但因为是全排列,罗列了所有的情况去判断,有没有更高效的做法呢?
方法二
对于等式:n*c = a*c +**b ,**要找出满足该等式的三个数a、b、c,在给定了n的情况下,其实只需要确定了a、c, 根据等式,b也就确定了,再用确定的一组a、b、c,去判断是否满足题目要求(三个数包含的数字有且只能1~9各出现一次),这样的做法,不会像全排列那样罗列所有情况,会更加高效
步骤:
- 先搜索可能满足情况的数字a ( 0 < a < n)
- 对于每一个a,再去搜索可能的c ( a、c用的数字个数小于9,c > 0 )
- 对于每一组a、c,通过等式n*c = a*c +b计算得出b,再判断是否满足题意
- 如何判断?先判断b的各个数字是否与a、c用的数字冲突,再怕判断1~9是否都被使用了
代码:
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20;
bool st[N],backup[N];
int n,cnt;
bool check(int a, int c)
{
long long b = c*(long long)n - c*a;
if(!a || !b || !c) return false;
memcpy(backup,st,sizeof st);
while(b)
{
int t = b % 10;
b /= 10;
if(!t || backup[t]) return false;
backup[t] = true;
}
for(int i=1; i <= 9; i++)
{
if(!backup[i]) return false;
}
return true;
}
void dfs_c(int x, int a, int c)
{
if(x >= 9) return ;
if(check(a,c)) cnt++;
for(int i=1; i <= 9; i++)
{
if(!st[i])
{
st[i] = true;
dfs_c(x+1,a,c*10+i);
st[i] = false;
}
}
}
void dfs_a(int x,int a)
{
if(a >=n) return ;
if(a) dfs_c(x,a,0);
for (int i = 1; i <= 9; i ++ )
{
if(!st[i])
{
st[i] = true;
dfs_a(x+1,a*10+i);
st[i] = false;
}
}
}
int main()
{
cin>>n;
dfs_a(0,0);
cout<<cnt<<endl;
return 0;
}
5、翻硬币
输入样例1:
**********
o****o****
输出样例1:
5
输入样例2:
*o**o***o***
*o***o**o***
输出样例2:
1
通过推演可以发现,我们只需用一个指针 i 从前向后扫描,如果当前 i 指向的位置初始状态和目标状态不同,就翻转 i 和 i+1 位置的硬币(并记录翻转次数+1),保证 i 之前的初始状态和目标状态相同;因为题目说一定有解,如此遍历一遍,就一定能保证字符串的初始状态转变成了目标状态
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
int main()
{
string src,dst;
cin >> src >> dst;
int ans = 0;
for(int i = 0; i < src.size(); i++ )
{
if(src[i] != dst[i])
{
src[i] = src[i] != '*' ? '*' : 'o';
src[i+1] = src[i+1] != '*' ? '*' : 'o';
ans++;
}
}
cout << ans << endl;
return 0;
}
6、飞行员兄弟
输入样例:
-+--
----
----
-+--
输出样例:
6
1 1
1 3
1 4
4 1
4 3
4 4
思路:题目数据是4 * 4的矩阵,对于每个开关我们可以有按或不按两种情况,也就是对于这个矩阵,枚举对矩阵的操作方案:共有2^16 可能;对于每种操作方案,去对矩阵操作一遍后判断是否满足题意(开关全开),保存操作次数最小的一种输出即可。
代码实现中,
- 需要先枚举所有操作方案,每行用一个int数来代表,范围【0,15】;op[i]表示第 i 行的操作方案,所以需要对op[4]做指数型枚举,16^4种可能
- 对于每一种操作方案,模拟一遍对矩阵的操作,所以需要一个备份矩阵backup,在每次矩阵操作、判断完成后,把被操作的矩阵恢复原样,方便下次操作
- 矩阵的操作过程中,涉及到某个点的开关如果要按下,则需要用一个vector记录其坐标,方便后续输出;因为要更新最小值和对应得坐标,所以vector用tmp临时存储,如果要更新答案就把tmp赋值给ans,如果不更新答案就把tmp清空,以记录下一种操作方案的操作坐标
代码一:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
char g[N][N],backup[N][N];
int op[4];
bool st[16];
int ways = 50;
struct point
{
int x;
int y;
};
vector<struct point> tmp,ans;
void turn(int x, int y)
{
for(int i = 0; i < 4; i++ )
{
g[x][i] = g[x][i] != '+' ? '+' : '-';
}
for(int i = 0; i < 4; i++ )
{
g[i][y] = g[i][y] != '+' ? '+' : '-';
}
g[x][y] = g[x][y] != '+' ? '+' : '-';
}
void check()
{
int step = 0;
for(int i = 0; i < 4; i++ )
{
for(int j = 0; j < 4; j++ )
{
if(op[i] >> j & 1)
{
step++;
turn(i,j);
tmp.push_back({i,j});
}
}
}
bool off = false;
for(int i = 0; i < 4; i++ )
{
for(int j = 0; j < 4; j++ )
{
if(g[i][j] == '+')
{
off = true;
break;
}
}
}
if(!off && (step < ways))
{
ways = min(ways,step);
ans = tmp;
}
else
{
tmp.clear();
}
memcpy(g, backup, sizeof backup);
}
void dfs_op(int x)
{
if(x == 4)
{
check();
return ;
}
for(int i = 0; i < 16; i++ )
{
op[x] = i;
dfs_op(x+1);
}
}
int main()
{
for(int i = 0; i < 4; i++ ) cin >> g[i];
memcpy(backup, g, sizeof g);
dfs_op(0);
cout << ways << endl;
for(auto& p : ans)
{
cout << p.x + 1 << " " << p.y + 1 <<endl;
}
return 0;
}
代码二:
这个版本,是枚举操作方案时,直接用一个int来表示4x4矩阵的开关操作,再通过映射函数来对应矩阵的每个元素(矩阵元素从0开始标序号)
cpp
#include<bits/stdc++.h>
using namespace std;
using PII = pair<int,int>;
char g[5][5],backup[5][5];
int cnt = 100;
vector<PII> ans;
void turn(int x, int y)
{
for(int i = 0; i < 4; i++ )
{
g[x][i] = g[x][i] != '+' ? '+' : '-';
g[i][y] = g[i][y] != '+' ? '+' : '-';
}
g[x][y] = g[x][y] != '+' ? '+' : '-';
}
int get(int x, int y)
{
return x*4 + y;
}
int main()
{
for(int i=0;i<4;i++) cin>>g[i];
memcpy(backup, g, sizeof g);
for(int op = 0; op < 1 << 16; op++)
{
vector<PII> tmp;
for(int i = 0; i < 4; i++ )
{
for(int j = 0; j < 4; j++ )
{
if(op >> get(i,j) & 1)
{
tmp.push_back({i,j});
turn(i,j);
}
}
}
bool off = false;
for(int i = 0; i < 4; i++ )
{
for(int j = 0; j < 4; j++ )
{
if(g[i][j] == '+')
{
off = true;
break;
}
}
}
if(!off && cnt > tmp.size())
{
cnt = tmp.size();
ans = tmp;
}
memcpy(g, backup, sizeof g);
}
cout << cnt << endl;
for(auto& op : ans) cout << op.first + 1 << " " << op.second + 1 <<endl;
return 0;
}
7、费解的开关
你玩过"拉灯"游戏吗?
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<<𝑛≤500
输入样例:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
输出样例:
3
2
-1
要使灯泡全亮,25个格子的开关矩阵,每一个开关有开或关两种状态,一共有 2^25 种对矩阵的操作方案,再加上要检测的矩阵数n最高有500个,故暴力枚举所有方案再判断是否满足的方法会超时
通过推演我们发现,一个灯是否改变状态,是由它周围上下左右相邻的开关和它本身的开关决定的,和其他开关并无联系;如果我们能先确定第一行灯的状态,那么第一行的灯状态的切换只能由第二行的开关决定!
那么如何确定第一行灯的状态呢?
就使用数据给出的第一行的状态去递推后面几行开关的操作可行吗? 这样做最终的操作次数可能不是最小的;
我们可以枚举对第一行的所有开关的操作,共有2^5种操作方案,每一种操作方案对应了一种第一行的泡的亮暗,再去根据上一行操作后续几行开关;最后只需判断最后一行是否全亮即可(因为通过递推的操作前面几行,已经保证了前四行是全亮的)
这种方法,通过枚举第一行所有情况和题目特定的递推关系,枚举2^5就相当于枚举了2^25(通过递推关系过滤了2^25中很多不可能全亮的情况),因为每当第一行确定后,后续的开关操作都是对应的一种情况
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 30;
char g[N][N],backup[N][N];
int dx[5] = {-1, 0, 1, 0, 0};
int 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];
int b = y + dy[i];
if(a < 0 || a >=5 || b < 0 || b >= 5) continue;
g[a][b] ^= 1; //通过对字符'0' '1'异或 1,可直接切换字符0/1
}
}
int solve()
{
int res = 10;
for(int i=0;i<5;i++) cin>>g[i];
//备份矩阵g,枚举每次操作后还原,以便下一次枚举
memcpy(backup, g, sizeof g);
for(int op = 0; op < 32; op++ )//对第一行用二进制枚举,用int存储枚举结果
{
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 backup);
}
if(res > 6) return -1;
else return res;
}
int main()
{
int T;
cin>>T;
while(T--)
{
cout << solve() << endl;
}
return 0;
}