例题1 递归实现指数型枚举
递推和递归问题,画树解决
题目
从 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 = 0;i < n;i++) //循环遍历这个数组
{
if(st[i + 1] == 1)
{
printf("%d ",i + 1);
}
}
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);
}
例题2 递归实现排列型枚举
递归和递推的时候切记恢复原状
题目
把 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<algorithm>
#include<iostream>
using namespace std;
const int N = 10; //表示数据的范围
int n;
int state[N]; //表示状态 0代表空 1~n代表有
bool used[N]; //ture 表示用过 false表示没用过
void dfs(int u) //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); //表示从第一个位置开始
}
例题3 简单斐波那契
题目
以下数列0 1 1 2 3 5 8 13 21 …被称为斐波纳契数列。
这个数列从第3项开始,每一项都等于前两项之和。
输入一个整数N,请你输出这个序列的前N项。
输入格式
一个整数N。
输出格式
在一行中输出斐波那契数列的前N项,数字之间用空格隔开。
数据范围
0<N<46
输入样例:
5
输出样例:
0 1 1 2 3
代码
#include<bits/stdc++.h>
using namespace std;
int n,feibo[99999];
int main(){
scanf("%d",&n);
feibo[1]=0,feibo[2]=1;
for(int i=3;i<=n;++i) feibo[i]=feibo[i-1]+feibo[i-2];
for(int i=1;i<=n;++i) printf("%d ",feibo[i]);
return 0;
}
例题4 费解的开关
递推和位运算
题目
你玩过“拉灯”游戏吗?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<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
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];
int b = y + dy[i];
if (a < 0 || a >= 5 || b < 0 || b >= 5) continue; //在边界外,直接忽略即可
g[a][b] ^= 1; //这个操作非常牛逼,位运算 主要是用来把二进制最后一位改变 0 变成 1 1 变成 0
}
}
int main()
{
int T; //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); //先把g备份给 backup
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;
}
例题5 递归实现组合性枚举
题目
从 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<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
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;
//边界的情况
if (u == m + 1) //够了
{
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;
}
非递归实现代码
这题直接二进制表示所有情况,
也就是for(i=1;i<1<<n;i++)
如果1的个数是m,就输出1的位置
简单的二进制操作找1的个数和位置
#include<queue>
#include<bitset>
#include<string>
#include<iostream>
#include<cstdio>
#include<map>
#include<algorithm>
using namespace std;
const int N=1<<21,inf=0x3f3f3f3f;
int n,m;
map<int ,int > m1;
struct point{
vector<int > v1;
}p1[N];
int lowbit(int x)
{
return x&(-x);
}
bool cmp(point x,point y){
int ptail=0;
while(x.v1[ptail]==y.v1[ptail])ptail++;
return x.v1[ptail]<y.v1[ptail];
}
int main(){
cin>>n>>m;
bitset<26> b1;
m1[1]=0;
int q=2,tail=0;
for(int i=1;i<=32;i++){
m1[q]=i;
q*=2;
}
for(int a=0;a<(1<<n);a++){
b1|=a;
if(b1.count()==m){
int pa=a;
while(pa){
p1[tail].v1.push_back(m1[lowbit(pa)]+1);
pa^=lowbit(pa);
}
tail++;
}
b1&=0;
}
sort(p1,p1+tail,cmp);
for(int a=0;a<tail;a++){
for(int b=0;b<m;b++)printf("%d ",p1[a].v1[b]);
printf("\n");
}
}
例题6 带分数
题目
100 可以表示为带分数的形式:100=3+69258714
还可以表示为:100=82+3546197
注意特征:带分数中,数字 1∼9 分别出现且只出现一次(不包含 0)。
类似这样的带分数,100 有 11 种表示法。
输入格式
一个正整数。
输出格式
输出输入数字用数码 1∼9 不重复不遗漏地组成带分数表示的全部种数。
数据范围
1≤N<106
输入样例1:
100
输出样例1:
11
输入样例2:
105
输出样例2:
6
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 20;
int n;
bool st[N],backup[N];
int ans;
bool check(int a,int c)
{
int b = n * c - a * c;
if (!a || !b || !c) return false; //三者当中任意一者不存在
memcpy(backup,st,sizeof st); //把st数组给了backup数组
while(b) //判定b 和 a c 有没有重复出现的数字
{
int x = b % 10; //取个位
b /= 10; //个位删掉
if(!x || backup[x]) return false; //x已经被占用的情况
backup[x] = true; //被占用
}
for (int i = 1;i <= 9;i++)
{
if (!backup[i]) return false; //backup 中还有没填满的数
}
return true;
}
void dfs_c(int u,int a,int c)
{
if(u == n) return; //满了的情况
if(check(a,c)) ans++; //a 和 c都满足情况 边界输出
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) //u 遍历到第几位了 a 就是a 的值
{
if (a >= n) return; //a>=n 满了
if (a) dfs_c(u,a,0); //如果a 存在的话,就递归c
//输出
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;
}
例题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
分析
因为i,j 的数据范围小,所以暴搜解决问题。
1.枚举所有的方案
2.按照方案对灯泡操作
3.判断
4.按照字典序排列(从小到大枚举,最后就是字典序)
代码
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 4;
typedef pair<int,int > PII;
char g[N][N],backup[N][N];
vector<PII> res; //定义一个动态的数组 PII 是 两个数为一对
char turn(char c)
{
if (c == '+') return '-';
return '+';
}
//改变一整行和一整列
void change(int x,int y)
{
for(int i = 0;i < 4;i++) g[x][i] = turn(g[x][i]);
for(int j = 0;j < 4;j++) g[j][y] = turn(g[j][y]);
g[x][y] = turn(g[x][y]);
// for(int i=0;i<4;i++)
// {
// if(i!=y)
// g[x][i]=turn(g[x][i]);
// g[i][y]=turn(g[i][y]);
// }
}
bool check()
{
for(int i = 0;i < 4;i++)
{
for(int j = 0;j < 4;j++)
{
if(g[i][j] == '+')
return false;
}
}
return true;
}
int main()
{
//输入数据
for(int i = 0;i < 4;i ++) cin >> g[i];
//遍历所有的可能
for(int k = 0; k < 1 << 16;k++)
{
vector<PII> temp; //建立一个临时的情况
memcpy(backup,g,sizeof g); //备份情况
//遍历16位上的情况
for (int i = 0;i < 4;i++)
{
for(int j = 0;j < 4;j++)
{
if(k >> (i * 4 + j) & 1)
{
change(i , j);
temp.push_back({i,j}); // 把这一步输入
}
}
}
if(check()) //如果全部是 '-' 的情况 也就是全都开了
{
if(res.empty() || res.size() > temp.size()) res = temp;
}
memcpy(g,backup,sizeof g); //恢复
}
cout << res.size() << endl;
for(auto op : res) cout << op.first + 1 << " " << op.second + 1<< endl;
return 0;
}
例题8 翻硬币
题目
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:oo*oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:
oo
输出样例1:
5
输入样例2:
ooo***
ooo***
输出样例2:
1
分析
从前往后翻就完事了,不走回头路
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#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()
{
int res = 0; //步骤数
cin >> start >> aim;
n = strlen(start);
for(int i = 0;i < n;i++)
{
if(start[i] != aim[i])
{
turn(i);
turn(i + 1);
res += 1;
}
}
cout << res << endl;
return 0;
}