【算法笔记】6.搜索与图论

DFS

DFS 重点是 顺序,回溯问题注意复原操作

843. n−皇后问题:将 n 个皇后放在 $n×n$ 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。现在给定整数 n,请你输出所有的满足条件的棋子摆法。

方法一:DFS(行)+ 剪枝,按行枚举,下一行必须放子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;

const int N = 10;
char g[N][N];
bool col[N], dg[N*2], udg[N*2]; // 对角线 dg(/), udg(\) 有 2*n-1 条
int n;

void dfs(int r) {
    // 最后一行已完成,输出
    if (r == n) {
        for (int i = 0; i < n; i++) puts(g[i]);
        puts("");
        return;
    }
    // 当前行选择一个
    for (int c = 0; c < n; c++) {
        // 剪枝,其中 [c - u + n] 保证下标为正
        if (!col[c] && !dg[c + r] && !udg[c - r + n]) {
            g[r][c] = 'Q';
            col[c] = dg[c + r] = udg[c - r + n] = true;
            dfs(r + 1);
            // 复原
            col[c] = dg[c + r] = udg[c - r + n] = false;
            g[r][c] = '.';
        }
    }
}

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;
}

方法二:DFS(点)+ 剪枝,更原始的按点搜索,下一个点不一定放子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
using namespace std;

const int N = 10;
char g[N][N];
bool row[N], col[N], dg[N*2], udg[N*2]; // 对角线 dg(/), udg(\) 有 2*n-1 条
int n;

void dfs(int r, int c, int sum) {
    // 一行结束,继续下一行
    if (c == n) r++, c = 0;
    
    // 最后一行已完成,但可能不满足条件
    if (r == n) {
        if (sum == n) {
            // 已经放置的棋子数目达到 n,输出
            for (int i = 0; i < n; i++) puts(g[i]);
            puts("");
        }
        return;
    }
    
    // 当前点不选
    dfs(r, c+1, sum);
    
    // 当前点选
    // 剪枝,其中 [c - u + n] 保证下标为正
    if (!row[r] && !col[c] && !dg[c + r] && !udg[c - r + n]) {
        g[r][c] = 'Q';
        row[r] = col[c] = dg[c + r] = udg[c - r + n] = true;
	    // dfs(r, c+1, sum+1);
	    dfs(r+1, 0, sum+1);    // 优化:放子后,当前行一点不能再放子了
        // 复原
        row[r] = col[c] = dg[c + r] = udg[c - r + n] = false;
        g[r][c] = '.';
    }
}

int main() {
    cin >> n;
    // 初始化
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            g[i][j] = '.';
        }
    }
    
    dfs(0, 0, 0);
    
    return 0;
}

比较按行搜索与按点搜索:

  1. 按行层次更高,已经从题意剥离出行互斥的信息(当然按点也可以在这方面优化)
  2. 落子情况和最终状态不同
    1. 按行每次循环保证落子,落子后再递归下一行,所以当 u = n 时一定满足要求
    2. 按点则可能当 x = n 时后面几行没有落子,所以要多一个参数记录落子数
  3. 按行搜索时间复杂度远远低于按点搜索

BFS

844. 走迷宫:给定一个 $n×m$ 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。最初,有一个人位于左上角 $(1,1)$ 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 $(n,m)$ 处,至少需要移动多少次。

数据保证 $(1,1)$ 处和 $(n,m)$ 处的数字为 0,且一定至少存在一条通路。

BFS 求边权相同的最短路

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <cstring> // memset
#include <queue>
using namespace std;

typedef pair<int, int> PII;

const int N = 110;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
int g[N][N], d[N][N];
int n, m;

