《算法笔记》编程笔记——第八章 搜索专题
-
递归解题模板
- 求解最大值、最小值。返回的值设置为全局变量,条件值设置为全局变量。
- 如果是函数表达式,则返回函数表达式。
- 总的结构:第一步——递归边界;第二步——递归过程
- 主函数中传入递归函数初始值
-
一类dfs常见问题
-
给定一个序列,枚举这个序列所有的子序列(可以不连续)。可以变形为,枚举从N个整数中选择K个数满足某种条件,并可能需要输出最优方案。
-
模板
//例题2:选择K符合条件的数,并输出 #include<cstdio> #include<iostream> #include<vector> using namespace std; int numK, num[10], sum, Max = -1, x, sqr = 0, n, k; vector<int> temp, ans; //因为选择的序列中的数字会需要加入与删除的变化,所以选择vector容器,而不是数组 void dfs(int index, int numK, int sum, int sqr){ if(numK == k && sum == x){ if(sqr > Max){ Max = sqr; ans = temp;//记录最优结果 } return; } if(numK > k || sum > x || index == n){//超过条件,结束 return; } //递归过程 temp.push_back(num[index]);//先进入容器 dfs(index + 1, numK + 1, sum + num[index], sqr + num[index] * num[index]);//对下一次进行判断 temp.pop_back();//失败会return,程序会至此,需要弹出刚才进入的数字 dfs(index + 1, numK, sum, sqr);//于是选择不选这个数,再进行程序 } int main(){ scanf("%d%d%d", &n, &k, &x); for(int i = 0; i < n; i++){ scanf("%d", &num[i]); } dfs(0, 0, 0, 0); for(int i = 0; i < ans.size(); i++){ printf("%5d", ans[i]); } return 0; }
-
-
补充递归类问题
-
全排列
//全排列之hashTable #include<cstdio> int hashTable[10] = {0}; int n, ans[10]; void dfs(int index){ if(index == n + 1){//已完成全部 for(int i = 1; i <= n; i++){ printf("%5d", ans[i]); } printf("\n"); return; } //递归过程 for(int i = 1; i <= n; i++){ if(hashTable[i] == 0){//未访问过 ans[index] = i;//加入数组 //标记模板,类似加入与删除 hashTable[i] = 1; dfs(index + 1); hashTable[i] = 0; } } } int main(){ scanf("%d", &n); dfs(1); return 0; }
-
全排列之回溯法,n皇后问题举例
//全排列之回溯法,举例n皇后问题 #include<cstdio> #include<math.h> int hashTable[10] = {0}; int ans[10], n, count = 0; void dfs(int index){ if(index == n + 1){//已到结果 for(int i = 1; i <= n; i++){ printf("%5d", ans[i]); } printf("\n"); count++; return; } //递归过程 for(int x = 1; x <= n; x++){//第x行 if(hashTable[x] == 0){//表示未被选择过 //进行递归条件优化 int flag = 1; //如果遍历前面的,看此次是否和前面的在同一对角线上,在则直接返回 for(int i = 1; i <= index - 1; i++){ //列号等于行号 if(abs(i - index) == abs(ans[i] - x)){ flag = 0; break; } } if(flag){//没有在对角线上的 ans[index] = x; hashTable[x] = 1; dfs(index + 1); hashTable[x] = 0;//更新,准备下一次递归 } } } } int main(){ scanf("%d", &n); dfs(1); printf("%d", count); return 0; }
-
全排列填数加dfs深搜,需要用两个步骤来解决。
-
问题描述【来自蓝桥杯第七届第3题】:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P7wj05lC-1614778592993)(C:\Users\dd\AppData\Roaming\Typora\typora-user-images\image-20200919101436480.png)]
-
思路:
①dfs函数先进行填数
②solve函数对填的数进行判断,是否满足条件。
-
代码如下:
#include <stdio.h> #include <math.h> int flag[3][4]; //表示哪些可以填数 int mpt[3][4]; //填数 bool visit[10]; int ans = 0; void init() //初始化 { int i,j; //一开始直接将flag数组设置为1,后续输出就出错,ans = 1。但是不明白为什么会这样, //总而言之,在初始化一个数组的时候,还是精确化比较好。 for(i = 0 ; i < 3 ; i ++) for(j = 0 ; j < 4 ; j ++) flag[i][j] = 1; //将地图中的不符合条件的点标记为0 flag[0][0] = 0; flag[2][3] = 0; } void Solve() { //因为对角线也可以走,所以总共有8个方向 int dir[8][2] = { 0,1,0,-1,1,0,-1,0,1,1,1,-1,-1,1,-1,-1}; int book = true; for(int i = 0 ; i < 3 ; i ++) { for(int j = 0 ; j < 4; j ++) { //判断每个数周围是否满足 if(flag[i][j] == 0)continue; for( int k = 0 ; k < 8 ; k ++) { int x,y; x = i + dir[k][0]; y = j + dir[k][1]; if(x < 0 || x >= 3 || y < 0 || y >= 4 || flag[x][y] == 0) continue; if(abs(mpt[x][y] - mpt[i][j]) == 1) book = false; } } } if(book) ans ++; } //深搜填数函数 void dfs(int index) { int x, y; //这里的处理很巧妙,将数字转为了地图上的具体位置。 //理解是,x等于增长的数字除以一行的长度,然后y是增长的数字取模一行的长度 //然后如果是不规则的图形,例如此题的左上角的空格和右下角的空格是无效的, //则通过flag数组来判断,如果flag数组显示为错,那么就让增长的数字加1(也就是次数的index加1) x = index / 4; y = index % 4; if( x == 3) //如果x到了地图外的一行,那么就代表填数已经结束,可以进行判断。 { Solve(); return; } if(flag[x][y]) //如果是在地图上 { for(int i = 0 ; i < 10 ; i ++) { if(!visit[i]) { visit[i] = true; mpt[x][y] = i; //填数 dfs(index+1); visit[i] = false; } } } else //如果地图上该空格是 无效的,那么就index加1 { dfs(index+1); } } int main() { init(); dfs(0); //从0开始 printf("%d\n",ans); return 0; }
-
-
全排列 + dfs判断连通性
-
题目描述【来自蓝桥杯第七届第七题】:剪邮票
如【图1.jpg】, 有12张连在一起的12生肖的邮票。
现在你要从中剪下5张来,要求必须是连着的。
(仅仅连接一个角不算相连)
比如,【图2.jpg】,【图3.jpg】中,粉红色所示部分就是合格的剪取。请你计算,一共有多少种不同的剪取方法。
请填写表示方案数目的整数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZdyDZVOE-1614778592999)(C:\Users\dd\AppData\Roaming\Typora\typora-user-images\image-20200919150936929.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W3rMcTlq-1614778593001)(C:\Users\dd\AppData\Roaming\Typora\typora-user-images\image-20200919151006995.png)]
-
题目分析:①首先发现,直接的dfs不满足题意,上图就是例子。②思路是,先对十二个数进行全排列,用到的是b数组和c++STL中的next_permutation函数。 然后将排列好的b数组的值复制到map数组中。 接下来利用dfs对map数组进行搜索,判断其连通性。 如果连通,那么这就是一种解答。③此处巧妙的地方在于,对b数组用了抓取法的方式来对map数组进行赋值,避免了去重操作,减少了程序运行的时间。
-
代码如下:
//全排列 (抓取法) + dfs来判断连通性问题 //很巧妙!!!!还需进行专题训练 #include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<cmath> using namespace std; int b[12] = {0,0,0,0,0,0,0,1,1,1,1,1}; //b数组存放接下来要进行全排列的数字,因为使用的是 next_permutation函数,所以 //数值大的在数组的最后面 int map[4][4]; int vis[4][4]; int dir[4][2] = {{0, 1}, {0, -1}, {1,0},{-1,0}}; //dfs函数判断连通性 void dfs(int x, int y){ vis[x][y] = 1; for(int i = 0; i < 4; i++){ int tx = x + dir[i][0]; int ty = y + dir[i][1]; //常见的dfs函数判断是否满足条件,通常写的是不满足条件然后continue if(tx < 0 || ty < 0 || tx >= 3 || ty >= 4){ continue; } if(!vis[tx][ty] && map[tx][ty]){ dfs(tx, ty); } } } int main(){ int ans = 0; do{ //不要忘记每一次循环都要初始化map和vis数组 memset(vis, 0, sizeof(vis)); memset(map, 0, sizeof(map)); int cnt = 0; //逐个把b数组中的值抓取放到map数组中,以实现不同的排列。这样做不会重复,很巧妙!!! for(int i = 0; i < 3; i++){ for(int j = 0; j < 4; j++){ map[i][j] = b[cnt++]; } } int num = 0; //判断连通分量的个数 for(int i = 0; i < 3; i++){ for(int j = 0; j < 4; j++){ //一旦找到一个起点,就进行dfs判断连通性,如果全连通,那么后序的vis数组被标记, //num数组的值只能为1. if(map[i][j] && !vis[i][j]){ num++; dfs(i, j); } } } if(num == 1)ans++; }while(next_permutation(b, b + 12)); //表示进行全排列组合 printf("%d", ans); return 0; }
-
此外,介绍一下next__permutation函数用法
//通常形式如下: do{ //通常会对数组(或者字符串)先进行排序 }while(next_permutation([first,last)); //①输出序列{1, 2, 3, 4}的全排列 int a[4] = {1, 2, 3, 4}; sort(a, a + 4); do{ for(int i = 0; i < 4; i++){ printf("%d ",a[i]); } printf("\n"); }while(next_permutation(a, a + 4)); //②输入任意一个字符串,输出其字典序的全排列 int main(){ string str; cin>>str; sort(str.begin(), str.end()); do{ cout<< str << endl; }while(next_permutation(str.begin(), str.end())); return 0; }
-
-
-
dfs搜索之多个表达式填空
-
题目描述【来自蓝桥杯第七届第六题】:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bpHWkKut-1614778593006)(C:\Users\dd\AppData\Roaming\Typora\typora-user-images\image-20200919134257176.png)]
-
题目分析:多个表达式,一开始想用全排列形式,但代码不知道该如何写。此处用的是dfs,因为总共有十二个数字,所以不可能进行十二次dfs。 题解中用到的方法是,①首先,将其划分4个平行的层次(因为总共有4个表达式),只要其中一个表达式不满足,就可以直接return;②利用index来作为a数组下标,那么每一个表达式有3个数,所以index=4,index=7,index=10,index=13分别是4个层次。③a[index] = i,外面加一个for循环,来表示第index个数填的值。
-
代码如下:
#include<cstdio> int a[15], vis[15]; int ans = 0; void dfs(int index){ if(index == 4){//index等于4的时候,前面三个数字已经定下来了,然后判断是否满足四个式子中的一个。 //此处先来判断乘法和除法,是因为这两种种数比较少,其实什么顺序都可以。 if(!(a[1]/a[2] == a[3] && a[1] % a[2] == 0)){ //如果不能满足这个表达式,那么就返回 return; } } else if(index == 7){ //之后的index会一直增加,所以用else if,前面一个式子就不用去判断了,加快程序的运行。 if(!(a[4] * a[5] == a[6])){ return; } }else if(index == 10){ if(!(a[7]+a[8]== a[9]))return; } else if(index == 13){ //最后一个判断,不是return,既然能走到最后一步,那么前面三个式子已经满足了,此时应该判断的是如果最后一个式子 //也满足条件,那么ans就会增加。 if(a[10]-a[11] == a[12]){ ans++; } return; } for(int i = 1; i <= 13; i++){ if(!vis[i]){ vis[i] = 1; a[index] = i; dfs(index + 1); vis[i] = 0; } } } int main(){ dfs(1); printf("%d", ans); return 0; }
-
-
记忆型递归之for循环
//蓝桥杯模拟赛9. 序列计数(记忆型递归),复杂度O(n^3) #include<cstdio> #include<math.h> long long flag[1001][1001] = {0}; long long num = 0; long long dfs(int pre, int cur){ if(flag[pre][cur] != 0){//返回已有的数 return flag[pre][cur]; } long long ans = 1;//设置为1,因为没有进行递归初始值是1 for(int i = 1; i <= abs(pre - cur) - 1; i++){ ans = (ans + dfs(cur, i)) % 10000; } flag[pre][cur] = ans;//记忆 return ans; } int main(){ int n; scanf("%d", &n); for(int i = 1; i <= n; i++){ num = (num + dfs(n, i)) % 10000; } printf("%lld", num); return 0; }
-
记忆型递归之表达式,例1,时间复杂度为O(n^2),时间 可控制在1s内。
//通常为纸上罗列关系,后用函数表达式来表达 #include<cstdio> #include<math.h> long long flag[1001][1001] = {0}; long long dfs(int pre, int cur){ //递归边界/递归终止条件 if(cur <= 0)return 0; //只有当最后一项为0时,才有确定的数 if(flag[pre][cur] != 0){ return flag[pre][cur]; } //在返回函数中顺便标记此次的个数 return flag[pre][cur] = (dfs(pre, cur - 1) + dfs(cur, abs(pre - cur) - 1) + 1) % 10000; } int main(){ int n; scanf("%d", &n); printf("%lld", dfs(n, n)); return 0; }
-
记忆型递归之表达式,例2,原理与例1一致
//计蒜客递归之弹簧板——记忆型递归 #include<cstdio> #include<algorithm> //min函数的头文件 using namespace std; long long flag[205] = {0}; int a[205], b[205]; int n; long long dfs(int index){ if(index > n)return 0; if(flag[index] != 0){ return flag[index]; } return flag[index] = min(dfs(index + a[index]), dfs(index + b[index])) + 1; } int main(){ scanf("%d", &n); for(int i = 1; i <= n; i++){ scanf("%d", &a[i]); } for(int i = 1; i <= n; i++){ scanf("%d", &b[i]); } printf("%lld", dfs(1)); return 0; }
-
-
迷宫问题,象棋问题(二维地图问题),表示方向的二维数组写法,可省去重复代码。
int dir[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
-
递归情况可分为两种:①找所有情况和找出路径②判断是否可行。第一种情况需要重置,第二种情况不需要重置。两者时间可相差1s。
//找一种情况的递归,可以通过一些条件来减少时间
#include<cstdio>
#include<string>
#include<iostream>
using namespace std;
int vis[11][11] = {0};
//标记前进方向
int dir[10][5] = {{-2, -1}, {-2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}, {2, -1}, {2, 1}};
//string loc[15];//用string会超时
char loc[15][15];
int f = 0;//标记f,根据f的值来判断是否可行,减少运行时间
//看点是否在地图内
int in(int x, int y){
if(x >= 0 && x < 10 && y >= 0 && y < 9){
return 1;
}else{
return 0;
}
}
void dfs(int x, int y){
//如果f已经为1, 表明已经找到一条路径,直接结束递归。
if(f){
return;
}
if(loc[x][y] == 'T'){
f = 1;
return;
}
//标记访问过
vis[x][y] = 1;
//进行8个方向的深搜
for(int i = 0; i < 8; i++){
int tx = x + dir[i][0];
int ty = y + dir[i][1];
if(in(tx, ty) && loc[tx][ty] != '#' && !vis[tx][ty]){
// if(dfs(tx, ty)){
// return 1;
// }
dfs(tx, ty);
}
}
//对于只要找一种正确情况的递归来说,不需要重置;重置是为了寻找所有情况,或者是为了标记路径。???为什么输出路径需要重置?
//重置
// vis[x][y] = 0;
// return;
}
int main(){
int x, y;
//读取地图
for(int i = 0; i < 10; i++){
// cin >> loc[i];
scanf("%s", loc[i]);
}
//寻找开始位置
for(int i = 0; i < 10; i++){
for(int j = 0; j < 9; j++){
if(loc[i][j] == 'S'){
x = i;
y = j;
}
}
}
dfs(x, y);
if(f){
printf("Yes\n");
}
else{
printf("No\n");
}
return 0;
}
- 广度搜索
- 一般由队列来实现,且总是按层次的顺序进行遍历。