0
点赞
收藏
分享

微信扫一扫

DFS思想及例题

小_北_爸 2022-01-16 阅读 86

深度优先搜索(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. 一个左括号必然有一个右括号匹配:你可以尝试生成各种各样的嵌套括号,方法是:从第 1 对括号“()”开始;把第 2 对括号的左括号和右括号分别随机插入到第 1 对括号的任意位置,例如“(())”;再把第 3​ 对括号随机插入,例如“(()())”,等等。只要括号是成对插入的,得到的括号串都是合法的。
  2. 用栈检查括号的合法性:每遇到一个左括号’(’,就进栈,每遇到一个右括号’)’,就完成了一次匹配,出栈。如果你不熟悉可以用一个嵌套括号来练习练习。
  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;                //返回到上一层
}
举报

相关推荐

0 条评论