int bfs() {
    // -1 代表未访问
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    
    queue<PII> q;
    q.push({0, 0});
    
    while (q.size()) {
        auto t = q.front(); q.pop();
        for (int i = 0; i < 4; i++) {
            int x = t.first + dx[i], 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();
    
    return 0;
}

845. 八数码(需要维护更复杂的状态):在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。例如:

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <cstring>  // find
#include <queue>
#include <unordered_map>
using namespace std;

const int N = 100;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
unordered_map<string, int> d;   // distance

int bfs(string start) {
    string end = "12345678x";
    queue<string> q;
    q.push(start);
    while (q.size()) {
        string t = q.front(); q.pop();
        int dist = d[t];
        if (t == end) return dist;
        
        for (int i = 0; i < 4; i++) {
            int k = t.find('x');    // string.find(x) 返回字符 x 下标
            int a = k / 3, b = k % 3;
            int x = a + dx[i], y = b + dy[i];
            if (x >= 0 && x < 3 && y >= 0 && y < 3) {
                swap(t[k], t[x*3 + y]);
                if (!d.count(t)) {
                    // 新的局面,更新距离并加入队列
                    d[t] = dist + 1;
                    q.push(t);
                }
                // 注意状态恢复,继续下一个方向
                swap(t[k], t[x*3 + y]);
            }
        }
    }
    return -1;
}

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

树、图中的 DFS

846. 树的重心:给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

每次 dfs 可以确定假设 u 为重心的最大连通块的节点数,并且更新一下 ans。即 dfs 并不直接返回答案,而是在每次更新中迭代一次答案。

tree-center-gravity.excalidraw.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5+10;
int h[N], e[N*2], ne[N*2], idx; // 无向图方式存储树,2*(n-1)条边
bool st[N];     // 防止回溯访问父节点
int n;
int ans = N;    // 重心的所有子树中,最大的结点数目

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 返回以 u 为根的所有子树的节点数量总和(包括 u 自身)
int dfs(int u) {
    st[u] = true;
    int sum = 1;    // 包含自身节点
    int cnt = 0;    // 如果将这个点删除后,剩余各个连通块中点数的最大值
    for (int i = h[u]; i != -1; i = ne[i]) {
        int v = e[i];
        if (!st[v]) {
            int s = dfs(v);
            sum += s;           // 各子节点数目累加
            cnt = max(cnt, s);
        }
    }
    cnt = max(cnt, n - sum);    // 父节点所在的连通块点数
    ans = min(ans, cnt);        // 最小的最大联通子图节点数
    return sum;
}

int main() {
    memset(h, -1, sizeof h);
    
    cin >> n;
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);   // 双向便于遍历
    }
    
    // 无向图,可以从任意节点开始遍历
    dfs(1);
    cout << ans;
    
    return 0;
}

树、图中的 BFS

847. 图中点的层次:给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。所有边的长度都是 1,点的编号为 1∼n。

请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;

const int N = 1e5+10;
int h[N], e[N], ne[N], idx;
int d[N];
int n, m;

// 添加一条边 a->b
void add(int a, int b) {
  e[idx] = b, ne[idx] = h[a], h[a] = idx++;  
}

int bfs() {
    memset(d, -1, sizeof d);
    
    queue<int> q;
    q.push(1);  // start from node 1
    d[1] = 0;
    
    while (q.size()) {
        int u = q.front(); q.pop();
        for (int i = h[u]; i != -1; i = ne[i]) {
            int v = e[i];
            if (d[v] == -1) {
                d[v] = d[u] + 1;
                q.push(v);
            }
        }
    }
    return d[n];
}

int main() {
    cin >> n >> m;
    
    // 邻接表初始化
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    
    cout << bfs();
    
    return 0;
}

拓扑排序

848. 有向图的拓扑序列:给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x, y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

有环图不存在拓扑序列,判断存在拓扑序列等价判断是图中是否存在环

入度表法

选择入度为 0 的点作为起点,将所有入度为 0 的节点入队,并更新剩下的点的入度,若有新的节点入度为 0 则加入队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5+10;
int h[N], e[N], ne[N], idx; // 邻接表
int q[N], hh, tt = -1;      // 队列
int d[N];                   // 点的入度
int n, m;

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool topo() {
    // 所有入度为 0 的点加入队列
    for (int i = 1; i <= n; i++) {
        if (d[i] == 0) {
            q[++tt] = i;
        }
    }
    
    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h[t]; i != -1; i = ne[i]) {
            int v = e[i];
            d[v]--;
            if (d[v] == 0) q[++tt] = v;
        }
    }
    
    return tt == n - 1; // 队列中有 n 个点
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m--) {
        int a, b;
        cin >> a >> b;
        add(a, b);
        d[b]++;
    }
    
    if (topo()) {
        for (int i = 0; i < n; i++) cout << q[i] << ' ';
    } else {
        cout << -1;
    }
    
    return 0;
}

