定义图中,n为点数,m为边数

m= $O(n^2)$,则认为是稠密图
m = $O(n)$,则认为是稀疏图

树与图的存储

  • 邻接表,适用于稀疏图
    1
    2
    3
    4
    5
    6
    const int N = 1e6+5;
    int h[N], e[N], ne[N], idx = 0;

    void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
  • 邻接矩阵,适用于稠密图
    1
    2
    const int N = 1e6+5;
    int g[N][N];

深度优先搜索 DFS

  • 用栈或函数递归来实现
    朴素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
    52
    53
    54
    55
    56
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>

    const int N = 1e6+5;
    int h[N], e[N], ne[N], idx = 0;
    int st[N], n, m;
    int stk[N], top = 0;

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

    // 递归版
    void dfs(int u) {
    if (st[u]) return;
    printf("%d -> ", u);
    st[u] = 1;
    int node = h[u];
    while (node != -1) {
    dfs(e[node]);
    node = ne[node];
    }
    }

    // 迭代版
    void dfs(int u) {
    // init stack
    stk[top] = 0;
    while (top >= 0) {
    // pop from stack
    int node = stk[top--];
    st[node] = 1;
    printf("%d -> ", node);
    for (int i = h[node]; i != -1; i = ne[i]) {
    if (st[e[i]]) continue;
    stk[++top] = e[i];
    }
    }
    }

    int main() {
    // init
    memset(h, -1, sizeof(h));
    memset(st, 0, sizeof(st));
    // build graph
    scanf("%d%d", &n, &m);
    while(m--) {
    int a, b;
    scanf("%d%d", &a, &b);
    add(a,b);
    add(b, a);
    }
    dfs(0);
    return 0;
    }

例题:回溯&剪枝, Leetcode 46. 全排列

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
class Solution {
private:
vector<vector<int>> ans;
vector<int> tmp;
vector<bool> visited;
public:
vector<vector<int>> permute(vector<int>& nums) {
ans.clear();
tmp.clear();
visited.assign(nums.size(), false);
helper(nums);
return ans;
}
void helper(vector<int>& nums) {
// 终止条件判断
if (tmp.size() == nums.size()) {
ans.push_back(tmp);
return;
}
for (int i = 0; i < nums.size(); i++) {
// 枚举
if (!visited[i]) {
visited[i] = true;
tmp.push_back(nums[i]);
helper(nums);
tmp.pop_back();
visited[i] = false;
}
}

}
};

宽度优先搜索 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
#include <cstring>
#include <cstdio>
#include <iostream>

using namespace std;

const int N = 100;
typedef pair<int, int> pii;
int maze[N][N];
int d[N][N];
pii q[N*N];
int n, m, hh = 0, tt = 0;

int bfs() {
memset(d, -1, sizeof(d));
d[0][0] = 0;
q[tt++] = {0, 0};
int dx[4] = {0, 1, 0 , -1}, dy[4] = {1, 0, -1, 0};
while (hh < tt) {
auto [x, y] = q[hh++];
for (int i = 0; i < 4; i++) {
if (x + dx[i] >= 0 && x + dx[i] < n
&& y + dy[i] >= 0 && y + dy[i] < m
&& maze[x+dx[i]][y+dy[i]] == 0
&& d[x+dx[i]][y+dy[i]] == -1) {
d[x+dx[i]][y+dy[i]] = d[x][y] + 1;
q[tt++] = {x+dx[i], y+dy[i]};
}
}
}

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", &maze[i][j]);
}
}
printf("%d", bfs());
return 0;
}
数据结构 空间 特点
DFS O(h) 不具有最短性
BFS 队列 $O(2^h)$ 最短路

拓扑排序

有向无环图的拓扑排序通过实时计算每个点的出度入度来实现

  • 出度:该节点直接连接到其他点的路径数
  • 入度:其他点直接到该点的路径数

    有向无环图必然存在入度为0的点

