搜索与回溯讲解

文章目录

  • 深搜
    • 方向向量:
    • DFS代码:
    • 题目讲解:
      • 八皇后问题
      • 字符序列
      • 自然数的拆分
  • 广搜
    • BFS代码:
    • 题目讲解:
      • 瓷砖
      • 关系网络
  • bfs与dfs的用途与区别
  • 搜索剪枝
    • 可行性剪枝
    • 最优性剪枝
    • 记忆化搜索
    • 搜索顺序剪枝
    • 题目:
      • [NOIP2002 提高组] 字串变换
  • 状压搜索
    • 例题:

深搜

DFS
一股莽到底,然后再找下一个,如果无法拓展,则退回一步到上一个状态,再按照原先设定的规则顺序重新寻找一个状态拓展。
例子:走迷宫,不走到路的尽头不回头

得到的序列:V0,V1,V2,V6,V5,V3,V4
代码:

void dfs(int dep, 参数表 );{自定义参数 ;if( 当前是目标状态 ){输出解或者作计数、评价处理 ;}elsefor(i = 1; i <= 状态的拓展可能数 ; i++)if( 第 i 种状态拓展可行 ){维护自定义参数 ;dfs(dep+1, 参数表 );}
}

回溯:
“回溯法”也称“试探法”。它是从问题的某一状态出发,不断“试探”着往前走一步,当一条路走到“尽头”,不能再前进(拓展出新状态)的时候,再倒回一步或者若干步,从另一种可能的状态出发,继续搜索,直到所有的“路径(状态)”都一一试探过。这种不断前进、不断回溯,寻找解的方法,称为“回溯法”。

void search(int dep){自定义参数 ;if( 当前是目标状态 ){输出解或者作计数和评价处理 ;}elsefor(i = 1; i <= 状态的拓展可能数 ; i++)if( 第 i 种状态拓展可行 ){保存现场 ( 断点 ), 维护自定义参数 ;search(dep+1);恢复现场 , 回溯到上一个断点继续执行 ;}
}

深度优先搜索可以采用递归(系统栈)和非递归(手工栈)两种方法实现

方向向量:

int dir[4][2]= {0,1,0,-1,1,0,-1,0}; // 方向向量,(x,y)周围的四个方向
-------
int dx[]={1,0,-1,0}
int dy[]={0,1,0,-1}

DFS代码:

/*
该DFS 框架以2D 坐标范围为例,来体现DFS 算法的实现思想。
*/
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
const int maxn=100;
bool vst[maxn][maxn]; // 访问标记
int map[maxn][maxn]; // 坐标范围
int dir[4][2]= {0,1,0,-1,1,0,-1,0}; // 方向向量,(x,y)周围的四个方向
bool CheckEdge(int x,int y) { // 边界条件和约束条件的判断if(!vst[x][y] && ...) // 满足条件return 1;else // 与约束条件冲突return 0;
}
void dfs(int x,int y) {vst[x][y]=1; // 标记该节点被访问过if(map[x][y]==G) { // 出现目标态G...... // 做相应处理return;}for(int i=0; i<4; i++) {if(CheckEdge(x+dir[i][0],y+dir[i][1])) // 按照规则生成下一个节点dfs(x+dir[i][0],y+dir[i][1]);}return; // 没有下层搜索节点,回溯
}
int main() {......return 0;
}

题目讲解:

八皇后问题

在国际象棋棋盘上(8*8)放置八个皇后,使得任意两个皇后之间不能在同一行,同一列,也不能位于同于对角线上。问共有多少种不同的方法,并且按字典序从小到大指出各种不同的放法。

思路:
枚举1~N的全排列,每列举完一组,然后就在该组排列中测试当前方案是否合法(排列中每个元素与该排列中其他的元素不在横、竖、斜线上)
代码:

#include <cstdio>
#include <cstdlib>const int MAXN = 100;
int N;
bool hashTable[MAXN]  = {false};
int P[MAXN];
int count = 0;void DFS(int index){//朴素算法if(index == N + 1){bool flag = true;//flag为true,表示当前方案合法for (int i = 1; i <= N ; ++i)//遍历两个皇后,{for (int j = i + 1; j <= N; ++j){//第四象限内,P[y]为纵坐标,i、j为横坐标if(abs(i - j) == abs(P[j] - P[i])){//如果在一条对角线上,也就是正方形相对的两个端点flag = false;}}}if(flag == true) {count++;for (int i = 1; i <= N; ++i){printf("%d", P[i]);if(i <= N - 1) printf(" ");}printf("\n");}}for (int i = 1; i <= N; ++i)//全排列{if(hashTable[i] == false){P[index] = i;hashTable[i] = true;DFS(index + 1);hashTable[i] = false;}}
}int main(){scanf("%d", &N);DFS(1);if(count == 0) printf("no solute!\n");return 0;
}

字符序列

题目描述

从三个元素的集合[A,B,C]中选取元素生成一个 N 个字符组成的序列,使得没有两个相邻的子序列(子序列长度=2)相同。例:N = 5 时 ABCBA 是合格的,而序列 ABCBC 与 ABABC 是不合格的,因为其中子序列 BC,AB 是相同的。
对于由键盘输入的 N(1<=N<=12),求出满足条件的 N 个字符的所有序列和其总数。

思路:
依旧靠模拟现在数组中的元素并且判断相邻两个
元素构成的子序列是否是满足条件的解,是就进行
计数并搜索下一层

代码:

#include<cstdio>
#define ll long long
ll a[1000005],n,sum;
bool check(int p)
{return p>3&&a[p-3]*10+a[p-2]==a[p-1]*10+a[p];
}
void dfs(int p)
{ll i;//要每次都重新定义i的变量,不然在调用的时候会改变外层搜索的i变量 for(i=1;i<=3;i++){a[p]=i;if(check(p))//只用判断相邻的字符串,之前的是因为判断了所有的情况 continue;else{if(p==n)sum++;elsedfs(p+1);}}
}
int main()
{scanf("%lld",&n);dfs(1);printf("%lld",sum);
}

自然数的拆分

题目描述:

任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
当n=7共14种拆分方法:
7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4

输入一个n,按字典序输出具体的方案。

输入样例
7
输出样例
1+1+1+1+1+1+1
1+1+1+1+1+2
1+1+1+1+3
1+1+1+2+2
1+1+1+4
1+1+2+3
1+1+5
1+2+2+2
1+2+4
1+3+3
1+6
2+2+3
2+5
3+4

题解:
层层分解,按照dfs的顺序进行

代码:

#include<cstdio>
int a[1005]={1},n;
int print(int t)
{int i;for(i=1;i<t-1;i++)printf("%d+",a[i]);if(a[t-1]!=n)printf("%d\n",a[t-1]);
}
int search(int s,int t)
{int i;if(s==0)print(t);for(i=a[t-1];i<=s;i++){a[t]=i;search(s-i,t+1);}
}
int main()
{scanf("%d",&n);search(n,1);
}

广搜

BFS
应用产生式规则和控制策略生成多层结点
广搜是扩散式的
例子:眼镜掉在地上,趴在地板上找,一开始先搜最接近的地方,如果没有再摸远一点的地方,距离依次增加

得到的一个序列为 V0,V1,V2,V3,V4,V6,V5。
宽度优先搜索是一种“盲目”搜索,所有结点的拓展都遵循“先进先出”的原则,所以采用“队列”来存储这些状态。宽度优先搜索的算法框架如下:

void BFS{while (front <= rear){// 当队列非空时做,front 和 rear 分别表示队列的头指针和尾指针      if (找到目标状态)做相应处理(如退出循环输出解、输出当前解、比较解的优劣);else{拓展头结点 ;if( 拓展出的新结点没出现过 ){rear++;将新结点插到队尾 ;}}front++;// 取下一个结点}
}

BFS代码:

#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int maxn = 100;
bool vst[maxn][maxn]; // 访问标记
int dir[4][2] = {0,1,0,-1,1,0,-1,0}; // 方向向量
struct State { // BFS 队列中的状态数据结构int x, y; // 坐标位置int Step_Counter; // 搜索步数统计器
};
State a[maxn];
bool CheckState(State s) { // 约束条件检验if(!vst[s.x][s.y] && ...) // 满足条件return 1;else // 约束条件冲突return 0;
}
void bfs(State st) {queue <State> q; // BFS 队列State now, next; // 定义2 个状态,当前和下一个st.Step_Counter = 0; // 计数器清零q.push(st); // 入队vst[st.x][st.y] = 1; // 访问标记while(!q.empty()) {now = q.front(); // 取队首元素进行扩展if(now == G) { // 出现目标态,此时为Step_Counter 的最小值,可以退出即可...... // 做相关处理return;}for(int i = 0; i < 4; i++) {next.x = now.x + dir[i][0]; // 按照规则生成下一个状态next.y = now.y + dir[i][1];next.Step_Counter = now.Step_Counter+1; // 计数器加1if(CheckState(next)) { // 如果状态满足约束条件则入队q.push(next);vst[next.x][next.y] = 1; //访问标记}}q.pop(); // 队首元素出队}return;
}
int main() {......return 0;
}

题目讲解:

瓷砖

在一个 w×h 的矩形广场上,每一块 1×1 的地面都铺设了红色或黑色的瓷砖。小林同学站在某一块黑色的瓷砖上,他可以从此处出发,移动到上、下、左、右四个相邻的且是黑色的瓷砖上。现在,他想知道,通过重复上述移动所能经过的黑色瓷砖数。

样例:

样例输入
11 9
. # . . . . . . . . .
. # . # # # # # # # .
. # . # . . . . . # .
. # . # . # # # . # .
. # . # . . @ # . # .
. # . # # # # # . # .
. # . . . . . . . # .
. # # # # # # # # # .
. . . . . . . . . . .
样例输出
59

题解:
找到小林的初始位置“@”,并把坐标入队,作为队头元素。宽度优先搜索,检查队头元素的上、下、左、右四个位置是否是黑色瓷砖“.”,是则入队,……不断取出队头元素进行四个方向的拓展,直到队列为空。
为了避免一个位置被多次重复走到,定义一个布尔型数组 vis[i,j]用来判重,位置(i,j)为黑色瓷砖设置为 true,红色的或者走过的瓷砖设置为 false
最后队列内元素数量
在本题中其实还牵扯一个应用:联通块
代码:

#include<iostream>
#include<queue>
using namespace std;const int N = 25;
char map[N][N];
int n, m;
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0 , -1};
int ans;// 判断 (x, y) 是否在地图中
bool inmap(int x, int y) {return x >= 0 && x < m && y >= 0 && y < n;
}void bfs(int x, int y) {queue<pair<int, int>> q;// 首先将当前位置改为 '#' 表示已经走过了map[x][y] = '#';// 将x, y加入到队列中q.push({x, y});while (!q.empty()) {// 取出队列的头结点pair<int, int> xy = q.front();q.pop();// 向4个方向拓展for (int i = 0; i < 4; i++) {int x1 = xy.first + dx[i], y1 = xy.second + dy[i];// 如果下一个要走的点在地图内并且为黑色砖块,那么就将该点改为 '#' 并加入到队列中if (inmap(x1, y1) && map[x1][y1] == '.') {q.push({x1, y1});map[x1][y1] = '#';ans++;}}}
}int main() {// 输入 n 列 m 行cin >> n >> m;while (n != 0  && m != 0) {for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {cin >> map[i][j];}}queue<pair<int, int>> p;// 找到开始的地点int x, y;for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (map[i][j] == '@') {x = i;y = j;break;}}}// bfs// 每次将 ans 初始化为 1,因为出发点算一个ans = 1;bfs(x, y);cout << ans << endl;cin >> n >> m;}return 0;
}

关系网络

描述
有N个人,编号为1到N,其中有一些人互相认识,现在x想认识y,可以通过他所认识的人来认识更多的人
如果x认识y,y认识z,则x可以通过y来认识z,求出x最少需要通过多少人才能认识y

输入
5 1 5
0 1 0 0 0
1 0 1 1 0
0 1 0 1 0
0 1 1 0 1
0 0 0 1 0
输出:
2

思路:
典型的BFS,直接搜就行了,最后输出时记得减一,因为要到达的人自己不算。
代码:

#include<bits/stdc++.h>
using namespace std;
int n,x,y;
struct node{int num;//编号 int t;//步数 node(){}node(int sum,int tt){num=sum;t=tt;}
};
int mp[101][101];//图
bool flag[101];//标记
queue<node> q;
void bfs()
{q.push(node(x,0));flag[x]=true;//打标记 while(!q.empty()){node head=q.front();q.pop();if(head.num==y){cout<<head.t-1;//一定要减一 return;}for(int i=1;i<=n;i++){if(mp[head.num][i]&&!flag[i]){flag[i]=true;q.push(node(i,head.t+1));}}}}int main(){cin>>n>>x>>y;for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){cin>>mp[i][j];//存图 }}bfs();
}

bfs与dfs的用途与区别

1.BFS是用来搜索最短径路的解是比较合适的,比如求最少步数的解,最少交换次数的解,因为BFS搜索过程中遇到的解一定是离根最近的,所以遇到一个解,一定就是最优解,此时搜索算法可以终止。这个时候不适宜使用DFS,因为DFS搜索到的解不一定是离根最近的,只有全局搜索完毕,才能从所有解中找出离根的最近的解。(当然这个DFS的不足,可以使用迭代加深搜索ID-DFS去弥补)
2.空间优劣上,DFS是有优势的,DFS不需要保存搜索过程中的状态,而BFS在搜索过程中需要保存搜索过的状态,而且一般情况需要一个队列来记录。
3.DFS适合搜索全部的解,因为要搜索全部的解,那么BFS搜索过程中,遇到离根最近的解,并没有什么用,也必须遍历完整棵搜索树,DFS搜索也会搜索全部,但是相比DFS不用记录过多信息,所以搜素全部解的问题,DFS显然更加合适。

搜索剪枝

常用的搜索有Dfs和Bfs。

Bfs的剪枝通常就是判重,因为一般Bfs寻找的是步数最少,重复的话必定不会在之前的情况前产生最优解。

深搜,它的进程近似一颗树(通常叫Dfs树)。

而剪枝就是一种生动的比喻:把不会产生答案的,或不必要的枝条“剪掉”。

剪枝的关键就在于剪枝的判断:什么枝该剪,什么枝不该剪,在什么地方减。
正确性,准确性,高效性。

常用的剪枝有:可行性剪枝、最优性剪枝、记忆化搜索、搜索顺序剪枝。

可行性剪枝

如果当前条件不合法就不再继续搜索,直接return。这是非常好理解的剪枝

最优性剪枝

如果当前条件所创造出的答案必定比之前的答案大,那么剩下的搜索就毫无必要,甚至可以剪掉。

我们利用某个函数估计出此时条件下答案的‘下界’,将它与已经推出的答案相比,如果不比当前答案小,就可以剪掉。

比如:
在搜索取和最大值时,如果后面的全部取最大仍然不比当前答案大就可以返回。
在搜和最小时同理,可以预处理后缀最大/最小和进行快速查询。

long long ans=987474477434487ll;
... Dfs(int x,...)
{if(x... && ...){ans=....;return ...;}if(check2(x)>=ans)return ...;    //最优性剪枝 for(int i=1;...;++i){vis[...]=1; dfs(...);vis[...]=0;}
}

记忆化搜索

(之后dp会讲)

搜索顺序剪枝

在一些迷宫题,网格题,或者其他搜索中可以贪心的题,搜索顺序显得十分重要
其实在迷宫、网格类的题目中,以左上->右下为例,右下左上就明显比左上右下优秀。

题目:

[NOIP2002 提高组] 字串变换

题目描述:
给字符串A和B,然后给出最多6个变换规则:
A1 -> B1
问A能否变成B

输入
abcd xyz
abc xu
ud y
y yz
3

题解:
题目一共就6个变换规则,而且问最少步数,基本上就锁定是广搜,不仅可以寻找解而且还能判断步数
起点为a串,搜索目标为b串,中间的路径是给出的变换关系
我们用一个map来记录某个串是否被搜索过
对于串str,我们从第i为看是否能用第j种手段改变,如果拼接出的串是合法的,那么我们就把这个串继续压入队列,再次搜索,中间记录一下步数step和ans。
代码:

#include<bits/stdc++.h>     //万能头文件
using namespace std;
string a,b;                 //字符串A与字符串B
string sa[8],sb[8];         //存放6种转换方式
map<string,int> map1;       //用map存放已经宽搜过的字符串,用来判重剪枝(否则会超时)
int l;                      //有l种转换方式
queue<string> q;            //存放转换出来的字符串
queue<int> bb;              //存放当前转换出来的字符串已经使用的步数
int bfs()
{int i,j,k,m,n;string s,ss;while (q.empty()==0&&q.front()!=b&&bb.front()<=10)      //当还能继续转换且没转换出字符串B且步数也没有超出10步时进行宽搜{if (map1[q.front()]==1)         //剪枝:如果当前字符串已经宽搜过了,就弹出,进入下一次循环.{q.pop();bb.pop();continue;}map1[q.front()]=1;              //记录下该字符串for (i=1;i<=l;i++)              //循环出每一种转换方式{   s=q.front();                //将S赋值为当前要操作的字符串while (1)                   //找出子串sa[i]的所有位置{   m=s.find(sa[i]);        //在S里查找子串sa[i]的第一次出现位置if (m==-1) break;       //如果全找出来(找不到)了,就结束循环ss=q.front();           //将SS赋值为当前要操作的字符串ss.replace(m,sa[i].size(),sb[i]);   //在SS中用子串sb[i]替换掉S里第一次出现的子串sa[i]q.push(ss);             //将转换后的SS压入队列bb.push(bb.front()+1);  //将转换后的SS已经使用的步数压入队列s[m]='~';              //将S里子串sa[i]的第一次出现位置随便换成另一种无关的字符,//这样就可以查找到S里子串sa[i]的下一个出现位置}}q.pop();                        //将操作过的字符串弹出队列bb.pop();                       //操作过的字符串已经用过的步数一块弹出}if (q.empty()==1||bb.front()>10) return -1;//没法再进行宽搜,或者超出步数,就返回-1else return bb.front();                 //否则,就是找到了,便返回最少使用步数
}
int main()
{int i,j,k,m,n;cin>>a>>b;                          //读入字符串A与字符串Bl=1;while (cin>>sa[l]>>sb[l]) l++;      //读入转换方式l--;                                //l初始值为1,所以要减1,才能表示转换方式的数量if (l==0&&a!=b)                     //若果没有转换方式且A也不等于B,直接输出"NO ANSWER!"(其实这步可以不要){cout<<"NO ANSWER!";return 0;}q.push(a);                          //将字符串A压入队列bb.push(0);                         //将初始步数0压入队列k=bfs();                            //宽搜if (k==-1)                          //返回-1说明NO ANSWER!{cout<<"NO ANSWER!";return 0;}cout<<k;                            //输出最小步数
}

状压搜索

就是在搜索过程中应用了状态压缩思想
状态压缩:原状态不容易表达或者状态太多,内存不够用,所以用一个数的二进制表示状态可以节省很多内存空间(当然也有使用的局限性)
例子:
一排10个座位,编号从左到右分别是1到10
其中第2,4,6,8的位置上没人,我们如何记录这个状态?
有人的是1,没人的是0
1101110101(二进制)

& ---- 按位与,可以将某个数的某二进制位置为0,也可以用于取出某个二进制位
| ---- 按位或,可以将某个数的某二进制位置为1.
~ ---- 非,将一个数的所有二进制位取反
^ ---- 异或,相同为0,不同为1

常为BFS与状态压缩结合(因为用DFS不好保存状态,写起来麻烦些)

例题:

hdu 5094 Maze