入度表法既可以使用 BFS 也可以使用 DFS

更多拓扑代码解法:207. 课程表(判断序列是否存在)| 210. 课程表 II(给出任意序列)

标记法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
    vector<vector<int>> child;  // 邻接表
    vector<int> st;             // 访问状态
    vector<int> path;
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        child.resize(numCourses);
        st.resize(numCourses, 0);
        // 建图
        for (auto& p: prerequisites) {
            int a = p[1], b = p[0]; // a->b
            child[a].push_back(b);
        }
        // DFS
        for (int i = 0; i < numCourses; i++) {
            if (!dfs(i)) return {};
        }
        // 路径逆序
        reverse(path.begin(), path.end());
        return path;
    }
    //  0 未被 DFS 访问
    // -1 其他节点启动的 DFS 访问过了,不需再访问
    //  1 本节点启动的 DFS 访问过了,遇到说明有环
    bool dfs(int u) {
        if (st[u] == -1) return true;
        if (st[u] == 1) return false;
        st[u] = 1;
        for (auto v: child[u]) {
            if (!dfs(v)) return false;
        }
        st[u] = -1;
        path.push_back(u);
        return true;
    }
};
  • 输出所有可能的拓扑序列

最短路问题

图的表示方式:稠密图→邻接矩阵,稀疏图→邻接表(详见博客

这里不区分有向图、无向图,均为有向图的算法,因为无向图是特殊的有向图

image.png

Dijkstra

朴素版

自环不影响最短路,重边在建图读入时选择最短的一条

849. Dijkstra求最短路 I

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510, inf = 0x3f3f3f3f;
int g[N][N];    // 邻接矩阵
int dist[N];    // 1号点到 n号点的最短距离
bool st[N];
int n, m;

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < n; i++) {
        // find min dist O(n^2) total
        int t = -1;
        for (int j = 1; j <= n; j++) {
            if (st[j]) continue;
            if (t == -1 || dist[j] < dist[t]) t = j;
        }
        st[t] = true;
        // update O(m) total
        for (int j = 1; j <= n; j++) {
            dist[j] = min(dist[j], dist[t] + g[t][j]);
        }
    }
    if (dist[n] == inf) return -1;
    return dist[n];
}

int main() {
    memset(g, 0x3f, sizeof g);
    cin >> n >> m;
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c);
    }
    
    cout << dijkstra();
    
    return 0;
}

堆优化版

堆的实现

  1. 手写堆:可直接修改制定元素,需要维护两个映射。n 个元素 O(mlgn)
  2. 优先队列:不能直接修改指定元素,会有冗余。m 个元素 O(mlgm)

自环不影响最短路,重边不需要处理(堆中冗余元素)

850. Dijkstra求最短路 II

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;

typedef pair<int, int> PII;

const int N = 150010, inf = 0x3f3f3f3f;
int h[N], e[N], w[N], ne[N], idx;    // 邻接表
int dist[N];    // 1号点到 n号点的最短距离
bool st[N];
int n, m;

// 添加一条边a->b,边权为c
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; 
}

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    // 定义小根堆
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});  // {dist, i}
    
    while (heap.size()) {
        // find min dist O(n) total
        auto t = heap.top(); heap.pop();
        int u = t.second;
        if (st[u]) continue;    // 排除冗余
        st[u] = true;
        
        // update O(m*logn) total [实际使用优先队列 O(m*logm)]
        for (int i = h[u]; i != -1; i = ne[i]) {
            int v = e[i], wg = w[i];
            if (dist[u] + wg < dist[v]) {
                dist[v] = dist[u] + wg;
                heap.push({dist[v], v});
            }
        }
    }
    if (dist[n] == inf) return -1;
    return dist[n];
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    
    cout << dijkstra();
    
    return 0;
}