思路:

  1. 找出所有入度为0的点,加入队列
  2. 每从队列取出一个点a,便打印该点,标记为已经遍历,并遍历它的所有出边(a -> b),将b的入度减一;若b的入度变为0,则将b节点加入队列
  3. 重复步骤2,至队列为空
    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
    #include <queue>
    #include <cstring>
    #include <cstdio>

    using namespace std;

    const int N = 100010;

    int n, m;
    int h[N], e[N], ne[N], d[N],idx = 0;

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

    int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof(h));
    memset(d, 0, sizeof(d));
    while (m--) {
    int a, b;
    scanf("%d%d", &a, &b);
    add(a, b);
    d[b]++;
    }

    // find in-degree
    queue<int> q;
    for (int i = 1; i <= n; i++) if (d[i] == 0) q.push(i);

    // topological sort
    while (!q.empty()) {
    int u = q.front();
    q.pop();
    printf("%d ", u);
    for (int i = h[u]; ~i; i = ne[i]) {
    int j = e[i];
    d[j]--;
    if (d[j] == 0) q.push(j);
    }
    }
    return 0;
    }
    例题:MarsCode, 94.数据表是否能顺利产出
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
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <queue>
#include <unordered_set>

using namespace std;

const int N = 265;
int h[26], e[N], ne[N], idx, in[26];

inline int getIndex(string x) {
    return x[0] - 'A';
}

inline void reset() {
    memset(h, -1, sizeof h);
    memset(in, 0, 26);
    idx = 0;
}

inline void add(int u, int v) {
    e[idx] = v;
    ne[idx] = h[u];
    h[u] = idx++;
}

bool solution(const std::vector<std::vector<std::string>>& relations) {
    reset();
    queue<int> q;
    unordered_set<int> table;

    for (auto& arr : relations) {
        int h = getIndex(arr[0]);
        for (int i = 1; i < arr.size(); i++) {
            int x = getIndex(arr[i]);
            add(h, x);
            in[x]++;
            table.insert(x);
        }
    }

    for (int i = 0; i < 26; i++) if (in[i] == 0) q.push(i);

    while (!q.empty()) {
        int top = q.front(); q.pop();
        for (int i = h[top]; ~i; i = ne[i]) {
            if (--in[e[i]] == 0) {
                q.push(e[i]);
                table.erase(e[i]);
            }
        }
    }

    return table.size() == 0;
}

最短路问题

单源最短路

边权为正

  • 朴素Dijkstra算法(O(n^2)),适用于稠密图
  • 堆优化版Dijkstra算法(O(mlogn)),适用于稀疏图
朴素Dijkstra

步骤:

  1. 找到当前距离源点最近的点N
  2. 标记访问
  3. 更新以N为中介的源点到其他点的最短距离
    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
    const int N = 1000;

    int n, m;
    int g[N][N];
    int dis[N];
    int vis[N];

    int dijkstra() {
    // init
    memset(dis, 0x3f, sizeof(dis));
    dis[1] = 0;

    for (int i = 0; i < n; i++) {
    // find shortest path
    int t = -1;
    for (int j = 1; j <= n; j++)
    if (!vis[j] && (t == -1 || dis[j] < dis[t])) t = j;

    // mark visited
    vis[t] = 1;

    // update distance
    for (int k = 1; k <= n; k++)
    if (g[t][k] != 0x3f3f3f3f)
    dis[k] = min(dis[k], dis[t] + g[t][k]);

    }

    return dis[n];
    }
堆优化Dijkstra
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
using namespace std;

const int N = 100010;

typedef pair<int, int> pii;

int n, m;
int h[N], e[N], ne[N], w[N], idx = 0;
int dis[N];
int vis[N];

