DFS和BFS及模板

发布时间 2023-09-01 00:11:00作者: gao79138

DFS和BFS及模板

1. 定义

    DFS俗称深度优先搜索,BFS俗称宽度优先搜索。这两种算法都可以保证遍历图中所有的节点。是一种非常常见的搜索算法。

2. DFS思想

img

    DFS在搜索时,尽量往深去搜索。这种算法的主要思想如下:
        1.  选取一个点为起始节点,做好标记代表已经搜索过当前节点。
        2.  看看起始节点是否邻接其余的节点,如果有,则判断下一个节点是否已经搜索过。如果搜索过,则寻找当前节点其余的邻接节点继续判断。如果没有搜索过,则移动到下一个节点,并给下一个节点打上标记代表已经搜索过。
        3.  如果从当前节点开始,无法再向下进行搜索,那么就会回溯到上一个节点,继续搜索。如果上一个节点仍无法再向下进行搜索,那么继续回溯,直到找到可以往下搜索的节点或搜索完毕为止。
    DFS的搜索过程是一个往"深"搜索的过程(行进过程一直往深走,不撞南墙不回头)。具体看上图即可。

img

3. BFS思想

    BFS在搜索时,尽量往宽去搜索。这种算法的主要思想如下:
        1.  选取一个点为起始节点,做好标记代表已经搜索过当前节点。
        2.  找到当前节点邻接的所有节点,做好标记代表已经搜索过。
        3.  重复2,直到遍历所有节点为止。
    BFS的搜索过程是一个往"宽"搜索的过程(行进过程一直往宽走,这一层搜完,再搜下一层)。具体看上图即可。换句话说,BFS实际上是按层搜索的。这一层搜完,再搜下一层,以此类推。

4. DFS与BFS的对比

img

    DFS与BFS都各占优势,我们需要根据具体问题,来选择合适的算法。具体看上图:
    需要注意的是:由于BFS具有最短路性质(因为每次遍历都会遍历离当前节点最近的节点,而DFS不能)。因此,如果每条边的权重都为1时,我们可以利用BFS来求解最短路问题。

5. DFS模板

int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}
    DFS算法重点在于思想,因此模板仅供参考即可。

6. BFS模板

queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

7. DFS例题

https://www.acwing.com/problem/content/submission/844/

img
img

    DFS的搜索过程我们可以把它想象成一颗树的形式。我们以三个数字的全排列为例,看看DFS如何解决这样的问题。
    我们可以将初始状态设为三个为空的数字。每次往里填数时,我们都应该确保里面不会重复。例如,第一次填的是1,第二次只能填除1之外的数字,以此类推即可。具体的过程可以参照上图。(红色路径代表DFS的遍历路径)
    这个问题主要是运用了DFS的回溯(回溯的特质:恢复现场)。在下一个问题中,我们会运用DFS的另一个功能:剪枝。
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

//代表最大有7个空位
int path[7];
//代表当前数字是否被填写,未被填写-1
int e[7];
int number,count = 0;

//n代表当前状态有几个空位
void dfs(int n){
    //代表没有空位了,数字填完了,应该输出
    if(n == 0){
        for(int i=0;i<number;i++){
            printf("%d ",path[i]);
        }
        printf("\n");
        return;
    }
    //选择数字
    for(int i=1;i<=number;i++){
        //如果当前数字未被填写,那么填写
        if(e[i-1] == -1){
            path[count++] = i;
            //当前数字已被填写
            e[i-1] = 0;
            //进行下一次填写
            dfs(n - 1);
            //当填写完后,我们需要进行恢复,以便处理其余情况
            e[i-1] = -1;
            count--;
        }
    }
    
}

int main(){
    int n;
    scanf("%d",&n);
    number = n;
    //初始化
    memset(e,-1,sizeof(e));
    dfs(n);
    return 0;
}
https://www.acwing.com/activity/content/problem/content/906/

