前言
人人都说搜素搜素有手就行,我听起来啥感觉自己没手呢!
在看这个前言之前,读者应该学过基本的算法和数据结构了。
通过这个题目来进行讲解生日蛋糕 下边附有代码:
有一篇文章将,领悟了二叉树的前、中和后序遍历后你就能理解搜索这个东西了。那么我们就依据这个来进行讨论一下。 在搜索的时候我们知道,搜索的状态 搜索树就是一颗多叉树 , 当前节点的所有决策就是该节点节点的孩子。 因为二叉树只有左右两个孩子。 下面这个dfs 其实就是前序遍历的变形。 二叉树的前序遍历是这样:
void dfs( BiTree T){
if(T){
vist(T);
if(T->lchild) dfs(T->lchild);
if(T->rchild) dfs(T->child);
}
}
// 可能感觉不太想,不过我们修改一下。
void dfs( BiTree T){
if(T){
vist(T);
for(int i = 0 ; i < 2 ; ++i){
if(i == 0 && T->lchild) dfs(T->lchild);
if(i ==1 && T->rchild) dfs(T->rchild);
}
}
}
之所以我们不写成下面的形式是因为,1是二叉树的下一个状态是已知的。2是二叉树直接通过左右孩子dfs即可,不用通过遍历来引出状态。3是同一个节点访问的结果是一样的所有访问一次就好。
还有一个问题是什么时候需要恢复现场什么时候不用呢?
好比下边这个代码我们在dfs结束后不需要恢复现场,而且也没有必要恢复现场。我们的的每一个路径上的节点都是相互独立的。 而有些题目需要就好比八皇后或者图的最短路径等。我们在遍历的时候以这个节点开始去遍历的点,我们通过别的节点也能到达,那么如果我们不恢复,那么别人就无法访问了。
判断是否DFS是否需要恢复现场
代码:
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 25 , INF = 1e9;
int n, m ;
int minv[N] , mins[N];
int H[N] , R[N] ;
int ans = INF;
void dfs(int dep , int v , int s){
if (v + minv[dep] > n) return;
if (s + mins[dep] >= ans) return;
if (s + 2 * (n - v) / R[dep + 1] >= ans) return;
if (!dep)
{
if (v == n) ans = s;
return;
}
for (int r = min(R[dep + 1] - 1, (int)sqrt(n - v)); r >= dep; r -- )
for (int h = min(H[dep + 1] - 1, (n - v) / r / r); h >= dep; h -- )
{
int t = 0;
if (dep == m) t = r * r;
R[dep] = r, H[dep] = h;
dfs(dep - 1, v + r * r * h, s + 2 * r * h + t);
}
}
int main(){
cin>> n >> m;
for(int i = 1 ; i <= m ; ++i){
minv[i] = minv[i-1] + i * i * i;
mins[i] = mins[i - 1] + 2 * i * i;
}
H[m+1] = R[m+1] = INF;
dfs(m , 0 , 0);
if(ans == INF)cout<<0 <<endl;
else
cout<<ans<<endl;
return 0;
}
深度优先搜素
如何搜索
这是最重要的一步,也就是说我们要以什么样的顺序搜索才能不重不漏的将每一个情况都能枚举到。只有先做到这一步才能想着如何剪枝等优化技巧。
下面以一道题来进行说明。
题目大意:
题目是让我们对每一个字母进行赋值,使得符合该加法等式。
解题思路:
就是爆搜。重点是如何进行搜索,即如何对每一个字母进行赋值所有的情况。首先第一点我们可以规定一个搜索顺序,我们可以对字母进行编号,而编号的话我们可以按照从右往左依次对字母进行编号。而因为我们这样编号的话那么是不符合字母表顺序的因此我们可以用一个数组q来存下这个映射关系。然后我们从第一个字母开始进行遍历 , 在每一次中字母有 n 种选择,在这n种中选出没有被挑选的。
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 30;
char g[N][N];
int st[N] , path[N] , q[N];
int n ;
bool check(){
// 遍历每一列
for(int i = n - 1 , t = 0; ~i ; --i){
int a = g[0][i] - 'A' , b = g[1][i] - 'A' , c = g[2][i] - 'A'; // 获取字母在序列中的位置
if(path[a] != - 1 && path[b] != -1 && path[c] != -1){
a= path[a] , b = path[b] , c = path[c];
if(t != -1){ // 前一列是确定的了
if((a + b + t) % n != c)return false;
if(!i && a + b >= n)return false; // 最高位
t = (a + b + t) / n; // 当t 确定了 那么这个t的进位再能确定
}else
{
if( (a + b ) % n != c && (a + b + 1) % n != c)return false;
if(!i && a + b >= n)return false;
}
}else
t = - 1 ;
}
return true;
}
// 这个u 第几个字母,所以是依次对字母进行赋值
bool dfs(int u )
{
if(u == n )return true;
//没一个path 位置遍历 n 中 可能 , 那每一个为值为一个字母的值
// 所以就相当于对每一个字母符 n 个值
for(int i = 0 ; i < n ; ++i){
if(!st[i]){
st[i] = true;
path[q[u]] = i; // 找到u 位置
if(check() && dfs(u + 1))return true;
st[i] = false;
path[q[u]] = -1;
}
}
return false;
}
int main(){
scanf("%d",&n);
for(int i = 0 ; i < 3 ; ++i)scanf("%s",g[i]);
for(int i = n - 1 , k = 0 ; ~i ; -- i)
for(int j = 0 ; j < 3 ; ++j){
int x = g[j][i] - 'A';
if(!st[x]){
st[x] = true;
q[k++] = x;
}
}
memset(path , -1 , sizeof path);
memset(st , 0 , sizeof st);
dfs(0);
for(int i = 0 ; i < n ; ++i)cout<<path[i] << " ";
return 0;
}
迭代加深
加成序列
这个迭代深度就有点想 ,答案解的长度。
解题思路:
看到最短的一个解,首先想到的应该都是BFS不过这题不好用BFS , 因此我们可以采用DFS中的迭代加深的方法。 其中迭代深度就是求解序列的长度。在没一次dfs中我们的状态是 当前要填的空,和序列的长度。 搜索结束是当 填的空已经为序列长度了就返回 , 当序列最后一个为n时(条件)返回true ,否则返回false 。 当前这个空 由条件 知 我们可选的集合 会前边 数两两想加的和 。同时这个和我们要满足比前一个大 , 同时不能大于n 。 在这里我们可以利用一个bool 数组来进行剪枝 , 避免对 和 一样的进行搜索。
代码:
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 110;
int n ;
int path[N];
bool dfs(int u, int depth){
if(u == depth) return path[u-1] == n;
bool st[N] = {false};
for(int i = u - 1 ; i >=0 ; --i)
for(int j = i ; j >= 0 ; --j){
int s = path[i] + path[j];
if(s > path[u-1] && s <= n && !st[s]){
st[s] = true;
path[u] = s;
if(dfs(u+1 , depth)) return true;
}
}
return false;
}
int main(){
while(cin>> n , n){
int depth = 1;
path[0] = 1 ;
while(!dfs(1 , depth)) depth++;
for(int i = 0 ; i < depth ; ++i)cout<<path[i]<< " ";
cout<<endl;
}
return 0;
}
导弹防御系统
- 小知识:对于DFS来求最短的问题我们一般有两个思路 , 一个是定义一个全局变量然后更新,二就是迭代加深
导弹防御系统
解题思路:
我们的搜索顺序就是依次遍历每一个数字的情况,而每一个数字都有两种选择,一个是上升,一个是下降。不过在两个序列中又有很多个上升或者下降序列。这会导致我们搜索空间太大,不过我们由一个性质类似单调队列的思路,我们在选上升序列的时候选择第一个小于的就好,因为当这个数x加入后有一个序列的下一个数一定是x结尾这个是固定的了,不过其余序列的结尾是变得,我们要想达到最优的情况,那么选择一个里其最近的一个小于它的是最优的。下降序列同理。
关键点:
- 对于加入别的序列我们要记录下原先的数,方便回溯。
代码:
```cpp
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 51;
int n ;
int h[N] , up[N] , down[N];
bool dfs(int depth , int u , int su , int sd){
if(su + sd > depth )return false;
if(u == n)return true;
bool flag = false;
for(int i = 0; i < su ; ++i){
if(up[i] < h[u]){
int t = up[i];
up[i] = h[u];
if(dfs(depth , u + 1 , su , sd))return true;
up[i] = t;
flag = true;
break;
}
}
if(!flag){
up[su] = h[u];
if(dfs(depth , u + 1 , su + 1 , sd))return true;
}
flag = false;
for(int i = 0; i < sd ; ++i){
if(down[i] > h[u]){
int t = down[i];
down[i] = h[u];
if(dfs(depth , u + 1 , su , sd))return true;
down[i] = t;
flag = true;
break;
}
}
if(!flag){
down[sd] = h[u]; // 从0开始存
if(dfs(depth , u + 1 , su , sd + 1))return true;
}
return false;
}
int main(){
while(cin>>n , n){
for(int i = 0 ; i < n ; ++i)scanf("%d",&h[i]);
int depth = 1 ;
while(!dfs(depth , 0 , 0 , 0))depth++;
cout<<depth<<endl;
}
return 0;
}
# 深度搜索(BFS)
## 权值相同的多源最短路问题
[矩阵距离](https://www.acwing.com/problem/content/175/)
问题描述:
>问题是让我们求出 , 每个数字到最近的1的曼哈顿距离。
解题思路:
> 该题可以转化为最短路问题 , 只不过边的权重都为1 。 边指的是相邻点才连边。由于边权都相同,那么我们就可以用BFS来求出最短路,这个就相当于一个简化版的dijkstra算法,队头一定是最小的节点。 由于这题是一个多源最短路问题,我们不可能遍历每一个节点。这题的巧妙之处是利用宽搜的性质,先将距离为0的点放入队列中 ,然后依次拓展, 那么我们就可以在O(n)的时间内解出。
关键问题:
1. 如何记录距离:一般BFS我们都会用一个数组(dist)来存储距离 , 同时 这个数组还可以用来记录这个节点是否被访问过。
代码:
```cpp
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int , int >PII;
const int N = 1000;
int g[N][N] , n , m;
int dist[N][N];
queue<PII>q;
void bfs(){
int dx[4] = { 1, -1 , 0 , 0 } , dy[4] = { 0,0,1,-1};
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0 ; i < 4 ; ++i){
int x = t.first + dx[i] , y = t.second + dy[i];
if(x < 0 || x >= n || y >= m ||y < 0)continue;
if(dist[x][y] == -1){
dist[x][y] = dist[t.first][t.second] + 1;
q.push({x,y});
}
}
}
}
int main(){
memset(dist , -1 , sizeof(dist));
cin>>n>>m;
getchar();
for(int i = 0 ; i < n; ++i)
{
for(int j = 0 ; j < m ; ++j)
{
char ch;
ch = getchar();
g[i][j] = ch - '0';
if(g[i][j] == 1)dist[i][j] = 0 , q.push({i , j});
}
getchar();
}
bfs();
for(int i = 0 ;i < n ; ++i){
for(int j = 0 ; j < m ; ++j)
cout<<dist[i][j] <<" ";
cout<<endl;
}
return 0;
}
BFS 变形
双端队列BFS
在最基本的广度优先搜索中,每次沿着分支的扩展都记为“一步”,我们通过逐层搜索,解决了求从起始状态到每 的最少步数的问题。这其实等价于在一张边视均为1的图上执行广度优先遍历,求出每个点相对于起点的最短距离(层次)。在第021节中我们曾讨论过这个问题,并得到了“队列中的状态的层数满足两段性和单调性”的结论。从而我们可以知道,每个状态在第一次被访问并入队时,计算出的步数即为所求
然而,如果图上的边权不全是1呢?换句话说,如果每次扩展都有各自不同的“代价“ , 我们相求出起始状态到每一个转态的最小代价? 下面通过一题来解决0和1的时候
参考:出处
小技巧:
- memset 的时候 , 是有四个字节的。因此 0x3f 会变为 0x3f3f3f3f .
代码:
#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<deque>
using namespace std;
const int N = 501;
typedef pair<int ,int > PII;
int n,m;
char g[N][N];
int d[N][N] ;
int bfs(){
memset(d, 0x3f , sizeof(d));
deque<PII> dq;
dq.push_back({0,0});
d[0][0] = 0;
int dx[4] = {-1 , -1 , 1,1} , dy[4] = {-1,1,1,-1 }; // 坐标的偏移量
int ix[4] = {-1,-1,0,0} , iy[4] = {-1,0,0,-1}; // 找到该坐标对应的字符的位置
char cs[] = "\\/\\/"; // 正确的字符位置
while(dq.size()){
auto t = dq.front();
dq.pop_front();
int x = t.first , y = t.second;
for(int i = 0 ; i < 4 ; ++i){
int xx = x + dx[i] , yy = y + dy[i];
if(xx >=0 && xx <= n && yy >=0 && yy <= m ){
int a = x + ix[i] , b = y + iy[i];
int w = 0 ;
if(g[a][b] != cs[i]) w = 1 ;
if(d[xx][yy] > d[x][y] + w){
d[xx][yy] = d[x][y] + w;
if(w) dq.push_back({xx,yy});
else dq.push_front({xx,yy});
}
}
}
}
if(d[n][m] == 0x3f3f3f3f)return -1;
else return d[n][m];
}
int main(){
int t ;
cin>>t;
while(t--){
cin>>n>>m;
for(int i = 0 ; i < n ; ++i)
scanf("%s",g[i]);
int ans = bfs() ;
if(ans == -1) cout<<"NO SOLUTION\n";
else printf("%d\n",ans);
}
return 0;
}
优先队列BFS
相对于上一个问题,对于更加普遍的情况,也就是每次扩展的代价是不一样的,求出初始状态到每一个状态的最小代价。就相当在一张带权图上求出起点到每一个点的最短路。那么这种情况我们就可以进行广搜了。下面以一道题来举例。
题目大意:
题目就是求出从开始城市到终点城市的最小花费。
解题思路:
和最短路不一样的是,这里油的数量是在变化的。 面对这种情况我们可以将城市与油量拆分成一个点对<城市,当前剩余油量> 通过这个点对来进行深搜。我们用一个二维数组
dist[N][C]
来记录最小花费。因此我们最终要求得急速dist[S][0] -- > dist[E][0]
的最小代价。为了方便建立优先队列,我们会以 代价也放入队列中,每次去出代价最少的进行扩展。对于每一个状态有两个可以进行转移的状态, 一 是 (如果加一升油没满)就加一升油 , 二、是遍历所有能去的城市。 因此我们需要建图。
代码:
#include<cstring>
#include<iostream>
#include<queue>
#include<algorithm>
#include<stdio.h>
using namespace std;
const int N = 1010, C = 110, M = 20010;
struct ver{
int d , u , c;
bool operator < (const ver & a)const{
return d > a.d;
}
};
int h[N] , E[M] , W[M] , ne[M] , tot;
int price[N] ;
bool st[N][C];
int dist[N][C];
int n, m ;
void add(int u , int v ,int w){
E[tot] = v , W[tot] = w , ne[tot] = h[u] , h[u] = tot++;
}
int bfs(int s , int e, int cab){
priority_queue<ver> heap;
heap.push({0 , s , 0});
memset(dist , 0x3f , sizeof dist);
memset(st , false , sizeof st);
dist[s][0] = 0;
while(heap.size()){
auto t = heap.top();
heap.pop();
if(t.u == e) return t.d; // 到达终点城市
if(st[t.u][t.c])continue; // 访问过了 , 避免重复访问
st[t.u][t.c] = true;
// 加一升油
if(t.c < cab){
if(dist[t.u][t.c + 1 ] > t.d + price[t.u]){
dist[t.u][t.c + 1] = t.d + price[t.u];
heap.push({dist[t.u][t.c + 1] , t.u , t.c + 1});
}
}
// 遍历能走的边
for(int i = h[t.u] ; ~i ; i = ne[i]){
int y = E[i];
if(t.c >= W[i]){ // 剩余的油能走这条边
if(dist[y][t.c - W[i]] > t.d){
dist[y][t.c - W[i]] = t.d;
heap.push({dist[y][t.c - W[i]] , y , t.c - W[i]});
}
}
}
}
return -1;
}
int main(){
memset(h , -1 , sizeof h);
scanf("%d%d", &n , &m);
for(int i = 0 ; i < n ;++i )scanf("%d", &price[i]);
for(int i = 0 ; i < m ;++i){
int u , v, w;
scanf("%d%d%d",&u, & v, & w);
add(u,v,w) , add(v,u,w);
}
int query;
scanf("%d" ,&query);
while(query --){
int c, s ,e;
scanf("%d%d%d",&c ,&s, &e);
int ans = bfs(s,e,c);
if(ans == -1) cout<<"impossible\n";
else cout<<ans<<endl;
}
return 0;
}
双向BFS
基本思路:
我们只需要从起始状态、目标状态分别开始,两边轮流进行 , 每次各次扩展,当两边各自有一个状态在记录数组中发生重复时,就说明这两个搜索过程相遇了。
如何实现的我们以下边这一题来讲解
噩梦
题目大意:
题目是让我们求男孩和女孩是否能相遇,如果能相遇求出最短时间
解题思路:
我们各自对男孩和女孩进行bfs , 如果 记录数组中有交集 就说明能相遇并返回当前所用时间。
关键点:
- 如何判断是否是鬼已经占领的地方:我们不需要记录鬼占领的区域,对于每一个坐标我们只需要判断是否被鬼占领即可。如果判断,题目已经明确了是曼哈顿距离。
- 如何双向BFS:我们还是利用一个BFS的框架,只不过变成了两个队列。每次我们在循环中依次让两个队列进行个一个队列时候的操作即可。
代码:
#include<stdio.h>
#include<queue>
#include<iostream>
#include<cstring>
using namespace std;
typedef pair<int, int>PII;
const int N = 800;
char g[N][N];
PII ghost[2] , boy , girl;
int st[N][N];
int dx[4] = { 1,-1,0,0} , dy[4] = { 0,0,1,-1};
int m ,n;
bool check(int x , int y , int tm){
if(x < 0 || x >= n || y < 0 || y >= m || g[x][y] == 'X')return false;
for(int i = 0 ; i < 2 ; ++i)
if(abs(x - ghost[i].first) + abs(y - ghost[i].second) <= 2 * tm) return false;
return true;
}
int bfs(){
memset(st , 0 , sizeof(st));
memset(st, 0, sizeof st);
int cnt = 0;
PII boy, girl;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (g[i][j] == 'M') boy = {i, j};
else if (g[i][j] == 'G') girl = {i, j};
else if (g[i][j] == 'Z') ghost[cnt ++ ] = {i, j};
queue<PII> qb, qg;
qb.push(boy);
qg.push(girl);
int step = 0;
while(qb.size() || qg.size()){
step ++ ;
for(int i = 0 ; i < 3 ; ++i)
for(int j = 0 , len = qb.size(); j < len ; ++j){
auto t = qb.front();
qb.pop();
int x = t.first , y = t.second;
if(!check(x,y , step))continue;
for(int k = 0 ; k < 4 ; ++k){
int xx = x + dx[k] , yy = y + dy[k] ;
if(!check(xx,yy , step))continue;
if(st[xx][yy] == 2) return step;
else if(!st[xx][yy])
{ st[xx][yy] = 1 ;
qb.push({xx,yy});
}
}
}
for(int i = 0 ; i < 1 ; ++i)
for(int j = 0 , len = qg.size() ; j < len ; ++j){
auto t = qg.front();
qg.pop();
int x = t.first , y = t.second;
if(!check(x,y,step))continue;
for(int k = 0 ; k < 4 ; ++k){
int xx = x + dx[k] , yy = y + dy[k] ;
if(!check(xx,yy , step))continue;
if(st[xx][yy] == 1) return step;
else if(!st[xx][yy])
{st[xx][yy] = 2 ;
qg.push({xx,yy});
}
}
}
}
return -1;
}
int main(){
int T ;
scanf("%d" ,&T);
while(T--){
scanf("%d%d" , &n , &m);
for(int i = 0 ; i < n; ++i)scanf("%s" , g[i]);
printf("%d\n" , bfs());
}
return 0;
}
字串变换
题目大意:
题目给了起始字符和终止字符和变换规则,让我们求出10步以内能否从起始状态变换到终止状态。
解题思路:
我们从起始和终止状态两边进行BFS , 如果它们在某一层有相交的那么就说明能变化。 注意的点:因为规则只能转换一部分字符,因此,对于没一个都是扩展一层,不是多层也不是单个节点。
关键点:
- 对于这里是字符,因此记录步数的话我们可以用哈希表建立起映射关系。
- 对于没一个状态我们如何扩展呢?首先是遍历所有规则,对于每一种规则我们都要遍历整个字符串的子串看是否符合规则。 在这里 可以利用
substra()
来进巧妙的进行。
代码:
#include<iostream>
#include<queue>
#include<string>
#include<unordered_map>
using namespace std;
const int N = 6;
string A,B;
string a[N] , b[N];
int n;
int extend(queue<string>&qa, unordered_map<string,int> &da , unordered_map<string,int> & db ,string a[N] , string b[N]){
int d = da[qa.front()];
while(qa.size() && d == da[qa.front()]){
auto t = qa.front();
qa.pop();
for(int i = 0 ; i < n ; ++i)
for(int j = 0 ; j < t.size() ; ++j){
if(t.substr(j , a[i].size()) == a[i]){
string r = t.substr(0 , j ) + b[i] + t.substr(j + a[i].size());
if(db.count(r)) return da[t] + db[r] + 1;
if(da.count(r)) continue;
da[r] = da[t] + 1;
qa.push(r);
}
}
}
return 11;
}
int bfs(){
if(A == B )return 0;
queue<string>qa,qb;
unordered_map<string,int>da,db;
da[A] = db[B] = 0;
qa.push(A) , qb.push(B);
int step = 0 , t;
while(qa.size() && qb.size()){
if(qa.size() < qb.size()) t = extend(qa,da,db,a,b);
else t =extend(qb , db,da, b,a);
if(t <= 10)return t;
if(++ step == 10)return -1;
}
return -1;
}
int main(){
cin>>A>>B;
while(cin>>a[n]>>b[n]) n++;
int ans = bfs();
if(ans == -1)puts("NO ANSWER!");
else
cout<<ans<<endl;
return 0;
}