int dijkstra() {
priority_queue<pii> q;
q.push({0, 1});
while (!q.empty()) {
pii u = q.top();
q.pop();
if (vis[u.second]) continue;
vis[u.second] = 1;
dis[u.second] = u.first;

// 更新距离/ 松弛操作
for (int i = h[u.second]; i != -1; i = ne[i]) {
if (dis[e[i]] > u.first + w[i]) {
q.push({u.first + w[i], e[i]});
}
}
}
return dis[n];
}

复杂度解释:

  1. 优先队列操作:图中每个边最多被添加或删除一次,故为$O(mlogn)$
  2. 遍历边操作:在遍历每条边时,可能会更新邻居节点的距离,并将其插入优先队列,总时间复杂度是 $O(mlogn)$
    故总复杂度应该是$O(mlogn)$

边权可为负

边权可为负的图中,若存在负权回路,则与源点间存在经过负环的路径的节点不存在最短路

  • Bellman-Ford(O(nm))
  • SPFA(O(m), 最坏O(nm))
Bellman-Ford
  • 可以找出负环(鸽巢原理)
  • 结果满足三角不等式$dist[b] \le dist[a] + w$
  • 图中允许存在负环
  • 可以解决限制经过边数的最短路问题
    算法思路:循环n次,每次遍历所有边,并更新每个点到源点的距离
    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
    const int N = 1e5 + 5;

    // 存边即可
    struct Edge {
    int a, b, w;
    } E[N];

    int dis[N];
    int backup[N];
    int n, m, k;

    // 返回到第n点的最多经过k段的最短距离
    int bellman_ford() {
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    for (int i = 0; i < k; i++) {
    memcpy(backup, dis, sizeof dis);
    for (int j = 0; j < m; j++) {
    int a = E[j].a, b = E[j].b, w = E[j].w;
    if (backup[a] == 0x3f3f3f3f) continue;
    dis[b] = min(backup[b], backup[a] + w);
    }
    }
    return dis[n];
    }
SPFA
  • 可以判断负环是否存在 (鸽巢原理)
  • 效率比bellman_ford高
    思路:初始化队列,队列初始只有一个源点;之后不断迭代,每从队列头取出一个节点,就跟新它的所有出边,并将邻居节点加入队列,直至队列为空
    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
    const int N = 1e6+5;
    int h[N], e[N], ne[N], w[N], idx = 0;
    int dis[N], cnt[N], vis[N];
    queue<int> q;
    int n, m;

    int spfa() {
    memset(dis, 0x3f, sizeof(dis));
    memset(vis, 0, sizeof(vis));
    dis[1] = 0;
    q.push(1);
    vis[1] = true;
    while (!q.empty()) {
    int u = q.front(); q.pop();
    vis[u] = false;
    for (int i = h[u]; ~i; i = ne[i]) {
    if (dis[e[i]] > dis[u] + w[i]) {
    // 判断是否存在负环
    if ((cnt[e[i]] = cnt[u] + 1) >= n) {
    return 0x3f3f3f3f;
    }
    dis[e[i]] = dis[u] + w[i];
    if (!vis[e[i]]) {
    vis[e[i]] = true;
    q.push(e[i]);
    }
    }
    }
    }
    return dis[n];
    }

多源最短路 Floyd

  • Floyd算法($O(n^3)$)
  • 不能处理具有负环的图
    思路:依次选择一个点为中转点,计算每两个点之间的最短路
    归纳证明:$d[k, i, j] = min(d[k-1, i, j], d[k-1, i, k] + d[k-1, k, j])$,其中$d[k-1,i,j]$表示从顶点i到顶点j,且中间顶点仅取自${1,2,…,m-1}$的最短路长度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void floyd() {
    for (int k = 1; k <= n; k++) {
    for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
    if (d[i][k] == INF || d[k][j] == INF) continue;
    d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    }
    }
    }
    }

最小生成树

最小生成树问题一般针对无向图

最小生成树算法有两个:

  • Prim算法
  • Kruskal算法
    两者都是基于贪心的思想