img

    这道题的思路跟全排列的思路差不多,具体如下:
        1.  由于一个皇后只能放在一行。因此,我们可以将递归的层数看作是行,然后对列进行遍历。
        2.  在遍历列的过程中,尝试将皇后放在每一列,同时确保不会发生题目中的冲突。如果在放置的过程中,没有发生冲突,那么就继续往下递归(继续从下一行开始放置)。如果发生了冲突,那么没有必要继续往下递归,因为这种方案一定不可行,继续递归只会白白浪费时间。因此,我们应该要把这种情况剔除掉(剪枝),直接回溯即可。
        3.  重复上述过程,直到遍历所有行为止,代表找到了一种情况。找到后,继续回溯,寻找下一种情况,直至找到所有情况。
    注意:这道题的对角线包括:正对角线和反对角线。

img

    关于这道题的对角线和反对角线的下标确定问题
        首先,对于一个反对角线:y = x + b。我们可以通过截距b来唯一确定这种反对角线的下标。即:b = y - x;
        其次,对于一个正对角线:y = -x + b。我们仍可以通过截距b来唯一确定这种正对角线的下标。即:b = y + x;
        最后,我们可以把平面直角坐标系,看作n*n的棋盘,那么对于当前层(行)u,以及遍历的列i,上述的内容就可以转换为:
            反对角线下标 = i - u + n;
            由于,j-u下标可能为负数,因此我们需要将其转换为一个正数,因此加上一个偏移量n即可。
            正对角线下标 = i + u;
    其实,就是通过行和列来找到对于这个行和列的正反对角线,进而唯一进行对应。
    例如:当行和列都为0的时候,反对角线下标:n 正对角线下标:0。
    对于其他情况,以此类推即可。
#include <iostream>
#include <cstdio>

using namespace std;

//当存在一个n行n列的矩阵时,那么这个矩阵正对角线的个数:2n-1,反对角线也是如此
const int N = 20;

//定义列,正对角线,反对角线
bool col[N],dg[N],udg[N];
//代表棋盘大小,皇后个数
int n;
//定义棋盘
char chess[N][N];

//u代表层,也代表行
void dfs(int u){
    //如果最后一个皇后放置完成
    if(u == n){
        //输出结果
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                printf("%c",chess[i][j]);
            }
            printf("\n");
        }
        printf("\n");
        return;
    }
    //遍历每一列
    for(int i=0;i<n;i++){
        //如果当前列,当前正对角线,当前反对角线都没有被占
        if(!col[i] && !dg[i + u] && !udg[n - u + i]){
            //放置皇后
            chess[u][i] = 'Q';
            //防止之后,进行占领
            col[i] = dg[i + u] = udg[i - u + n] = true;
            //下一层(行)继续放置
            dfs(u + 1);
            //进行回溯(恢复现场)
            col[i] = dg[i + u] = udg[i - u + n] = false;
            //取消放置皇后
            chess[u][i] = '.';
        }
    }
}

int main(){
    scanf("%d",&n);
    //初始化棋盘
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            chess[i][j] = '.';
        }
    }
    dfs(0);
    return 0;
}

img

    以上介绍的是用全排列的思想去解决n皇后问题。我们接下来再介绍另外一种解题思路:
    1.  我们可以遍历每一个格子。
    2.  遍历格子的时候,我们都可以选择放置皇后和不放置两种选择。这两种选择分别对应着DFS搜索树的分支。
    3.  当我们遍历完最后一个格子后,就代表找到了其中之一的情况(不一定满足题目条件)。
    4.  通过递归和回溯和剪枝就可以找到所有满足题目条件的情况了。
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 20;

bool row[N],col[N],dg[N],udg[N];

char chess[N][N];

