重温DFS&BFS

深度优先搜索(DFS)和广度优先搜索(BFS)是两种搜索树和图的基本策略。其搜索的思想所引申来的题目也比较多,需要引起足够的重视

深度优先搜索DFS

深度优先搜索它是从某个状态开始,不断地转移状态直到无法转移(一层一层往下搜),然后回退到前一步状态,继续转移到其他状态,如此不断重复,直至找到最终的解。

image-20201204153912604

邻接矩阵存储图的DFS遍历

  • 节点编号为 1 ~ n
const int INF = 0x3f3f3f3f;
// 邻接矩阵存储图的信息g[i][j]表示顶点 i 到顶点 j 这条边的权值大小
int g[N][N];
bool st[N];
// 初始化
memset(g, 0x3f, sizeof g);

void dfs(int u) {
    // 先标记这个点已经被访问过
    st[u] = true;
    // 递归地去遍历和该点连通的点
    for (int i = 1; i <= n; i++) {
        if (!st[i] && g[u][i] != INF)
            dfs(i);
    }
}

邻接表存储图的DFS遍历

  • 节点编号为 1 ~ n
// 使用邻接表来存储图
vector<int> g[N];
bool st[N];

void dfs(int u) {
    // st[u] 表示点u已经被遍历过
    st[u] = true;
	// 递归地去遍历和该点连通的点
    for (int i = 0; i < g[u].size(); i++) {
        if (!st[g[u][i]]) 
            dfs(g[u][i]);
    }
}

树的重心

给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心

输入格式

第一行包含整数n,表示树的结点数。接下来n-1行,每行包含两个整数a和b,表示点a和点b之间存在一条边。

输出格式

输出一个整数m,表示将重心删除后,剩余各个连通块中点数的最大值。

数据范围

1n1051≤n≤10^5

输入样例

9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6

输出样例:

4
image-20201204210031426
  • DFS遍历每个结点,记录以每个结点为根的子树中点的数量,不断更新,找到子树连通块中最大数的连通块
  • 该结点上面一共 nsumn - sum个结点,再取最大值
  • 找到该最大值最小的那一种情况
#include <iostream>
#include <vector>

using namespace std;

const int N = 100010;

vector<int> g[N];
bool st[N];
int ans = N;
int n;

// 以u为根的子树中点的数量
int dfs(int u) {
    st[u] = true;
    
    // sum记录子树中所有连通块的结点数量
    int sum = 1;
    // 记录将该点删掉之后,子树中连通块的最大值
    int size = 0;
    for (int i = 0; i < g[u].size(); i++) {
        int j = g[u][i];
        if (!st[j]) {
            // 记录当前子树的结点
            int s = dfs(j);
            size = max(size, s);
            sum += s;    
        }
    }
    // 和非子树的连通块的结点数比较,取最大结点数
    size = max(size, n - sum);
    // 答案取最小的一种情况
    ans = min(ans, size);
    return sum;
}

int main() {
    cin >> n;
    
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    
    dfs(1);
    
    cout << ans << endl;
    return 0;
}

枚举排列

给定一个整数n,将数字1~n排成一排,将会有很多种排列方法。现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1n71≤n≤7

输入样例:

3

输出样例:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

这其实是一种DFS搜索的思想,我们构造一颗搜索树:

image-20200426220829697

在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列。「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:撤销选择,恢复现场,即为回溯

#include <iostream>

using namespace std;

const int N = 10;

int n;
int path[N];
bool st[N];

void dfs(int u) {
    // 已经做完了全部选择,输出路径
    if (u == n) {
        for(int i = 0; i < n; i++) 
            cout << path[i] << ' ';
        cout << endl;
        return;
    }
    // 还可继续向下递归
    for (int i = 1; i <= n; i++) {
        // 如果该数字没有被用过
        if (!st[i]) {
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            // 恢复现场
            st[i] = false;
        }
    }
}

int main() {
    cin >> n;
    dfs(0);
    return 0;
}

n - 皇后问题

n-皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

1_597ec77c49-8-queens.png

现在给定整数n,请你输出所有的满足条件的棋子摆法。

输入格式

共一行,包含整数n。

输出格式

