深度优先搜索(DFS, Depth-First Search)和宽度优先搜索(BFS, Breadth-First Search,或称为广度优先搜索)是基本的暴力技术,常用于解决图、树的遍历问题。
递归是深搜的过程,自己调用自己,将自己这个大问题逐步缩小,换成小问题。
如果要在 4 个数中取任意 3 个数的排列,那么只在 2 行改为 n=4,然后在 dfs()
中修改第 7 行:
还好,看这答案挺简单的。
#include<bits/stdc++.h>
using namespace std;
const int n=10;
char mp[n+2][n+2]; //用矩阵mp[][]存迷宫图。把静态数组定义在全局
bool vis[n+2][n+2]; //判断点是否曾走过,是“记忆化”功能
int ans = 0;
int cnt = 0;
bool dfs(int i, int j){
if (i<0 || i>n-1 || j<0 || j>n-1) return true; //走出了迷宫,停止
if (vis[i][j]) return false; //如果已经搜过,说明兜圈子了,走不出去
cnt++; //统计dfs()了多少次
vis[i][j] = true; //标记已搜索
if (mp[i][j] == 'L') return dfs(i, j - 1); //往左,继续dfs
if (mp[i][j] == 'R') return dfs(i, j + 1); //往右
if (mp[i][j] == 'U') return dfs(i - 1, j); //往上
if (mp[i][j] == 'D') return dfs(i + 1, j); //往下
return true;//得加上,不然warning
}
int main(){
//本题是填空题,直接输出答案:
// cout << 31; return 0;
//如果不是填空,就写下面的代码:
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> mp[i][j]; //读取迷宫图
for (int i = 0; i < n; i++) //对每个点,判断是否能走出去
for (int j = 0; j < n; j++){
memset(vis, 0, sizeof(vis)); //搜索每个点前,都清空vis[]
if(dfs(i, j)) ans++; //点mp[i][j]能走出去,统计答案
}
cout <<"ans="<< ans <<", cnt="<<cnt<< endl; //输出答案
return 0;
}
复杂度分析:如果迷宫有 n 行 n 列,做一次 dfs()
,那么最多需要走遍所有的点,即 O(n^2) 次。代码的 25-29 行对 n^2 个点,每个点都做了一次 dfs()
,所以总复杂度是 O(n^4) 的。
n为10,不会超时,但若n为1000,代码将严重超时。
DFS是暴力技术,会搜索所有可能的情况。
算法优化:我们考虑优化一下前面 O(n^4) 的代码。通过观察你会发现,其实用不着对每个点都做一次 dfs()
。例如从一个点出发,走过一条路径,最后走出了迷宫,那么以这条路径上所有的点为起点,都能走出迷宫;若这条路径兜圈子了,那么这条路径上所有的点都不能走出迷宫。因此如果我们有办法对路径进行记录,就能大大减少计算量。所以优化的关键是如何标记整个路径。
#include<bits/stdc++.h>
using namespace std;
const int n=10;
char mp[n+2][n+2];
bool vis[n+2][n+2];
int solve[n+2][n+2]; //solve[i][j]=1表示这个点能走出去;=2表示出不去
int ans = 0;
int cnt = 0;
bool dfs(int i, int j){
if (i<0 || i>n-1 || j<0 || j>n-1) return true;
if(solve[i][j]==1) return true; //点(i,j)已经算过了,能出去
if(solve[i][j]==2) return false; //点(i,j)已经算过了,出不去
if (vis[i][j]) return false;
cnt++; //统计dfs()了多少次
vis[i][j] = true;
if (mp[i][j] == 'L'){
if(dfs(i, j - 1)){ solve[i][j] = 1;return true;}
//回退,记录整条路径都能走出去
else { solve[i][j] = 2;return false;}
//回退,记录整条路径都出不去
}
if (mp[i][j] == 'R') {
if(dfs(i, j + 1)){ solve[i][j] = 1;return true;}
else { solve[i][j] = 2;return false;}
}
if (mp[i][j] == 'U') {
if(dfs(i - 1, j)){ solve[i][j] = 1;return true;}
else { solve[i][j] = 2;return false;}
}
if (mp[i][j] == 'D') {
if(dfs(i + 1, j)){ solve[i][j] = 1;return true;}
else { solve[i][j] = 2;return false;}
}
}
int main(){
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> mp[i][j];
memset(solve, 0, sizeof(solve));
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++){
memset(vis, 0, sizeof(vis));
if(dfs(i, j)) ans++;
}
cout <<"ans="<< ans <<",cnt="<<cnt<< endl;
return 0;
}
上面是优化后的代码,代码中我用 solve[][]完成了标记整个路径>这一工作:
- 当 solve[i][j]=1 时,表示点 (i, j) 能走出去;
- 档 solve[i][j]=2 时表示出不去。
我们以 17-20 行为例:当 dfs(i, j - 1)
的返回值是 true
,说明 (i, j-1) 能出去,那么它的上一个点 (i, j) 自然也能出去,此时记录solve[i][j]=1。若 dfs(i, j - 1)
的返回值是 false
,说明 (i, j-1) 出不去,它的上一个点 (i, j) 也出不去,记录 solve[i][j]=2。在 dfs()
逐步退回到起点的过程中,整个路径上的点的 solve[][] 都得到了结果。
复杂度分析:由于只需要对迷宫内每个点的 solve[][] 赋值一次就得到了答案,所以总复杂度是 O(n^2) 的。由于迷宫问题共有 n^2 个点,每个点都需要搜到,所以 O(n^2) 已经是能达到的最好复杂度了。同时代码也用 cnt 统计 dfs()
了多少次,输出:cnt=100,说明刚好每个点搜一次。
这题真是好题,首先切割线一定会经过图的中心点,只要确定半条到达边界的分割线,就能根据这半条对称画出另外半条。另外,题目要求旋转对称属于同一种分割法,因为结果是中心对称的,搜索出来的个数除以 4 即可。但在搜索过程中需要注意,搜索出的半条分割线不能同时经过关于中心对称的两个点,所以在标记时,需要将对称的点也标上。
从中间的点进行dfs
#include<bits/stdc++.h>
using namespace std;
int X[] = {0, -1, 1, 0, 0}; //上下左右4个方向
int Y[] = {0, 0, 0, -1, 1};
bool vis[10][10]; //标记点是否被访问过
int res = 0;
void dfs(int x, int y){
if(x == 0 || y == 0 || x == 6 || y == 6){
res++;
return ;
}
for(int i = 1 ; i <= 4 ; i++){ //上下左右四个方向
x += X[i]; y += Y[i]; //走一步
if(!vis[x][y]){ // 若该点未访问则继续深搜
vis[x][y] = true; // 当前的点标注为已访问
vis[6 - x][6 - y] = true;
dfs(x, y); // 继续深搜
vis[6 - x][6 - y] = false;
vis[x][y] = false;
}
x -= X[i]; y -= Y[i];
}
}
int main(){
vis[3][3] = true;
dfs(3, 3);
cout << res / 4 << endl;
return 0;
}
这个代码也很妙,层次鲜明。
我们的思路、步骤为以下三步:
- 一个左括号必然有一个右括号匹配:你可以尝试生成各种各样的嵌套括号,方法是:从第 1 对括号“()”开始;把第 2 对括号的左括号和右括号分别随机插入到第 1 对括号的任意位置,例如“(())”;再把第 3 对括号随机插入,例如“(()())”,等等。只要括号是成对插入的,得到的括号串都是合法的。
- 用栈检查括号的合法性:每遇到一个左括号’(’,就进栈,每遇到一个右括号’)’,就完成了一次匹配,出栈。如果你不熟悉可以用一个嵌套括号来练习练习。
- 编码:可以直接用栈编码,也可以用 DFS(递归)编码,更简单一点。
#include<bits/stdc++.h>
using namespace std;
string s;
int pos = 0; //当前的位置
int dfs(){
int tmp = 0, ans = 0;
int len = s.size();
while(pos < len){
if(s[pos] == '('){ //左括号,继续递归。相当于进栈
pos++;
tmp += dfs();
}else if(s[pos] == ')'){ //右括号,递归返回。相等于出栈
pos++;
break;
}else if(s[pos] == '|'){ //检查或
pos++;
ans = max(ans, tmp);
tmp = 0;
}else{ //检查X,并统计X的个数
pos++;
tmp++;
}
}
ans = max(ans, tmp);
return ans;
}
int main(){
cin >> s;
cout << dfs() << endl;
return 0;
}
萌新(超时)代码:
#include <bits/stdc++.h>
using namespace std;
int a[20] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
int main() {
int ans=0;
do{
if( a[0]+a[1]==a[2] && a[3]-a[4]==a[5]
&&a[6]*a[7]==a[8] && a[11]*a[10]==a[9])
ans++;
}while(next_permutation(a,a+13));
cout<<ans<<endl;
}
因为 13! = 6227020800。不过分析题目可知,实际上并不用生成一个完整排列。例如一个排列的前 3 个数,如果不满足“□ + □ = □”,那么后面的 9 个数不管怎么排列都不对。这种提前终止搜索的技术叫剪枝,剪枝是搜索中常见的优化技术。由于 next_permutation()
每次都必须生成一个完整的排列,而不能在中间停止,所以在这种场合下并不好用。
所以,我们可以用最开始介绍的排列代码,用不用顺序都行。
//第一种排列代码
#include <bits/stdc++.h>
using namespace std;
int a[20] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
int ans = 0;
void dfs(int s, int t)
{
if (s == 12) {
if(a[9] * a[10] == a[11]) ans++;
return;
}
//前3个数无法满足条件
if (s == 3 && a[0] + a[1] != a[2]) return;
//前6个数无法满足条件
if (s == 6 && a[3] - a[4] != a[5]) return;
//前9个数无法满足条件
if (s == 9 && a[6] * a[7] != a[8]) return;
for (int i = s; i <= t; i++) {
swap(a[s], a[i]);
dfs(s+1, t);
swap(a[s], a[i]);
}
}
int main()
{
int n = 13;
dfs(0, n - 1);
cout << ans;
return 0;
}
//第二种排列代码,顺序
#include<bits/stdc++.h>
using namespace std;
int a[20]={1,2,3,4,5,6,7,8,9,10,11,12,13};
bool vis[20];
int b[20];
int ans=0;
void dfs(int s,int t){
if(s==12) {
if(b[9]*b[10] == b[11]) ans++;
return;
}
if(s==3 && b[0]+b[1]!=b[2]) return; //剪枝
if(s==6 && b[3]-b[4]!=b[5]) return; //剪枝
if(s==9 && b[6]*b[7]!=b[8]) return; //剪枝
for(int i=0;i<t;i++)
if(!vis[i]){
vis[i]=true;
b[s]=a[i];
dfs(s+1,t);
vis[i]=false;
}
}
int main(){
int n=13;
dfs(0,n); //前n个数的全排列
cout<<ans;
return 0;
}
OK,例题到这里就结束了,下面给出DFS框架,好好体会。
ans; //答案,用全局变量表示
void dfs(层数,其他参数){
if (出局判断){ //到达最底层,或者满足条件退出
更新答案; //答案一般用全局变量表示
return; //返回到上一层
}
(剪枝) //在进一步DFS之前剪枝
for (枚举下一层可能的情况) //对每一个情况继续DFS
if (used[i] == 0) { //如果状态i没有用过,就可以进入下一层
used[i] = 1; //标记状态i,表示已经用过,在更底层不能再使用
dfs(层数+1,其他参数); //下一层
used[i] = 0; //恢复状态,回溯时,不影响上一层对这个状态的使用
}
return; //返回到上一层
}