仅仅是做过这些题,还不能说是掌握了搜索,只是现阶段就学到这里了.之后的时间如果看到洛谷月赛里面的搜索可以做一做,进阶更是之后的事情.
1.DFS
一条路走到黑,到头了再重来,直至所有的情况被遍历(或者剪枝).
入门题有全排列问题.可以看出来思想是在每一步都把之后所有可能的排列顺序都遍历一遍.每一个排列顺序即对应于一个解.这里把每一个解(即排列本身)直接输出就行了.
用到类似做法的有吃奶酪问题,遍历所有的顺序,每一个顺序对应于一个解,这里解的求取方法是老鼠按这个遍历顺序吃掉所有奶酪所得到的移动距离之和.
注意,如何不重不漏地遍历所有的顺序呢?在上面两个问题中,都用到了"used"这个数组,并且都有类似于如下的结构:
for(int i = 1; i <= n; i++){ if(!used[i]){ ... used[i] = true; dfs(...); used[i] = false; } }
先把状态i标记为已在当前位置选择,在此基础上搜索下一个状态便不会重复地选择状态i了.
在此后将状态i重新标记为未使用会怎么样?当你进行下一次dfs时,状态i可以再次被选用,但是这时候是在另一个位置上了,想象一下一个树状图,状态是如何在这个树状图上面转移的.
以全排列问题n=4的情况为例:
3 → 4
↗
2 → 4 → 3
↗ 2 → 4
↗
1 → 3
↘
↘ 4 → 2
4 → 2 → 3
↘
3 → 2
(极简艺术大师)
遍历过程:1 2 3 4 ← ← 4 3 ← ← ← 3 2 4 ← ← 4 2 ← ← ← 4 2 3 ← ← 3 2 ← ← ← 结束
每一次←都表示返回到上一层的dfs.注意used在这个过程中的变化,很容易理解为什么可以遍历所有状态.
这种做法叫做回溯,在dfs中是很常见的,很多情况还是必要的.
(算法竞赛入门-)经典的回溯题目有八皇后问题.里面用到一种独特的标记方法.
有时候,回溯可能不一定是简单地把某状态标记为true 或 false,比如这个例子.

#include <algorithm> #include <cstdio> #include <cstring> #include <iostream> using namespace std; int n, s[10]; void dfs(int add, int sum) { if (sum == n) { for (int i = 1; i <= n - 1; i++) { int tmp = s[i]; while (tmp--) { printf("%d", i); if (tmp || s[i + 1] || s[i + 2] || s[i + 3] || s[i + 4] || s[i + 5] || s[i + 6] || s[i + 7] || s[i + 8]) printf("+"); } } puts(""); } else if (sum < n && add + sum <= n) { for (int i = add; i + sum <= n; i++) { s[i]++; dfs(i, sum + i); s[i]--; } } } int main() { cin >> n; if (n == 1) puts("1"); else dfs(1, 0); return 0; }
此外,map可以用来标记字符串是否已经遍历.例如:(虽然这题是bfs)
字串变换 (注意map标记字符串最好用string,用char*会带来麻烦)