每个解决方案占n行,每行输出一个长度为n的字符串,用来表示完整的棋盘状态。其中”.”表示某一个位置的方格状态为空,”Q”表示某一个位置的方格上摆着皇后。每个方案输出完成后,输出一个空行。输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围

1n91≤n≤9

输入样例:

4

输出样例:

.Q..
...Q
Q...
..Q.

..Q.
Q...
...Q
.Q..
  • 相当于是枚举每一行的皇后放在哪一列
  • 只是多了规则,很多不合理的方案可以提前结束搜索,这就是所谓需要进行剪枝
#include <iostream>

using namespace std;

const int N = 20;

int n;
char g[N][N];

/**
 * 检查第i行和第j列能否放置
 **/
bool isValid(int x, int y) {
    // 检查第y列上是否有冲突
    for (int i = 0; i < n; i++) {
        if (g[i][y] == 'Q')
            return false;
    }
    // 检查右上方是否有皇后冲突(因为我们逐层放,只用考虑斜上方的情形)
    for (int i = x - 1, j = y + 1; i >= 0 && j < n; i--, j++) {
        if (g[i][j] == 'Q')
            return false;
    }
    // 检查左上方是否有皇后冲突
    for (int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {
        if (g[i][j] == 'Q')
            return false;
    }
    return true;
}

void dfs(int u) {
    if (u == n) {
        for (int i = 0; i < n; i++)
            puts(g[i]);
        puts("");
        return;
    }

    for (int i = 0; i < n; i++) {
        // 这里相当于进行剪枝操作
        if (!isValid(u, i))
            continue;
        // 选择
        g[u][i] = 'Q';
        dfs(u + 1);
        // 撤销选择
        g[u][i] = '.';
    }
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i++) 
        for (int j = 0; j < n; j++)
            g[i][j] = '.';

    dfs(0);
    return 0;
}


广度优先搜索 BFS

  • 广度优先搜索(BFS)与深度优先搜索不同在于搜索的顺序,广度优先搜索总是先搜索距离初始状态近的状态。一层一层往下搜索,使用队列。(开始状态 --> 只需要1次就可以到达的所有状态 --> ......)对于同一个状态,广度优先搜索只经过一次,因此时间复杂度为O(状态数×转移方式)
  • 广度优先搜索可以找到最短路
image-20201204171227363

邻接矩阵存储图的BFS遍历

  • 节点编号为 1 ~ n
const int INF = 0x3f3f3f3f;
// 邻接矩阵存储图的信息g[i][j]表示顶点 i 到顶点 j 这条边的权值大小
int g[N][N];
bool st[N];
// 初始化
memset(g, 0x3f, sizeof g);
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (!q.empty()) {
    int t = q.front();
    q.pop();
    for (int i = 1; i <= n; i++) {
        if (!st[i] && g[t][i] != INF) {
            st[i] = true; // 表示点i已经被遍历过
            q.push(i);
        }
    }
}

邻接表存储图的BFS遍历

  • 节点编号为 1 ~ n
vector<int> g[N];
queue<int> q;
bool st[N];
// 初始化
st[1] = true;
q.push(1);
while (!q.empty()) {
    int t = q.front();
    q.pop();
    for (int i = 0; i < g[t].size(); i++) {
        int j = g[t][i];
        if (!st[j]) {
            st[j] = true;	// 表示点j已经被遍历过
            q.push(j);
        }
    }
}

图中点的层次

给定一个n个点m条边的有向图,图中可能存在重边和自环。所有边的长度都是1,点的编号为1~n。请你求出1号点到n号点的最短距离,如果从1号点无法走到n号点,输出-1。

输入格式

第一行包含两个整数n和m。接下来m行,每行包含两个整数a和b,表示存在一条从a走到b的长度为1的边。

输出格式

输出一个整数,表示1号点到n号点的最短距离。

数据范围

1n, m1051≤n,\ m≤10^5

输入样例:

4 5
1 2
2 3
3 4
1 3
1 4

输出样例:

1
  • 我们使用BFS遍历,每一次队列取出元素:更新距离,最后输出d[n]d[n]
#include <iostream>
#include <queue>

using namespace std;
const int N = 100010;

