递归
例题:递归实现指数型枚举
从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数 n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好 1 个空格隔开。
对于没有选任何数的方案,输出空行。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
解题思路:
解题代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#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;
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 <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
const int N=10;
int n;
int state[N];//当前状态。0表示未放数,1~n表示放了哪些数
bool used[N];//表示该数是否被用过,false表示未用,true表示用过
void dfs(int u)
{
if(u>n)//每个空得数都遍历完了,边界
{
//挨个将数输出
for(int i=1;i<=n;i++) printf("%d ",state[i]);//打印方案
puts("");
return;
}
//以此枚举每个分支,即当前位置可以填那些数
for(int i=1;i<=n;i++)
if(!used[i])
{
state[u]=i;
used[i]=true;
dfs(u+1);
//恢复现场
state[u]=0;
used[i]=false;
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
递推
例题:简单斐波那契
以下数列 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 <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;
}
例题:费解的开关
你玩过“拉灯”游戏吗?
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
解题思路:
解题代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#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;
}
习题训练
递归实现组合型枚举
从 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 <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
const int N=30;
int n,m;
int way[N];//表示方案
void dfs(int u,int start)
{
//剪枝操作,如果把后面所有数都选上,都不够m个则一定无解
if(u+n-start<m) return;
//u==1一个没选,u==2选了一个数了
if(u==m+1)//恰好选了n个数
{
//把方案输出
for(int i=1;i<=m;i++) printf("%d ",way[i]);
puts("");
return;
}
for(int i=start;i<=n;i++)
{
way[u]=i;
dfs(u+1,i+1);
way[u]=0;//恢复现场
}
}
int main()
{
scanf("%d%d",&n,&m);
dfs(1,1);
return 0;
}
带分数
100可以表示为带分数的形式:100=3+69258714
还可以表示为:100=82+3546197
注意特征:带分数中,数字 1∼9分别出现且只出现一次(不包含 0)。
类似这样的带分数,100 有 11 种表示法。
输入格式
一个正整数。
输出格式
输出输入数字用数码 1∼9不重复不遗漏地组成带分数表示的全部种数。
数据范围
1≤N<10^6
输入样例1:
100
输出样例1:
11
输入样例2:
105
输出样例2:
6
解题思路
解题代码
解一:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int N=20;
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;
//x不能是0或者出现过
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);//更新下一层同时更新c即在后面添一位
st[i]=false;//恢复现场
}
}
void dfs_a(int u,int a)//u当前已经用来几个数字
{
//a>=n无解
if(a>=n) return;
if(a) dfs_c(u,a,0);//c=0
for(int i=1;i<=9;i++)//当前这一位可以用的数字
//如果当前没被用过
if(!st[i])
{
st[i]=true;//已用
dfs_a(u+1,a*10+i);//更新下一层同时更新a即在后面添一位
st[i]=false;//恢复现场
}
}
int main()
{
cin>>n;
dfs_a(0,0);
cout <<ans<<endl;
return 0;
}
解二:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int a = 0, b = 1;
int n;
cin >> n;
for (int i = 0; i < n; i ++ )
{
cout << a << ' ';
int c = a + b;
a = b, b = c;
}
cout << endl;
return 0;
}
例题:飞行员兄弟
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 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
解题思路
解题代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#define x first
#define y second
using namespace std;
typedef pair <int ,int >PII;
const int N=5;
char g[N][N],backup[N][N];
//得到第x行第y列映射的数是多少
int get(int x,int y)
{
return x*4+y;
}
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);
}
int main()
{
//先把所有灯泡读进来
for(int i=0;i<4;i++) cin>>g[i];
vector<PII> res;
//枚举下所有操作
for(int op=0;op<1<<16;op++)//小于1左移16就是2的16次方
{
//先把方案存下来
vector<PII>temp;
memcpy(backup,g,sizeof g);//备份
//按照该方案对所有灯泡进行操作
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(op>>get(i,j)&1)
{
temp.push_back({i,j});
turn_all(i,j);//摁下开关
}
//判断是否都是亮的状态
bool has_closed=false;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(g[i][j]=='+')
has_closed=true;
if(has_closed==false)
{
if(res.empty()||res.size()>temp.size()) res=temp;
}
memcpy(g,backup,sizeof g);//还原
}
//输出解
cout<<res.size()<<endl;
//输出每一个操作
for(auto op:res) cout <<op.x+1<<' '<<op.y+1<<endl;
return 0;
}
例题:翻硬币
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:**oo***oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:
**********
o****o****
输出样例1:
5
输入样例2:
*o**o***o***
*o***o**o***
输出样例2:
1
解题思路
解题代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=110;
int n;
char start[N],aim[N];
void turn(int i)
{
if(start[i]=='*') start[i]='o';
else start[i]='*';
}
int main()
{
cin>>start>>aim;
n=strlen(start);
int res=0;
//从前到后扫描每个字符
for(int i=0;i<n-1;i++)
if(start[i]!=aim[i])
{
turn(i),turn(i+1);
res++;
}
cout<<res<<endl;
return 0;
}