Bellman-Ford

853. 有边数限制的最短路:给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。 请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

边 a → b (w)

松弛操作:d[b] = min(d[b], d[a] + w)

三角不等式:d[b] ≤ d[a] + w

出发点与目标的路径中存在负权回路则无最短路径,BF 算法可检测负权回路。第 k 次迭代是不超过 k 步的最短路径,若第 n 次迭代仍有最短路更新,则说明存在 n 条边的最短路,则存在负权回路。但检测负权回路一般不用 BF 因为时间复杂度较高。

限制了路径的边数,有负环不影响。

外层 k 次迭代,得到不超过 k 条边的最短路径,内层遍历所有边。

不需要考虑点与边的关系,无脑遍历所有边,边直接用结构体数组存储。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510, M = 10010;
int dist[N], backup[N];
int n, m, k;

struct Edge{
    int a, b, w;
} edges[M];

int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < k; i ++) {
        memcpy(backup, dist, sizeof dist);        // 防止更新串联,备份上次迭代的数据
        for (int j = 0; j < m; j ++) {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    return dist[n];
}

int main() {
    cin >> n >> m >> k;
    for (int i = 0; i < m; i ++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    int t = bellman_ford();
    
    if (ans > 0x3f3f3f3f / 2) puts("impossible");
    else cout << ans;
    
    return 0;
}

SPFA

SPFA (Shortest Path Faster Algorithm) 是 Bellman Ford 的优化版

dist[b] = min(dist[b], backup[a] + w);

当 backup[a] 变小才会更新,记录变小的 backup[a] 来选择更新

最短路径

851. spfa 求最短路:迭代过程中不对所有边遍历,只对 dist[a] 减少的点的所有出边遍历

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;
const int N = 1e6 + 10;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];  // 点是否在队列中

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int spfa() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    queue<int> q;
    q.push(1);
    st[1] = true;   // 在队列中
    
    while (q.size()) {
        int t = q.front();
        q.pop();
        st[t] = false;
        
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    return dist[n]; 
}

int main() {
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);
    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    int t = spfa();
    if (t == 0x3f3f3f3f) puts("impossible");
    else cout << t << endl;

    return 0;
}

判断负环

记录求最短路径时途径点的个数,当个数 ≥ n 时,说明存在负环。

不求 dist 的绝对大小,可不初始化为 inf。

dist 初始全为 0,遇到负边更新,有负环则不断绕圈,dist 不断减小。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;

const int N = 1e6 + 10;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[N];
bool st[N];  // 是否在队列中

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int spfa() {
    queue<int> q;
    for (int i = 1; i <= n; i ++) {          // 从 1 出发的点经过的路径不一定含负环
        q.push(i);                           // 初始时所有点全部加入保证一定能检测到环
        st[i] = true;
    }
    
    while (q.size()) {
        int t = q.front();
        q.pop();
        st[t] = false;
        
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                
                if (cnt[j] >= n) return true;
                
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);
    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;
}

Floyd

dp 思路:d[k, i, j] 只经过 1~k 中的点,i → j 的最短路径

d[k, i, j] = d[k - 1, i, k] + d[k-1, k, j] → d[i, j] = d[i, k] + d[k, j]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];

void floyd() {
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}           

int main() {
    cin >> n >> m >> Q;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;
            
    int a, b, w;
    while (m--) {
        scanf("%d%d%d", &a, &b, &w);
        d[a][b] = min(d[a][b], w);    // 重边
    }
    
    floyd();
    
    while (Q--) {
        cin >> a >> b;
        if (d[a][b] > INF / 2 ) puts("impossible");
        else cout << d[a][b] << endl;
    }
    
    return 0;
}

最小生成树

image.png

Prim

858. Prim算法求最小生成树:与 Dijkstra 类似,Dijkstra 更新其他点到 起点 的距离,Prim 更新其他点到 集合 的距离

