例題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;
}