int n, m;
vector<int> g[N];
int d[N];

int bfs() {
    memset(d, -1, sizeof d);
    queue<int> q;
    d[1] = 0;
    q.push(1);
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        for (int i = 0; i < g[t].size(); i++) {
            int j = g[t][i];
            // 如果这个点没有遍历到,更新距离
            if (d[j] == -1) {
                d[j] = d[t] + 1;
                q.push(j);
            }
        }
    }
    return d[n];
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
    }
    
    cout << bfs() << endl;
    return 0;
}

走迷宫

给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。

输入格式

第一行包含两个整数n和m。接下来n行,每行包含m个整数(0或1),表示完整的二维数组迷宫。

输出格式

输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围

1n,m1001≤n,m≤100

输入样例:

5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0

输出样例:

8
  • d[i][j]d[i] [j] 记录了搜索到(i,j)(i, j)所需的最少次数
#include <iostream>
#include <cstring>
#include <queue>

using namespace std;
const int N = 110;
int n, m;
int g[N][N], d[N][N];

typedef pair<int, int> PII;

int bfs() {
    queue<PII> q;
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    q.push({0, 0});
    int dx[4] = {-1, 0, 1, 0};
    int dy[4] = {0, 1, 0, -1};
    
    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        // 枚举4个方向
        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;
                q.push({x, y});
            }
        }
    }
    
    return d[n - 1][m - 1];
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cin >> g[i][j];
    cout << bfs() << endl;
    return 0;
}

八数码

在一个3×3的网格中,1~8这8个数字和一个“x”恰好不重不漏地分布在这3×3的网格中。例如:

1 2 3
x 4 6
7 5 8

在游戏过程中,可以把“x”与其上、下、左、右四个方向之一的数字交换(如果存在)。我们的目的是通过交换,使得网格变为如下排列(称为正确排列):

1 2 3
4 5 6
7 8 x

例如,示例中图形就可以通过让“x”先后与右、下、右三个方向的数字交换成功得到正确排列。交换过程如下:

1 2 3   1 2 3   1 2 3   1 2 3
x 4 6   4 x 6   4 5 6   4 5 6
7 5 8   7 5 8   7 x 8   7 8 x

现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。

输入格式

输入占一行,将3×3的初始网格描绘出来。例如,如果初始网格如下所示:
1 2 3

x 4 6

7 5 8

则输入为:1 2 3 x 4 6 7 5 8

输出格式

输出占一行,包含一个整数,表示最少交换次数。如果不存在解决方案,则输出”-1”。

输入样例:

2  3  4  1  5  x  7  6  8 

输出样例

19
  • 最少交换次数 ==> BFS
  • 这里是3×33×3的二维数组,在我们使用BFS时候状态表示就显得不方便,因此我们做一个转换:将二维转换成一维,我们使用stringstring来表示每一个状态,最终我们需要找到的状态就是12345678x12345678x
  • 在搜索的过程中,一维再换回二维去更新状态并更新距离数组dist
#include <iostream>
#include <string>
#include <unordered_map>
#include <queue>

using namespace std;

int bfs(string start) {
    string end = "12345678x";
    unordered_map<string, int> dist;
    dist[start] = 0;
    queue<string> q;
    q.push(start);
    if (start == end)
        return dist[start];

    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        int res = dist[t];
        // 如果找到了结果,直接返回
        if (t == end)
            return res;
        int k = t.find('x');
        int x = k / 3, y = k % 3;
        // 找到下一个可以更新的状态
        for (int i = 0; i < 4; i++) {
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < 3 && b >= 0 && b < 3) {
                // 更新状态
                swap(t[k], t[a * 3 + b]);
                // 如果更新后的状态是之前没有搜到过的状态,更新距离并加入到队列
                if (!dist.count(t)) {
                    dist[t] = res + 1;
                    q.push(t);
                }
                // 恢复状态
                swap(t[k], t[a * 3 + b]);
            }
        }
    }
    
    return -1;
}

int main() {
    string start;
    for (int i = 0; i < 9; i++) {
        char c;
        cin >> c;
        start += c;
    }
    cout << bfs(start) << endl;
    return 0;
}
赞赏