int n;
//x,y代表当前遍历的格子坐标,s代表已放置的皇后总数
void dfs(int x,int y,int s){
    //如果列出界了
    if(y == n){
        //转换到下一行
        y = 0;
        x++;
    }
    //如果行遍历完了,代表找到了某种情况
    if(x == n){
        //如果在这种情况下,皇后放置完毕
        //那么就输出
        if(s == n){
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    printf("%c",chess[i][j]);
                }
                printf("\n");
            }
            printf("\n");
        }
        return;
    }
    //两种选择
    //1. 不放置皇后
    dfs(x,y+1,s);
    //2. 放置皇后
    if(!row[x] && !col[y] && !dg[x+y] && !udg[y-x+n]){
        chess[x][y] = 'Q';
        row[x] = col[y] = dg[x+y] = udg[y-x+n] = true;
        dfs(x,y+1,s+1);
        //恢复现场
        row[x] = col[y] = dg[x+y] = udg[y-x+n] = false;
        chess[x][y] = '.';
    }
}

int main(){
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            chess[i][j] = '.';
        }
    }
    dfs(0,0,0);
    return 0;
}

    以上介绍八皇后的两种遍历方式时,我们可以发现:第一种比第二种的时间复杂度要低。因为,第一种是直接按行遍历的,每行一定会放置一个皇后,因此不会遍历到皇后总数小于n的情况,这种遍历方式皇后总数一定等于n。而第二种情况是按照格子来进行遍历的,每一种格子都会产生放置和不放置的情况。因此,整个遍历过程会出现每一个格子都不会放置皇后的情况或者皇后总数小于n的情况。因此,第二种遍历方式比第一种遍历方式遍历的情况要多。因此,第一种遍历方式比第二种遍历方式的时间复杂度低。
    以上的情况证明了一点:dfs算法根据搜索方式的不同,时间复杂度也不同。

8. BFS例题

https://www.acwing.com/activity/content/problem/content/907/
    对于这样的问题,我们可以用BFS算法来进行求解。因为BFS算法具有最短路性质。而DFS算法只能保证可以搜到终点,却不能保证是以最短路径搜到终点。
    用bfs算法来解决迷宫问题的思路如下:
        1. 从起点开始遍历,找到所有距离为1的点。并且需要保证这些点可以行走,并且给当前点添加标记,放置重复遍历。
        2. 从各种距离为1的点开始,找到所有距离为2的点。保证可以行走的同时,添加标记。
        3. 重复上述操作,直到找到终点为止。此时的步数就是最优的。因为bfs具有最短路性质(每条边权重相等)。
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 110;
int n,m;
//代表地图
int g[N][N];
//代表记忆数组,防止重复遍历,也可以反应当前点到起点的距离。
int d[N][N];
//代表队列
pair<int,int> q[N*N];
int hh = 0;
int tt = -1;
//代表路径数组,用于输出路径
pair<int,int> Prev[N][N];

int bfs(){
    //将起点加入到队列中
    q[++tt] = {0,0};
    //起点已经遍历过,起点到起点的距离为0
    d[0][0] = 0;
    while(hh <= tt){
        //取出队头元素
        pair<int,int> t = q[hh++];
        //代表上右下左四个方向的偏移量
        int dx[4] = {-1,0,1,0};
        int dy[4] = {0,1,0,-1};
        //尝试往四个方向行走
        for(int i=0;i<4;i++){
            int x = t.first + dx[i];
            int y = t.second + dy[i];
            //如果下一个点没有越界且是空地且没有遍历过
            if(x >= 0 && x < n  && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1){
                //更新距离
                d[x][y] = d[t.first][t.second] + 1;
                // //记录路径
                // Prev[x][y] = t;
                //将当前点添加到队列
                q[++tt] = {x,y};
            }
            
        }
    }
    // //输出路径
    // int dx = n-1;
    // int dy = m-1;
    // while(dx || dy){
    //     cout << dx << "," << dy << endl;
    //     pair<int,int> t = Prev[dx][dy];
    //     dx = t.first;
    //     dy = t.second;
    // }
    return d[n-1][m-1];
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            scanf("%d",&g[i][j]);
        }
    }
    //初始化记忆数组,-1代表没有遍历过该节点,除此之外,均代表遍历过
    memset(d,-1,sizeof(d));
    printf("%d",bfs());
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。