Prim

与Dijkstra算法非常想,也有朴素版($O(n^2)$)和堆优化版($O(mlogn)$),分别适用于稠密图和稀疏图。这里仅介绍朴素版

思路:

  1. 初始化生成树集合,所有点到该集合的距离设为无穷
  2. 找到集合外随便一点,加入集合,并以该点更新其他店到集合的距离
  3. 进行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
const int N = 105, INF = 0x3f3f3f3f;
int n, m;
int g[N][N], dist[N], st[N];

int prim() {
int res = 0;

memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);

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 INF;
// 先累加再更新距离,避免自环
if (i) res += dist[t];

for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
st[t] = 1;
}
return res;
}

Kruskal

复杂度为$O(mlogm)$,适用于稀疏图,与Prim堆优化版算法更推荐Kruskal算法。

同$O(mlogn)$, 因为$m \le n^2$

思路:

  1. 初始化生成树集合,为空
  2. 将所有边按权重从小到大排序 $O(mlogm)$
  3. 依次枚举每条边a-b:$O(m)$
    • if a-b不连通(指在集合中,可用并查集进行判断),则将边ab加入集合
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 <algorithm>

using namespace std;

const int N = 100005;

int n, m, cnt = 0;
int p[N];

struct Edge {
int u, v, w;
bool operator < (const Edge& e) const {
return w < e.w;
}
} e[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, c;
scanf("%d%d%d", &a, &b, &c);
e[i] = {a, b, c};
}

sort(e, e + m);

// init disjoint set
for (int i = 0; i < n; i++) p[i] = i;

int ans = 0;

for (int i = 0; i < m; i++) {
int u = e[i].u, v = e[i].v;
int a = find(u), b = find(v);
if (a != b) {
p[a] = b;
ans += e[i].w;
cnt++;
}
}
if (cnt < n-1) puts("impossible");
else printf("%d\n", ans);
return 0;
}

二分图

定义:一个图的所有顶点,可以把所有顶点分为两个顶点集合,每个集合中两两不直接相连。

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

二分图相关的算法有两个:

  • 染色法
  • 匈牙利算法

染色法

该算法可以判断一个图是否为二分图,复杂度为$O(n+m)$

思路: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
52
53
#include <cstring>
#include <cstdio>

using namespace std;

const int N = 1e5 + 5, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[N], ne[N], color[N], idx = 0;

// color: 0: uncolored, 1: red, 2: blue

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; i = ne[i]) {
int j = e[i];
if (!color[j]) {
if (!dfs(j, 3 - c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}

int main() {
bool flag = true;
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));

for (int i = 0; i < m; i++) {
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}

for (int i = 1; i <= n; i++) {
if (!color[i]) {
if (!dfs(i, 1)) {
flag = false;
}
}
}

if (flag) puts("YES");
else puts("NO");

return 0;
}

匈牙利算法

该算法可以解决二分图最大匹配问题,复杂度为$O(mn)$,实际运行时间远小于$O(mn)$

匹配是指一组边的集合,其中任意两条边都没有共同的顶点。

思路:

  1. 将二分图的点分为两个点集AB,保存A到B的路径信息
  2. 遍历A的每个点a,遍历连接a的点b
    • 如果b还没被匹配,使a匹配b,并记录match[b] = a
    • 如果b被匹配,则尝试使match[b]匹配其他节点(),如果成功则记录match[b] = a,否则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
#include <iostream>
#include <cstring>

using namespace std;

const int N = 1e5 + 5;

int n1, n2, m;
int h[N], e[N], ne[N], idx = 0;
int match[N];
bool st[N];

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

bool find(int u) {

for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (!st[j]) {
st[j] = true;
if (!match[j] || find(match[j])) {
match[j] = u;
return true;
}
}
}

return false;
}

int main() {
scanf("%d%d%d", &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++;
}

printf("%d\n", res);
return 0;
}