#include <algorithm> #include <cstring> #include <iostream> #include <map> #include <queue> #include <string> using namespace std; struct S { string s; int t; }; string A, B; string Ai[30]; string Bi[30]; queue<S> que; int n, ans; map<string, int> used; string canLink(const string &s, int i, int j) { string ans = ""; if (i + Ai[j].length() > s.length()) return ans; for (int k = 0; k < Ai[j].length(); k++) if (s[i + k] != Ai[j][k]) return ans; ans = s.substr(0, i); ans += Bi[j]; ans += s.substr(i + Ai[j].length()); return ans; } int main() { cin >> A >> B; while (cin >> Ai[n] >> Bi[n]) n++; S s = {A, 0}; que.push(s); while (!que.empty()) { S tp = que.front(); que.pop(); string tmp; if (used.count(tp.s) == 1) continue; if (tp.s == B) { ans = tp.t; break; } used[tp.s] = 1; for (int i = 0; i < tp.s.length(); i++) for (int j = 0; j < n; j++) { tmp = canLink(tp.s, i, j); if (tmp != "") que.push({tmp, tp.t + 1}); } } if (ans > 10 || ans == 0) cout << "NO ANSWER!" << endl; else cout << ans << endl; return 0; }
有时候,合理设置dfs(...)的参数可以省去回溯,使得过程更加简洁.见此.这种情况并不常见.
除此之外还有做法比较简单的剪枝.
如果dfs以上一状态某个表示解的过程值为参数,那么当发现过程值已经劣于先前发现的解,那么就没比较继续搜索下去了,直接return.直接放上两个例子.
可以看到如下结构:
if(当前状态 != 最终状态 && ct <= ans){ // 只有当前过程值ct不大于已经发现的解才会继续搜索 ...(搜索操作) }else ans = min(ans, ct); // 目标是找到最小的ct的值
这就是我所见到的大部分dfs剪枝的方法,如果剪枝也不行的话,见吃奶酪问题里的卡时.但这时候应该换算法了.
如你所见,dfs的大部分题目都会同时用到剪枝和回溯.
2.BFS
有相当多的题目,DFS和BFS都可以做,只是效率有所区别.
一道近乎于模板的bfs:好奇怪的游戏.
注意vis这个标记,在bfs中一个点第一次被遍历到时一定是最快/最短的路径,所以在vis标记之后就不需要再去这个点了.(隐性的剪枝)
如果用dfs+回溯来做的话,就必须等到所有的方案(剪枝后的)穷举之后才能得出答案,相比bfs效率会低很多.
bfs每标记一个vis,都是在向正确答案靠近一步.
dfs回溯完毕时,才能够"总结"出正确答案.
所以求到某已知状态的最短路的问题适合用bfs.
如果最终状态不给出的话,怎么用bfs呢?比如说吃奶酪.这时候暴力方法还是用dfs.
这题的dfs做法转载于此.

#include<bits/stdc++.h>//万能头 using namespace std; int a,b,c,d,f[25][25],dx[13]={0,2,2,2,2,1,1,-1,-1,-2,-2,-2,-2},dy[13]={0,2,1,-1,-2,2,-2,2,-2,2,1,-1,-2};//前面已经说过了,不解释(貌似已经解释了QWQ) void dfs(int x,int y,int s){//到达第x行y列,步数为x f[x][y]=s;//标记 for(int u=1;u<=12;u++){//开始拓展 int ux=x+dx[u],uy=y+dy[u];//找到坐标 if(ux<1||ux>20||uy<1||uy>20)//判边界 continue; if(f[ux][uy]==0||f[ux][uy]>s+1)//可以拓展 dfs(ux,uy,s+1);//拓展 } } int main() { scanf("%d%d%d%d",&a,&b,&c,&d); dfs(a,b,1); printf("%d ",f[1][1]-1); memset(f,0,sizeof(f));//别忘清0 dfs(c,d,1); printf("%d ",f[1][1]-1); return 0; }
注意到
if(f[ux][uy]==0||f[ux][uy]>s+1)
所产生的效果是与bfs等同的.实际上具有相同的思想,即记录到达某点时的最短路径,在此基础上向各向拓展并最终到达终点.
区别在于bfs从起点向四周逐层拓展,dfs则一个方向走到底后回头继续走.至于这时效率的区分,我是懒得计算这里的复杂度的.
也许只要bfs或者dfs可以求解的问题,另一者总是可以求解,只是在设计上面会有所困难?做题时常常会看到题解区同时存在两种解法,但是第一眼看上去是想不到另一者是怎么做的.
想要深入研究看这里吧,暂时只需要做到能够快速做出选择并AC就行了,不需要过于纠结.
再放两道bfs的题,就写到这里吧.
又做了一段时间的题目才发现这个标记方法还是非常常用的,几乎算是bfs模板的一部分了,当时还是太菜了
标记还是不标记?这是个问题.
补充一道记忆搜索的题目.