目录
递归实现指数型枚举
题目
算法解析
递归实现排列型枚举
题目
算法解析
费解的开关
题目
算法解析
递归实现组合型枚举
题目
算法解析
带分数
题目
算法解析
飞行员兄弟
题目
算法解析
翻硬币
题目
算法解析
递归实现指数型枚举
题目
算法解析
对于1-n中任意的数来说,考虑这个数时只会面临两种情况
- 最终结果选择该数
- 最终结果不选择该数
我们只需要枚举每个数的两个状态:选和不选,考虑完后递归到下一层考虑下一个数即可
时间复杂度:2 × 2 .... × 2 = 2 ^ n
题中n为15,2^15 = 32768 ,符合要求
代码:
#include <iostream>
using namespace std;
const int N = 16;
int path[N];
bool st[N]; //st[i]为true,表示选第i个数,st[i]为false,表示不选该数
int n;
void dfs(int u)
{if(u > n){//前n个数全部考虑完了,输出方案for(int i = 1 ; i <= n ; ++i)if(st[i]) cout << i << " ";cout << endl;return;}//1.不选u,直接进入到下一层dfs(u+1);//2.选u,进入到下一层st[u] = true;dfs(u+1);st[u] = false; //恢复现场
}
int main()
{cin >> n;dfs(1); return 0;
}
递归实现排列型枚举
题目
算法解析
首先考虑排列型枚举与指数型枚举的区别
- 指数型枚举考虑的角度是对于1-n中的所有数,选和不选所产生的所有方案。
- 排列型枚举考虑的角度是对于1-n中的所有数必须选。它们分别可以放在哪个位置
所以排列型枚举的结果长度已经是固定了。我们分别考虑每个位置能放哪个元素。
假设n为3,可以画出如下树
- 只需要通过深度优先遍历,当遍历到树的叶子节点时就把答案数组输出
- 具体操作就是考虑答案数组的第一个位置填什么,若填的数是合法的,那么就递归到下一层
- 注意:不能填已经填过的数,例如第一个位置填的是3,那么我们填第二个位置时就不能填3了。我们可以使用一个st数组来标识这个位置能填什么
时间复杂度:O(n! * n)
该题中n为10,10! * 10 = 36288000, 由于代码常数比较小,所以该数量级其实符合题目要求
代码:
#include <iostream>
using namespace std;
const int N = 10;
int path[N]; //答案数组
bool st[N]; //st[i] 为true标识i不能选了,为false表示能选i
int n;
void dfs(int u)
{if(u > n) //前n个位置都考虑完了输出即可{for(int i = 1 ; i <= n ; ++i)cout << path[i] << " ";cout << endl;return;}for(int i = 1 ; i <= n ; ++i){if(!st[i]){path[u] = i; //第u个数填ist[i] = true;dfs(u+1);st[i] = false; //恢复现场}}
}
int main()
{cin >> n;dfs(1); //从第1个位置开始考虑return 0;
}
费解的开关
题目
算法解析
首先先考虑暴力枚举的方式,我们是否可以用指数型枚举的方式来枚举所有的按法呢?
实际上是可以的,每一个位置的按钮都只有按和不按两种状态,如果我们枚举所有方案,那么正确答案一定会被枚举到。
但时间复杂度为O(2^25 n),题目中2^25 = 33554432 , 而题目中n为500,显然必超时。
注意:对于有些相似题目来说,其实可以使用暴力解法的,这里把暴力解法也留在下面。
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6;
int T;
char g[N][N] , backup[N][N];
int dx[5] = {-1,0,1,0,0} , dy[5] = {0,0,0,-1,1};
//dx为方向向量,对应为上、中、下、左、右
//turn函数的功能是按下(x,y)位置的按钮,并处理它所带来的连锁反应
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){g[a][b] = ((g[a][b] - '0') ^ 1) + '0'; //按下开关(0变1,1变0)}}
}int main()
{cin >> T;while(T--){for(int i = 0 ; i < 5 ; ++i) scanf("%s",g[i]);//枚举所有方案int res = 10;memcpy(backup,g,sizeof g);for(int i = 0 ; i < 1 << 25 ; ++i){int step = 0;for(int j = 0 ; j < 25 ; ++j){if(i >> j & 1){step++;turn(j / 5 , j % 5);}}bool has_sol = true;//判断该方案是否为解for(int j = 0 ; j < 5 ; ++j)for(int k = 0 ; k < 5 ; ++k)if(g[j][k] == '0') has_sol = false;if(has_sol && step <= 6) res = min(res , step);memcpy(g,backup,sizeof backup);}if(res <= 6) cout << res << endl;else cout << "-1" << endl;}return 0;
}
接下来就是说明一下正解了。
假设我们已经知道了第一行应该怎么按最后才能得到解,接下来考虑第二行时我们应该怎么做呢?
由于第一行得按法已经确定了,那么此时第二行必须使得第一行当中为0的位置变为1,因为题目要求全部为1。如果第一行的某个位置在按完以后为1,那么我们在第二行按的时候就绝对不能按它对应的位置,因为他会使得第一行的1变为0
如下图:
第一行确定,假设如下:
由于第一行为0的位置下面的位置必须按,第一行为1的位置必须不按,所以第二行怎么按实际上是确定的,如下图:
此时第三行与第二行考虑的方式也一样,第二行为0的位置下面的位置必须得按,第二行为1的位置下面的位置必须不按。不再赘述了
当最终到了第5行,由于已经没有第6行来改变它的状态了,那么此时如果第5行为0那也没办法,说明没解。如果第五行为1说明有解,统一一下按的次数,如果小于6,则更新结果。
最后一个问题:第一行怎么按是怎么确定的呢?
我们可以通过指数型枚举的方式枚举第一行的按法,如果有正确按法,则枚举一定能枚举到
时间复杂度:2^5*5*n =80000,符合题目要求
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6;
int T;
char g[N][N] , backup[N][N];
int dx[5] = {-1,0,1,0,0} , dy[5] = {0,0,0,-1,1};
//dx为方向向量,对应为上、中、下、左、右
//turn函数的功能是按下(x,y)位置的按钮,并处理它所带来的连锁反应
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){g[a][b] = ((g[a][b] - '0') ^ 1) + '0'; //按下开关(0变1,1变0)}}
}int main()
{cin >> T;while(T--){for(int i = 0 ; i < 5 ; ++i) scanf("%s",g[i]);//枚举所有方案int res = 10;memcpy(backup,g,sizeof g);for(int i = 0 ; i < 1 << 5 ; ++i){int step = 0;for(int j = 0 ; j < 5 ; ++j){if(i >> j & 1){step++;turn(0 , j);}}//第一行确定了接下来的所有行的按法都确定了for(int j = 0 ; j < 4 ; ++j){for(int k = 0 ; k < 5 ; ++k)if(g[j][k] == '0'){step++;turn(j+1,k);}}bool has_sol = true;//判断该方案是否为解,第五行是否有0for(int j = 0 ; j < 5 ; ++j)if(g[4][j] == '0') has_sol = false;if(has_sol && step <= 6) res = min(res , step);memcpy(g,backup,sizeof backup);}if(res <= 6) cout << res << endl;else cout << "-1" << endl;}return 0;
}
递归实现组合型枚举
题目
算法解析
考虑一下组合型枚举和排列型枚举的区别
组合型枚举实际上是排列型枚举的一种特殊情况,在排列型枚举中(1,3,2)实际上是和(1,2,3)是完全不同的两个结果
但在组合型枚举中(1,3,2)和(1,2,3)是相同的,(1,3,2)和(3,2,1)也是相同的,只有当两个结果数组中有至少一个元素不同时才会记作答案
所以我们可以约定,组合型枚举的答案数组一定是单调递增的,即假设有3个元素1,2,3,那么我们只枚举(1,2,3)而不枚举(2,1,3)和(3,2,1)这两种不是单调递增的情况。这实际上完成了排列型枚举剪枝成组合型枚举
如何实现?
当我们枚举第u个元素时,从第u-1选择的元素+1开始枚举即可
代码:
#include <iostream>
using namespace std;
const int N = 25;
int path[N] , n , m;void dfs(int u)
{if(u > m) // 如果已经选了m个元素{for(int i = 1 ; i <= m ; ++i)cout << path[i] << " ";cout << endl;return;}for(int i = path[u-1] + 1 ; i <= n ; ++i){path[u] = i;dfs(u+1);}
}int main()
{cin >> n >> m;dfs(1); //从第1个位置开始考虑return 0;
}
带分数
题目
算法解析
简单来说,该题就是输入一个n,枚举出a、b、c三个数,使得a + b/c = n。具体要求如下:
- a + b / c = n
- b必须能整除c ,即b % c = 0
- a、b、c中所有位数,必须包含1-9,这9个数
通过如上分析我们可以得出如下做法:
- 枚举全排列(排列型枚举) 1-9的数
- 枚举的全排列一定包含且只出现一次1-9的数
- 把全排列分成三段,枚举全排列中a、b、c的值
- 检查上述具体要求是否满足即可
时间复杂度:全排列的复杂度和枚举位数的复杂度 :9! * 9 * 45 (尽管有些高,但经过测试还是能过得)
如下代码:
#include <iostream>
using namespace std;
const int N = 10;
int path[N]; //全排列数组
bool st[N]; //判重数组,用于求全排列
int n;
int res; //结果bool check(int a , int b , int c)
{if(!a || !b || !c) return false; //题目要求不能出现0if(a + b / c == n && b % c == 0) return true; return false;
}void dfs(int u)
{if(u == 9){for(int i = 0 ; i < 9 ; ++i){for(int j = i + 1 ; j < 9 ; ++j){//[0,i) = a//[i,j) = b//[j,9) = cint a = 0 , b = 0 , c = 0;int k = 0;while(k < i){a = a * 10 + path[k];++k;}while(k < j){b = b * 10 + path[k];++k;}while(k < 9){c = c * 10 + path[k];++k;}if(check(a,b,c)) res ++; //判断a、b、c是否满足题目要求}}}for(int i = 1 ; i <= 9 ; ++i){if(!st[i]){path[u] = i;st[i] = true;dfs(u+1);st[i] = false;}}
}
int main()
{cin >> n;dfs(0);cout << res << endl;return 0;
}
飞行员兄弟
题目
算法解析
这题与费解的开关很类似,都是类似于是否按开关的问题,首先考虑他们两个的区别:
- 这题的按开关所产生的影响比费解的开关要大的多。在费解的开关中每一次按开关仅仅会影响上下左右四个格子,正因为这种性质,我们可以从1-n-1层开关的按法,唯一的确定第n层的按法。但这题按下开关后,一行一列都会受到影响,换句话来说对于第一层开关的状态不能将下一层唯一确定,因为它会受到下面所有层的按开关的影响
- 幸运的是这题的数据范围比较小,只有4x4的矩阵,且无多组测试用例,这也就意味着我们可以枚举所有的可能的方案,因为每一个开关只会存在是否按两种状态,枚举所有的按法,也就一定包含了正解
所以最终解法就是:枚举所有的按法,确定结果的按法。
注意:该题需要我们输出最小解,如果存在多个最小解则按照从上到下,从左到右的方案来输出
我们在枚举按法时,i是从0到2^16次方的,只需要遇到相同步数的解时不要更新结果,当遇到比当前解更小的解时才更新结果即可保证这个性质
时间复杂度:具体的真不好算了,粗略算成2^16 * 16吧,反正能擦边过
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
typedef pair<int,int> PII;
const int N = 5;
char g[N][N] , backup[N][N];void turn(int x , int y)
{for(int i = 0 ; i < 4 ; ++i)if(g[x][i] == '+') g[x][i] = '-';else g[x][i] = '+';for(int i = 0 ; i < 4 ; ++i)if(g[i][y] == '+') g[i][y] = '-';else g[i][y] = '+';if(g[x][y] == '+') g[x][y] = '-';else g[x][y] = '+';
}int main()
{for(int i = 0 ; i < 4 ; ++i)scanf("%s",g[i]);memcpy(backup,g,sizeof g); //保存一下原始状态vector<PII> path; //结果路径int res = 1e9; //最小解//1.先枚举所有的按法for(int i = 0 ; i < 1 << 16 ; ++i){int step = 0;vector<PII> temp;for(int j = 0 ; j < 16 ; ++j){if(i >> j & 1) // 如果为1则表示按下开关,否则表示不按{turn(j / 4 , j % 4);step++;temp.push_back({j / 4 , j % 4});}}bool has_sol = true; //判断是否有解for(int j = 0 ; j < 4 ; ++j)for(int k = 0 ; k < 4 ; ++k)if(g[j][k] == '+') has_sol = false;if(has_sol && step < res) //如果有解,且比当前的最小解还小就更新一下{res = min(res , step);path = temp;}memcpy(g,backup,sizeof backup);//还原原始状态}cout << res << endl;for(int i = 0 ; i < path.size() ; ++i){//题目中下标是从1开始的所以需要+1cout << path[i].first + 1 << " " << path[i].second + 1<< endl;}return 0;
}
翻硬币
题目
算法解析
对于第一个字符串来说,它的第i个位置的状态仅由第i-1个位置和第i个位置决定
依次类推,第i-1个位置的状态仅由i-2位置和i-1位置决定,直到第0个位置,仅由第0个位置决定
那么第0个位置是不是要翻取决于它是否与目标字符串第0个位置相等,同理第1个位置是否要翻取决于遍历到第1个位置时它是否与目标字符串第1个位置相等
换句话来说,我们从前往后遍历第一个字符串,当第i个位置与目标字符串不符合时我们就翻一下硬币,最终如果有解那么一定会翻成跟目标字符串一样且是最小的解
代码如下:
#include <iostream>
#include <string>
using namespace std;string src , dst;
void turn(int i)
{src[i] = (src[i] == '*' ? 'o' : '*');if(i+1 < src.size())src[i+1] = (src[i+1] == '*' ? 'o' : '*');
}
int main()
{cin >> src >> dst;int res = 0;for(int i = 0 ; i < src.size() ; ++i){if(src[i] != dst[i]){turn(i);res++;}}cout << res << endl;return 0;
}