《算法笔记》编程笔记——第八章 搜索专题

《算法笔记》编程笔记——第八章 搜索专题

  • 递归解题模板

    • 求解最大值、最小值。返回的值设置为全局变量,条件值设置为全局变量。
    • 如果是函数表达式,则返回函数表达式。
    • 总的结构:第一步——递归边界;第二步——递归过程
    • 主函数中传入递归函数初始值
  • 一类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;
}
  • 广度搜索
    • 一般由队列来实现,且总是按层次的顺序进行遍历。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦想总比行动多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值