先加入集合,累加结果,再更新其他点(先更新会引入自环)

  • 堆优化版
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510, INF = 0x3f3f3f3f;
int g[N][N], dist[N];       // dist[] 点到集合的距离
bool st[N];
int n, m;

int prim() {
    memset(dist, 0x3f, sizeof dist);
    
    int res = 0;
    for (int i = 0; i < n; i++) {
        int t = -1;
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        if (i && dist[t] == INF) return -1;  // 存在不连通的情况
        
        // 加入集合并更新结果,初始第一个点不加
        st[t] = true;
        if (i) res += dist[t];
        
        // 跟新其他点到集合的距离
        for (int j = 1; j <= n; j++)
            dist[j] = min(dist[j], g[t][j]);   
    }
    
    return res;
}

int main() {
    scanf("%d%d", &n, &m);
    
    memset(g, 0x3f, sizeof g);
    
    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    
    int t = prim();
    if (t == -1) puts("impossible");
    else printf("%d\n", t);
    
    return 0;
}

Kruskal

859. Kruskal算法求最小生成树

  1. 对边由小至大排序
  2. 边的两点不在同一连通则加入,加入了 n - 1 条边,则找到最小生成树
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 200010;
int n, m;
int p[N];

struct Edge{
    int a, b, w;
    bool operator < (const Edge &e) {
        return w < e.w;
    }
} edges[N];

int Find(int x) {
    if (p[x] != x) p[x] = Find(p[x]);
    return p[x];
}

int main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    // 对边进行排序
    sort(edges, edges + m);
    
    for (int i = 0; i < m; i++) p[i] = i;
    
    int res = 0, cnt = 0;
    for (int i = 0; i < m; i++) {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        a = Find(a), b = Find(b);
        if (a != b) {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }
    if (cnt < n - 1) puts("impossible");  // 加入 n - 1 次,加入 n - 1 条边,得到 n 个点
    else cout << res;
    
    return 0;
}

二分图

二分图:一条边的两个端点不在同一个集合

给定一个图是二分图,当且仅当不含奇数环

染色法

860. 染色法判定二分图:算法时间复杂度:$O(n+m)$

图中不含奇数环,在交替染色过程中不会存在染色矛盾。0 未染色 1 A 2 B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 100010, M = N * 2;
int h[N], e[M], ne[M], idx;
int color[N];
int n, m;

// 添加一条边a->b
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool dfs(int u, int c) {
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!color[j]) {
            if (!dfs(j, 3 - c))  return false;
            // dfs(j, 3 - c);
        }
        else if (color[j] == c)  return false;
    }
    return true;
}

int main() {
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    int a, b;
    while (m--) {
        scanf("%d%d", &a, &b);
        add(a, b); add(b, a);
    }
    
    bool flag = true;
    // 图可能不连通
    for (int i = 1; i <= n; i ++) {
        if (!color[i]) {
            if (!dfs(i, 1)) {
                flag = false;
                break;
            }
        }
    }
    
    if (flag) puts("Yes");
    else puts("No");
    
    return 0;
}

匈牙利算法

861. 二分图的最大匹配:算法时间复杂度:$O(mn)$,实际运行时间远小于 $O(mn)$

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 510, M = 100010;
int h[N], e[M], ne[M], idx;
int n1, n2, m;
int match[N];      // 右集合的匹配
bool st[N];        // 每次搜索右集合的元素不能重复

int add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int find(int x) {
    for (int i = h[x]; i != -1; i = ne[i]) {
        int j = e[i];
        // 右集合的元素还没考虑过
        if (!st[j]) {
            st[j] = true;
			// 右边 j 没有匹配 or 已经匹配 j 的左部可以找到其他右部
            if (match[j] == 0 || find(match[j])) {
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

int main() {
    cin >> n1 >> n2 >> m;

    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    int res = 0;
    for (int i = 1; i <= n1; i++){
        memset(st, false, sizeof st);  // 搜索右集合的元素不能重复
        if (find(i)) res ++;
    }
    cout << res;
    
    return 0;
}
0%