深搜、广搜、搜索剪枝相关推荐

  1. 深搜广搜专题【DFS】【BFS】

    深搜广搜专题 又是一年专题时,这次的专题是BFS和DFS,我们刚加入acm时的噩梦,然而现在已经写起来很舒服了(OS:那你还A不出题?) BFS和DFS都是通过对所有的点进行遍历来得到结果的,是一种比 ...

  2. 简单深搜广搜基本模板

    简单搜索 DFS: 剪枝,条件 容易超时,超时后基本就是剪枝的问题/无限递归?,或者用广搜试试? 模板(自己的理解) int n,m;//一般输入的行列数/边界 int mov[4][2] = {1, ...

  3. 第七届蓝桥杯-剪邮票(深搜+广搜)

    剪邮票 如下图, 有12张连在一起的12生肖的邮票. 现在你要从中剪下5张来,要求必须是连着的. (仅仅连接一个角不算相连)比如,下面两个图中,粉红色所示部分就是合格的剪取. 请你计算,一共有多少种不 ...

  4. 力扣 547. 朋友圈 c语言 三种解法 深搜 广搜 并查集。

    题目: 并查集: /*力扣 547 朋友圈 并查集 c语言 2020/12/14 1:04 by ksks14*/ /*初始化 查找 合并*/ #define maxsize 10000 int fl ...

  5. poj3083 Children of the Candy Corn 深搜+广搜

    这道题有深搜和广搜.深搜还有要求,靠左或靠右.下面以靠左为例,可以把简单分为上北,下南,左西,右东四个方向.向东就是横坐标i不变,纵坐标j加1(i与j其实就是下标).其他方向也可以这样确定.通过上一步 ...

  6. 搜索:深搜/广搜 获取岛屿数量

    题目描述: 用一个二维数组代表一张地图,这张地图由字符"0"与字符"1"组 成,其中"0"字符代表水域,"1"字符代表小 ...

  7. 深搜+广搜——Lake Counting S(洛谷 P1596)

    题目选自洛谷P1596 这道题目我觉得是比较综合的搜索题了,可以用dfs.bfs来解题.下面给出2种方法的思路: 首先,确定什么情况是一个水坑: 对于每一个'w' ,如果在八个方向上有于其相邻的'w' ...

  8. 【算法合集】深搜广搜Prim与Kruskal

  9. 【蓝桥杯省赛】冲刺练习题【深搜广搜】倒计时【09】天

      

  10. 最是一年留不住,彩云易散琉璃碎---补题模拟,深搜广搜简单题

    链接:登录-专业IT笔试面试备考平台_牛客网 来源:牛客网 题目a 在风景如画,美女如云的SMU生活学习一年后,多金的花样少年JolerJolerJoler练成了见女生必送鲜花的浪漫绝技.这一天,Jo ...

最新文章

  1. darknet53的网络结构笔记
  2. linux命令行怎么注释,Bash Shell 注释多行的几种方法
  3. firefox4脚本执行顺序与jquery.globalEval
  4. Oracle数据库之创建和管理表
  5. 中国移动游戏趋势洞察报告
  6. Nacos Spring Cloud 快速开始
  7. asp html转word文档,asp生成word文档
  8. win7下程序运行权限问题解决方案
  9. 科技公司 CEO 合谋“诈骗”自家公司超 900 万美元,现已被捕
  10. 《深入理解Android2》读书笔记(五)
  11. c语言识别按了esc键_憋了三年,史上最全的 F1~F12 键用法整理出来了
  12. OpenCV和java做人脸识别
  13. 特殊矩阵——三对角矩阵(Tridiagonal Matrix)
  14. Raid5磁盘阵列数据恢复,服务器raid数据恢复步骤和方法
  15. 脑电情绪识别资源整理
  16. word里面空白页怎么删除
  17. 在3dmax软件中添加样条的方法和详细步骤
  18. 配置多SessionFactory
  19. 如何将本地项目存入华为云
  20. 知识问答KBQA简介

热门文章

  1. IMS医药数据库简介
  2. Polkadot 波卡交易所对接资料收集以及测试/公链部署参考
  3. 微信公众号添加自定义菜单
  4. CEM计算电磁学 -- Lecture 2 学习笔记 (1) ---TMM 传输矩阵法(1)
  5. 2.前端笔记-CSS-字体属性
  6. 程序员,996的压力下,还要去做副业吗?忙,也要做,这是我的答案
  7. 国外投资哪些域名比较受欢迎?
  8. NotePad功能添加
  9. js数组中filter、map、reduce、find等方法实现的原理
  10. C语言关键字浅析-while