搜索与图论
DFS 深度优先搜索
数据结构
空间复杂度
特性
DFS
stack
$O(h)$
不具有“最短路”特性
BFS
queue
$O(2^h)$
具有“最短路”特性
DFS俗称“暴搜”,思考问题时最重要的是想清楚搜索的顺序 ,要确定用什么样的顺序来把问题的所有方案全部遍历一遍比较方便。
每个DFS都对应一个搜索树,当比较难思考时,就从搜索树的角度 来思考。通过画图可以帮助我们理解。
DFS是一种递归,不用将DFS和递归区分
DFS的两个重要概念是是回溯、剪枝
回溯是指从深层回退的过程,回溯时的关键是回溯时必须“恢复现场”,会到原有的状态。
算法思路比较奇怪的或对空间要求比较高的一般用DFS而不用BFS
深搜会一条路走到黑,往深层搜索。
深搜空间复杂度低的原因,是因为深搜只需要同时存储一条路径。
DFS+存储即记忆化搜索
本题的搜索顺序是对空着的位置从左到右一个一个填。第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 #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) printf ("%d " , path[i]); puts ("" ); } for (int i = 1 ; i <= n ; ++ i) if (!st[i]){ path[u] = i; st[i] = true ; dfs (u + 1 ); st[i] = false ; } } int main () { scanf ("%d" , &n); dfs (0 ); return 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 #include <iostream> using namespace std;const int N = 10 ;int n;int path[N];void dfs (int u, int state) { if (u == n){ for (int i = 0 ; i < n; ++ i) printf ("%d " , path[i]); puts ("" ); return ; } for (int i = 0 ; i < n; ++ i) if (!(state >> i & 1 )){ path[u] = i + 1 ; dfs (u +1 , state + ( 1 << i)); } } int main () { scanf ("%d" , &n); dfs (0 , 0 ); return 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 53 54 #include <iostream> #include <algorithm> using namespace std;const int N = 10 ;int n;bool row[N], col[N], dg[N * 2 ], udg[N * 2 ];char g[N][N];void dfs (int x, int y , int s) { if (s > n) return ; if ( y == n) y = 0 , x ++; if (x == n){ if (s == n){ for (int i = 0 ; i < n; ++ i) puts (g[i]); puts ("" ); } return ; } g[x][y] = '.' ; dfs (x, y + 1 , s); if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]){ row[x] = col[y] = dg[x + y] = udg[x - y + n] = true ; g[x][y] = 'Q' ; dfs (x, y + 1 , s + 1 ); g[x][y] = '.' ; row[x] = col[y] = dg[x + y] = udg[x - y + n] = false ; } } int main () { cin >> n; dfs (0 , 0 , 0 ); return 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 #include <iostream> #include <algorithm> using namespace std;const int N = 20 ;int n;char g[N][N];bool col[N], dg[N], udg[N];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 (!col[i] && !dg[u + i] && !udg[n - u + i]){ g[u][i] = 'Q' ; col[i] = dg[u + i] = udg[n - u + i] = true ; dfs (u + 1 ); col[i] = dg[u + i] = udg[n - u + i] = false ; 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是一层一层来搜的,当图的路径权值相同 时,BFS具有“最短路”特性 (边权相同时才能用BFS求解最短路问题)。此时涉及最小步数、最短距离、最少操作次数的问题基本上都可以用BFS
dp问题和最短路问题是互通的,dp问题可以看成一种特殊的最短路问题,是没有环存在的最短路问题。
记忆化搜索即深搜+存储。
最短路问题包括用最短路算法求解的题,BFS求解的题,dp求解的题。
BFS的基本框架:
1 2 3 4 5 queue <- 初始状态(入队列) while queue不空{ 取出队头 拓展队头 }
自己手写队列
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 #include <iostream> #include <algorithm> #include <cstring> using namespace std;typedef pair<int , int > PII;const int N = 110 ;int n, m;int g[N][N]; int d[N][N]; PII q[N * N]; int bfs () { int hh = 0 , tt = -1 ; memset (d, -1 , sizeof d); d[0 ][0 ] = 0 ; q[++ tt] = {0 , 0 }; int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; while (hh <= tt) { auto t = q[hh ++]; for (int i = 0 ; i < 4 ; ++ i) { int x = t.first + dx[i], y = t.second + dy[i]; if (g[x][y] == 0 && d[x][y] == -1 && x >= 0 && x < n && y >= 0 && y < m){ d[x][y] = d[t.first][t.second] + 1 ; q[++ tt] = {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; } int bfs () { int hh = 0 , tt = -1 ; memset (d, -1 , sizeof d); d[0 ][0 ] = 0 ; q[++ tt] = {0 , 0 }; int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; while (hh <= tt){ auto t = q[hh ++]; for (int i = 0 ; i < 4 ; ++ i) { int x = t.first + dx[i], y = t.second + dy[i]; if (g[x][y] == 0 && d[x][y] == -1 && x >= 0 && x < n && y >= 0 && y < m){ d[x][y] = d[t.first][t.second] + 1 ; q[++ tt] = {x, y}; } } } return d[n - 1 ][m - 1 ]; } int bfs () { int hh = 0 , tt = -1 ; memset (d, -1 , sizeof d); d[0 ][0 ] = 0 ; int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; while (hh <= tt){ auto t = q[hh ++]; for (int i = 0 ; i < 4 ; ++ i){ int x = t.first + dx[i], y = t.second + dy[i]; if (g[x][y] == 0 && d[x][y] == -1 && x >= 0 && x < n && y >= 0 && y < m){ d[x][y] = d.[t.first][t.second] + 1 ; q[++ tt] = {x, y}; } } } } int bfs () { int hh = 0 , tt = -1 ; memset (d, -1 , sizeof d); d[0 ][0 ] = 0 ; q[++ tt] = {0 , 0 }; int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; while (hh <= tt){ auto t = q[hh ++]; for (int i = 0 ; i < 4 ; ++ i){ int x = t.first + dx[i], y = t.second + dy[i]; if (g[x][y] == 0 && d[x][y] == -1 && x >= 0 && x < n && y >= 0 && y < m){ d[x][y] = d[t.first][t.second] + 1 ; q[++ tt] = {x, y}; } } } return d[n - 1 ][m - 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 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std;typedef pair<int , int > PII;const int N = 110 ;int n, m;int g[N][N]; int d[N][N]; queue<PII> q; int bfs () { memset (d, -1 , sizeof d); d[0 ][0 ] = 0 ; q.push (make_pair (0 , 0 )); int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; 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 (g[x][y] == 0 && d[x][y] == -1 && x >= 0 && x < n && y >= 0 && y < m){ d[x][y] = d[t.first][t.second] + 1 ; q.push (make_pair (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; }
本题是一个重点要掌握的外部搜索的的题。外部表示往往涉及状态表示和状态转移。
本题的两大难点:
状态表示
状态转移
本题状态表示比较复杂,每个状态都是一个3 X 3的小矩阵。一种比较简单的方式是使用字符串来表示状态,将二维矩阵转化为一维字符串(展成一行)。入队时使用queue queue,记录距离时使用哈希表unorrdered_map<string, int> dist。
在进行状态转移时,首先将字符串变回矩阵(可以是想象的),然后进行转移——枚举上下左右,最后将矩阵再变回字符串。
这里要用到一个常用技巧:二维坐标与一维坐标的转换。设矩阵大小为n X m,元素二维坐标为(x, y)(x为行号,y为列号),一维坐标是k,则:$$x = k / n, y = k % n$$
$$ k = x * n + y$$
注意这里要求一维序列和二维矩阵都要从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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include <iostream> #include <algorithm> #include <unordered_map> #include <queue> using namespace std;int bfs (string state) { queue<string> q; unordered_map<string, int > d; q.push (state); d[state] = 0 ; int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; string end = "12345678x" ; while (q.size ()) { auto t = q.front (); q.pop (); if (t == end) return d[t]; int distance = d[t]; 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[a * 3 + b], t[k]); if (!d.count (t)) { d[t] = distance + 1 ; q.push (t); } swap (t[a * 3 + b], t[k]); } } } return -1 ; } int main () { char s[2 ]; string state; for (int i = 0 ; i < 9 ; i ++ ) { cin >> s; state += *s; } cout << bfs (state) << endl; return 0 ; }
树和图的存储
树是一种特殊的图,是无环连通图,与图的存储方式相同。对于无向图中的边ab
,存储两条有向边a->b
, b->a
。因此我们可以只考虑有向图的存储。
有向图存储:
邻接矩阵:开个二维数组,g[a, b]
存储是否有边或边权,适合稠密图不能存储重边,空间复杂度为$O(N^2)$
邻接表:头结点数组 + n个单链表每个节点都对应一个单链表,单链表中存储当前结点直接指向的所有结点(有边),顺序可任意
1 2 3 4 5 6 7 8 9 10 11 12 13 int h[N], e[N], ne[N], idx;void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } idx = 0 ; memset (h, -1 , sizeof h);
树和图的深度优先遍历
注意树和图的深度优先和宽度优先搜索对于每个节点都只会搜索一层,需要标记。
时间复杂度$O(n+m)$, $n$ 表示点数,$m$ 表示边数
1 2 3 4 5 6 7 8 9 int dfs (int u) { st[u] = true ; for (int i = h[u]; i != -1 ; i = ne[i]){ int j = e[i]; if (!st[j]) dfs (j); } }
树的深度优先遍历的一大优点是能够可以算出每个子树的大小
本题的难点在于充分研究连通块,挖掘其性质和规律,并由此确定搜索的顺序。这种涉及复杂思路的问题显然一般用DFS。
经研究,我们发现删除某节点后,其父节点往上的部分会做成一个连通块,以其子节点为根节点的子树会各自形成一个连通块。而父节点往上的部分又等于总结点数减去以当前结点为根节点的子树的所有节点数,即等于以当前结点子节点为根节点的所有子树结点数加1。
由此我们只需要计算每个结点子节点为根节点的子树各自的结点数即可。
考虑到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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std;const int N = 100010 , M = N * 2 ; int n;int h[N], e[M], ne[M], idx; int ans = N; bool st[N]; void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } int dfs (int u) { st[u] = true ; int size = 0 , sum = 0 ; for (int i = h[u]; i != -1 ; i = ne[i]){ int j = e[i]; if (st[j]) continue ; int s = dfs (j); size = max (size, s); sum += s; } size = max (size, n - sum - 1 ); ans = min (ans, size); return sum + 1 ; } int main () { scanf ("%d" , &n); memset (h, -1 , sizeof h); for (int i = 0 ; i < n - 1 ; ++ i) { int a, b; scanf ("%d%d" , &a, &b); add (a, b), add (b, a); } dfs (1 ); printf ("%d\n" , ans); return 0 ; }
这道题目边的方向是不确定的,所以还是需要将两个方向的边都建出来。在输入时,由于我们并不能确定DFS时搜索的方向,因此两个方向都需要创建出来。否则如果我们从1号点开始搜,但是建立边的时候,选择的方向是 2 -> 1,那么在DFS时就搜索不到2号点了。
在遍历一棵树的时候,即使是无向边,也可以将st[]数组去掉,只需要在dfs时传入父节点这个参数就可以了,这样就可以避免重新搜回父节点。
只要题目指明了父子关系,就可以省略st[]数组。无向树当根节点确定之后,每条边就有了方向,另一个方向就被忽略掉了。如果出现子节点循环到了父节点,会直接contine,不会再次访问到父节点从而出现死循环的。
树和图的广度优先遍历
注意树和图的深度优先和宽度优先搜索对于每个节点都只会搜索一层,需要标记。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 queue<int > q; st[1 ] = true ; 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 ; 更新信息, 如d[x] = d[t] + 1 q.push (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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std;const int N = 100010 ;int n, m;int h[N], e[N], ne[N], idx;int d[N];int q[N];void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void bfs () { int hh = 0 , tt = -1 ; memset (d, -1 , sizeof d); d[1 ] = 0 ; q[++ tt] = 1 ; while (hh <= tt){ int t = q[hh ++]; int j; for (int i = h[t]; i != -1 ; i = ne[i]){ j = e[i]; if (d[j] == -1 ){ d[j] = d[t] + 1 ; if (j == n) break ; q[++ tt] = j; } } if (j == n) break ; } printf ("%d\n" , d[n]); } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); for (int i = 0 ; i < m; ++ i){ int a, b; scanf ("%d%d" ,&a, &b); add (a, b); } bfs (); return 0 ; }
拓扑序列
拓扑序列是针对有向无环 图而言的。
对一个有向无环图 (Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边$<u,v>∈E(G)$,则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序 ,这个操作称之为拓扑排序。
由AOV网构造出拓扑序列的实际意义是:如果按照拓扑序列中的顶点次序,在开始每一项活动时,能够保证它的所有前驱活动都已完成 ,从而使整个工程顺序进行,不会出现冲突的情况。
一个有向无环图至少存在一个入度为0的结点(指向该节点的边数为0)。可简单用反证法和抽屉原理证明。
因而有向无环图一定存在拓扑序列 ,称为拓扑图。不是有向无环图的一定不存在拓扑序列 ,因而拓扑排序也用来判断一个图是否存在环 。
算法
由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。
先拿出所有入度为0的点入队
在原图中将它们删除此顶点及所有出边。改变子节点的度数。循环结束后,若输出的顶点数小于网中的顶点数,则输出“有回路”信息,否则输出的顶点序列就是一种拓扑序列。
拓扑序列不唯一。有时会要求输出字典序最小的方案,这时把queue改成priority_queue即可
所有边都是从前指向后的。
求有向图的拓扑序的图的宽度优先遍历的经典应用 ,使用BFS实现
时间复杂度 $O(n+m)$, n表示点数,m表示边数
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 bool topsort () { int hh = 0 , tt = -1 ; for (int i = 1 ; i <= n; ++ i) if (!d[i]) q[ ++ tt] = i; while (hh <= tt){ int t = q[hh ++]; for (int i = h[t]; i != -1 ; i = ne[i]){ int j = e[i]; d[j]--; if (d[j] == 0 ) q[ ++ tt] = j; } } return tt == 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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 100010 ;int n, m;int h[N], e[N], ne[N], idx;int d[N]; int q[N];void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } bool topsort () { int hh = 0 , tt = -1 ; for (int i = 1 ; i <= n; ++ i) if (!d[i]) q[ ++ tt] = i; while (hh <= tt){ int t = q[hh ++]; for (int i = h[t]; i != -1 ; i = ne[i]){ int j = e[i]; d[j]--; if (d[j] == 0 ) q[ ++ tt] = j; } } return tt == n - 1 ; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); for (int i = 0 ; i < m; ++ i){ int a, b; scanf ("%d%d" , &a, &b); add (a, b); ++ d[b]; } if (topsort ()){ for (int i = 0 ; i < n; ++ i) printf ("%d " , q[i]); puts ("" ); } else puts ("-1" ); return 0 ; }
最短路问题
常见的最短路问题可分为两大类:
单源最短路问题求从一个点到其他所有点的最短距离。可以分为两类
所有边的权重都是正数常用算法:
朴素Dijkstra算法:$O(n^2)$,时间复杂度与边数无关,适用于稠密图。基于贪心
堆优化版的Dijkstra算法:$O(mlogn)$,适用于稀疏图(如n和m相同数量级时,n
表示点数,m
表示边数)
存在权重为负的边常用算法:
Bellman-Ford算法: $O(nm)$,基于离散数学知识
SPFA算法:平均复杂度为$O(m)$,最坏复杂度为$O(nm)$
SPFA算法是对Bellman-Ford算法的优化,一般效率更高,但有些情况下无法使用,如对最短路经过的边数进行限制m <= k
等时,只能用Bellman-Ford。
多源汇最短路源指源点,即起点。汇指汇点,即终点。多源汇即多个起点和终点。多源汇最短路问题会有多个询问,每次起点和终点不同,求最短距离。常用算法:
Floyd算法:时间复杂度为$O(n^3)$,基于动态规划
最短路问题的侧重点和难点在于建图 ,如何定义点和边,把原问题抽象成最短路问题 ,套用什么模板来做。
朴素Dijkstra算法
稠密图用邻接矩阵来存
时间复杂是 $O(n^2+m)$, $n$表示点数,$m$表示边数
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 int g[N][N]; int dist[N]; bool st[N]; int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 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; for (int j = 1 ; j <= n; ++ j) dist[j] = min (dist[j], dist[t] + g[t][j]); st[t] = true ; } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[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 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 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 510 ;int n, m;int g[N][N]; int dist[N]; bool st[N]; int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 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; for (int j = 1 ; j <= n; ++ j) dist[j] = min (dist[j], dist[t] + g[t][j]); st[t] = true ; } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; } int main () { memset (g, 0x3f , sizeof g); scanf ("%d%d" , &n, &m); for (int i = 0 ; i < m ;++ i){ int a, b, w; scanf ("%d%d%d" , &a, &b, &w); g[a][b] = min (g[a][b], w); } printf ("%d\n" , dijkstra ()); return 0 ; }
Python
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 def main (): n, m = map (int , input ().split()) g = [[float ('inf' )] * (n + 1 ) for _ in range (n+1 )] def dijkstra (): d = [float ('inf' )] * (n + 1 ) d[1 ] = 0 st = set () for _ in range (n): t = -1 for i in range (1 , n + 1 ): if i not in st and (t == -1 or d[i] < d[t]): t = i st.add(t) for i in range (1 , n + 1 ): d[i] = min (d[i], d[t] + g[t][i]) return d[-1 ] if d[-1 ] != float ('inf' ) else -1 for _ in range (m): a, b, w = map (int , input ().split()) g[a][b] = min (g[a][b], w) print (dijkstra()) if __name__ == "__main__" : main()
堆优化版的Dijkstra算法
时间复杂度 $O(mlogn)$, $n$表示点数,$m$表示边数。适用于稀疏图,使用邻接表存储。
朴素版Dijkstra需要遍历所有点来找剩余点中到起点距离最小的点,这一步可以用堆来优化。
注意直接使用STL优先队列时,优先队列中不支持修改/删除任意元素,因此每次直接插入新的数而不进行删除,堆中会存在冗余,堆中可能存在个元素,导致复杂度变为$ O(mlogm) $ 。且需要判重。手写堆则不会出现这种状况(可以删除任意节点),保证堆中至多存在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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <queue> typedef pair<int , int > PII;int n; int h[N], w[N], e[N], ne[N], idx; int dist[N]; bool st[N]; int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push ({0 , 1 }); while (heap.size ()){ auto t = heap.top (); heap.pop (); int ver = t.second, distance = t.first; if (st[ver]) continue ; st[ver] = true ; for (int i = h[ver]; i != -1 ; i = ne[i]){ int j = e[i]; if (dist[j] > distance + w[i]){ dist[j] = distance + w[i]; heap.push ({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[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 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <iostream> #include <algorithm> #include <cstring> #include <queue> #include <vector> using namespace std;const int N = 150010 ;typedef pair<int , int > PII;int n; 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 dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push ({0 , 1 }); while (heap.size ()){ auto t = heap.top (); heap.pop (); int ver = t.second, distance = t.first; if (st[ver]) continue ; st[ver] = true ; for (int i = h[ver]; i != -1 ; i = ne[i]){ int j = e[i]; if (dist[j] > distance + w[i]){ dist[j] = distance + w[i]; heap.push ({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); for (int i = 0 ; i < m; ++ i){ int a, b, c; scanf ("%d%d%d" , &a, & b, &c); add (a, b, c); } printf ("%d\n" , dijkstra ()); return 0 ; }
Python
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 import heapqdef main (): n, m = map (int , input ().split()) head = [-1 ] * (n+1 ) e = [0 ] * m ne = [0 ] * m w = [0 ] * m idx = 0 st = set () def add (a, b, c ): nonlocal idx e[idx] = b w[idx] = c ne[idx] = head[a] head[a] = idx idx += 1 def dijksta (): d = [float ('inf' ) for _ in range (n+1 )] d[1 ] = 0 heap = [] heapq.heappush(heap, (0 , 1 )) while heap: dist, node = heapq.heappop(heap) if node in st: continue st.add(node) i = head[node] while i != -1 : j = e[i] if d[j] > dist + w[i]: d[j] = dist + w[i] heapq.heappush(heap, (d[j], j)) i = ne[i] if d[-1 ] == float ('inf' ): print (-1 ) else : print (d[-1 ]) for _ in range (m): a, b, c = map (int , input ().split()) add(a, b, c) dijksta() if __name__ == "__main__" : main()
Python
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 from collections import defaultdictfrom heapq import heappush, heappopif __name__ == '__main__' : n, m = map (int , input ().split()) graph = defaultdict(lambda : defaultdict(lambda : float ('inf' ))) for _ in range (m): a, b, weight = map (int , input ().split()) graph[a][b] = min (graph[a][b], weight) dist = [float ('inf' )] * (n+1 ) dist[1 ] = 0 heap = [] heappush(heap, (0 , 1 )) set_min = set () while heap: d, node = heappop(heap) if node in set_min: continue set_min.add(node) for adj_v in graph[node]: if dist[adj_v] > d + graph[node][adj_v]: dist[adj_v] = d + graph[node][adj_v] heappush(heap, (dist[adj_v], adj_v)) res = dist[-1 ] if dist[-1 ] != float ('inf' ) else -1 print (res)
Bellman-Ford算法
用于求带负权边的最短路问题。使用两重循环进行求解,不用显式存储整个图,只用把每个边及两端点存储即可。
求最短路问题的图中不能有负权回路,当存在负权回路时,会出现死循环,不存在最短路径(最短路径负无穷)。
Bellman-Ford算法可用来判断是否存在负权回路,如果当迭代n次后还有更新 ,由抽屉原理,说明存在负权回路 。第k次迭代含义是从起点经过不超过k条边走到每个点的最短距离,n次迭代还有更新说明有路径存在n条边n+1个节点,说明有负权回路。
虽然Bellman-Ford算法可以判断负权回路,但复杂度比较高,因此一般不用此算法而是用SPFA算法判断负环。
对于有边数限制的最短路问题,只能用Bellman-Ford算法,而不能用SPFA算法。
时间复杂度 $O(nm)$, n表示点数,m表示边数
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 int n, m; int dist[N]; struct Edge { int a, b, w; }edges[M]; int bellman_ford () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < n; ++ i ){ for (int j = 0 ; j < m; ++ j ){ int a = edges[j].a, b = edges[j].b, w = edges[j].w; if (dist[b] > dist[a] + w) dist[b] = dist[a] + w; } } if (dist[n] > 0x3f3f3f3f / 2 ) return -1 ; return dist[n]; }
本题因为有步数限制,所以即使有负权回路也会存在最短路径。所以可以使用Bellman-Ford算法,但有负环一定不能用SFPA算法
串联问题
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 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 510 , M = 10010 ;int n, m, k;int dist[N], backup[N]; struct Edge { int a, b, w; }edges[M]; void 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); } } } int main () { scanf ("%d%d%d" , &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}; } bellman_ford (); if (dist[n] > 0x3f3f3f3f / 2 ) puts ("impossible" ); else printf ("%d\n" , dist[n]); return 0 ; }
Python
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 def main (): n, m, k = map (int , input ().split()) edge = [] dist = [float ('inf' )] * (n + 1 ) def Ballman_Ford (): dist[1 ] = 0 for _ in range (k): back_up = dist.copy() for j in range (m): a, b, w = edge[j] dist[b] = min (dist[b], back_up[a] + w) if back_up == dist: break if dist[n] == float ('inf' ): return "impossible" else : return dist[n] for i in range (m): a, b, w = map (int , input ().split()) edge.append([a, b, w]) print (Ballman_Ford()) if __name__ == '__main__' : main()
SPFA 算法
时间复杂度 平均情况下 $O(m)$,最坏情况下 $O(nm)$,$n$表示点数,$m$表示边数
SPFA算法使用队列优化Bellman-Ford算法。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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 int n; int h[N], w[N], e[N], ne[N], idx; int dist[N]; bool st[N]; int spfa () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; queue<int > q; q.push (1 ); st[1 ] = true ; while (q.size ()){ auto 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 ; } } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[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 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std;const int N = 100010 ;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 ()){ auto 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); for (int i = 0 ; i < m;++ i){ int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add (a, b, c); } int t = spfa (); if (t == 0x3f3f3f3f ) puts ("impossible" ); else printf ("%d\n" , t); return 0 ; }
Python
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 from collections import dequedef main (): n, m = map (int , input ().split()) dist = [float ('inf' )] * (n + 1 ) head = [-1 ] * (n + 1 ) e = [0 ] * m ne = [0 ] * m w = [0 ] * m idx = 0 st = [False ] * (n + 1 ) def add (a, b, c ): nonlocal idx e[idx] = b w[idx] = c ne[idx] = head[a] head[a] = idx idx += 1 def spfa (): q= deque() dist[1 ] = 0 q.append(1 ) st[1 ] = True while q: t = q.popleft() st[t] = False i = head[t] while i != -1 : j = e[i] if dist[j] > dist[t] + w[i]: dist[j] = dist[t] + w[i] if not st[j]: st[j] = True q.append(j) i = ne[i] if dist[n] == float ('inf' ): return "impossible" else : return dist[n] for _ in range (m): a, b, c = map (int , input ().split()) add(a, b, c) print (spfa()) if __name__ == "__main__" : main()
SPFA判断图中是否存在负环
时间复杂度是$O(nm)$, n表示点数,m表示边数
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 int n, m;int h[N], w[M], e[M], ne[M], idx;int dist[N], cnt[N];bool st[N];bool spfa () { queue<int > q; for (int i = 1 ; i <= n; ++ i){ q.push (i); st[i] = true ; } while (q.size ()){ auto 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 ; }
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 62 63 64 65 66 67 68 69 70 71 72 #include <iostream> #include <algorithm> #include <queue> #include <cstring> using namespace std;const int N = 2010 , M = 10010 ;int n, m;int h[N], w[M], e[M], ne[M], 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 ++; } bool spfa () { queue<int > q; for (int i = 1 ; i <= n; ++ i){ q.push (i); st[i] = true ; } while (q.size ()){ auto 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); for (int i = 0 ; i < m; ++ i){ int a, b, c; scanf ("%d%d%d" , &a, &b,&c); add (a, b, c); } if (spfa ()) puts ("Yes" ); else puts ("No" ); return 0 ; }
Python
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 from collections import dequedef main (): n, m = map (int , input ().split()) d = [0 ] * (n + 1 ) head = [-1 ] * (n + 1 ) e = [0 ] * m ne = [0 ] * m w = [0 ] * m st = [False ] * (n + 1 ) cnt = [0 ] * ( n + 1 ) idx = 0 def add (a, b, c ): nonlocal idx e[idx] = b ne[idx] = head[a] w[idx] = c head[a] = idx idx += 1 def spfa (): q = deque() for i in range (1 , n + 1 ): q.append(i) st[i] = True while q: t = q.popleft() st[t]= False i = head[t] while i != -1 : j = e[i] if d[j] > d[t] + w[i]: d[j] = d[t] + w[i] cnt[j] = cnt[t] + 1 if cnt[j] >= n: return True if not st[j]: st[j] = True q.append(j) i = ne[i] return False for _ in range (m): a, b, c = map (int , input ().split()) add(a, b, c) print ("Yes" if spfa() else "No" ) if __name__ == "__main__" : main()
Floyd算法
时间复杂度是 $O(n^3)$, n表示点数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 初始化: 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; 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]); }
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 <iostream> #include <algorithm> using namespace std;const int N = 210 , M = 40010 , INF = 0x3f3f3f3f ;int n, m, k;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 () { scanf ("%d%d%d" , &n, &m, &k); 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; for (int i = 0 ; i < m; ++ i){ int a, b, w; scanf ("%d%d%d" , &a, &b, &w); d[a][b] = min (d[a][b], w); } floyd (); while (k--){ int a, b; scanf ("%d%d" , &a, & b); int t = d[a][b]; if (t > INF / 2 ) puts ("impossible" ); else printf ("%d\n" , t); } return 0 ; }
重边只保留最短的边,自环则直接删掉
Python
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 def main (): n, m, k = map (int , input ().split()) g = [[float ('inf' )] * (n + 1 ) for _ in range (n + 1 )] def Floyd (): for i in range (1 , n + 1 ): g[i][i] = 0 for k in range (1 , n + 1 ): for i in range (1 , n + 1 ): for j in range (1 , n + 1 ): g[i][j] = min (g[i][j], g[i][k] + g[k][j]) for _ in range (m): a, b, c = map (int , input ().split()) g[a][b] = min (g[a][b], c) Floyd() for _ in range (k): a, b = map (int , input ().split()) if g[a][b] == float ('inf' ): print ("impossible" ) else : print (g[a][b]) if __name__ == "__main__" : main()
最小生成树
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边(n-1条)。
最小生成树一般考察无向图,有向图一般不会用到,存边的时候需要存两次
难点也在于建图
常用算法
朴素版prim算法
时间复杂度是 $O(n^2+m)$, n表示点数,m表示边数
算法流程:
输入:一个加权连通图,其中顶点集合为V,边集合为E;
初始化:$V_{new} = {x}$,其中x为集合V中的任一节点(起始点),$E_{new} = {}$,为空;
重复下列操作,直到$V_{new} = V$:
在集合E中选取权值最小的边$<u, v>$,其中u为集合Vnew中的元素,而v不在$V_{new}$集合当中,并且$v∈V$(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
将v加入集合$V_{new}$中,将$<u, v>$边加入集合$E_new$中;
输出:使用集合$V_{new}$和$E_{new}$来描述所得到的最小生成树。
Prim算法和Dijkstra算法非常相似,区别在于Dijkstra算法每次找到不在集合内的距离起点最近的点,并用其更新其他点到起点的距离。而Prim算法每次找到不在集合内的距离起点最近的点,并用其更新其他点到集合的距离。Dijkstra算法起始时默认起点已经在集合中,只需迭代n-1次。Prim算法初始时集合为空,所有点到集合距离初始化为正无穷,要迭代n次,且起点可以随机取。
注意Prim算法可以用于有负权边的图,而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 31 32 33 34 35 36 37 38 39 int n; int g[N][N]; int dist[N]; bool st[N]; 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 INF; if (i) res += dist[t]; st[t] = true ; for (int j = 1 ; j <= n; j ++ ) dist[j] = min (dist[j], g[t][j]); } return res; }
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 62 63 64 65 66 67 68 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 510 ;const int INF = 0x3f3f3f3f ;int n, m; int g[N][N]; int dist[N]; bool st[N]; 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 INF; if (i) res += dist[t]; st[t] = true ; 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); for (int i = 0 ; i < m; ++ i){ int a, b, w; scanf ("%d%d%d" , &a, &b, &w); g[a][b] = g[b][a] = min (g[a][b], w); } int t = prim (); if ( t == INF) puts ("impossible" ); else printf ("%d\n" , t); return 0 ; }
Python
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 def main (): n, m = map (int , input ().split()) g = [[float ('inf' )] * (n + 1 ) for _ in range (n + 1 )] st = [False ] * (n + 1 ) dist = [float ('inf' )] * (n + 1 ) def Prim (): res = 0 dist[1 ] = 0 for i in range (n): t = -1 for j in range (1 , n + 1 ): if not st[j] and (t == -1 or dist[t] > dist[j]): t = j if dist[t] == float ('inf' ): return print ("impossible" ) res += dist[t] st[t] = True for i in range (1 , n + 1 ): dist[i] = min (dist[i], g[t][i]) return print (res) for _ in range (m): a, b, c = map (int , input ().split()) g[b][a] = g[a][b] = min (g[a][b], c) Prim() if __name__ == "__main__" : main()
堆优化版的Prim算法
把求最小值这一步从遍历变成由堆来优化
克鲁斯卡尔算法(Kruskal):
从小到大排序,时间主要花在排序上
时间复杂度是$O(mlogm)$, n表示点数,m表示边数
基本思想是:假设连通网G=(V,E),令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),概述图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点分别在T中不同的连通分量上,则将此边加入到T中;否则,舍去此边而选择下一条代价最小的边。依此类推,直至T中所有顶点构成一个连通分量为止。
步骤:
将所有边按权重从小到大排序($O(mlogm))
枚举每条边<a, b>,如果a,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 int n, m; int p[N]; struct Edge { int a, b, w; bool operator < (const Edge &W)const { return w < W.w; } }edges[M]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } int kruskal () { sort (edges, edges + m); for (int i = 1 ; i <= n; 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 ) return INF; return res; }
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 62 63 64 65 66 67 68 69 70 71 72 73 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 100010 , M = 200010 ;const int INF = 0x3f3f3f3f ;int n, m; int p[N]; struct Edge { int a, b, w; bool operator < (const Edge &W)const { return w < W.w; } }edges[M]; int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } int kruskal () { sort (edges, edges + m); for (int i = 1 ; i <= n; ++ 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 ) return INF; return res; } 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}; } int t = kruskal (); if (t == INF) puts ("impossible" ); else printf ("%d\n" , t); return 0 ; }
Python
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 def main (): n, m = map (int , input ().split()) edge = [] p = [0 ] * (n + 1 ) def find (x ): if x != p[x]: p[x] = find(p[x]) return p[x] def Kruskal (): nonlocal edge res = 0 cnt = 0 edge = sorted (edge, key=lambda s: s[2 ]) for i in range (m): a, b, c = edge[i] a = find(a) b = find(b) if a != b: res += c p[b] = a cnt += 1 if cnt != n - 1 : return "impossible" else : return res for _ in range (m): a, b, c = map (int , input ().split()) edge.append([a, b, c]) for i in range (1 , n + 1 ): p[i] = i print (Kruskal()) if __name__ == "__main__" : main()
二分图
常用算法:
染色法判别二分图
时间复杂度是 $O(n+m)$, n表示点数,m表示边数
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 int n;int h[N], e[M], ne[M], idx;int color[N];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 ; } else if (color[j] == c) return false ; } return true ; } bool check () { for (int i = 1 ; i <= n; i ++ ) if (!color[i]) if (!dfs (i, 1 )) { return false } return true ; }
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 62 63 64 65 66 67 68 69 70 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 100010 , M = 200010 ;int n, m;int h[N], e[M], ne[M], idx;int color[N];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 ; } else if (color[j] == c) return false ; } return true ; } bool check () { for (int i = 1 ; i <= n; i ++ ) if (!color[i]) if (!dfs (i, 1 )){ return false ; } return true ; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); for (int i = 0 ;i < m; ++ i){ int a, b; scanf ("%d%d" , &a, &b); add (a, b), add (b, a); } if (check ()) puts ("Yes" ); else puts ("No" ); return 0 ; }
匈牙利算法求二分图的最大匹配
时间复杂度是 $O(nm)$, n表示点数,m表示边数
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 int n1, n2;int h[N], e[M], ne[M], idx;int match[N];bool st[N];bool find (int x) { for (int i = h[x]; i != -1 ; i = ne[i]){ int j = e[i]; if (!st[j]){ st[j] = true ; if (match[j] == 0 || find (match[j])){ match[j] = x; return true ; } } } return false ; } int res = 0 ;for (int i = 1 ; i <= n1; i ++ ){ memset (st, false , sizeof st); if (find (i)) res ++ ; }
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 <algorithm> #include <cstring> using namespace std;const int N = 510 , M = 100010 ;int n1, n2, m;int h[N], e[M], ne[M], idx;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 x) { for (int i = h[x]; i != -1 ; i = ne[i]){ int j = e[i]; if (!st[j]){ st[j] = true ; if (match[j] == 0 || find (match[j])){ match[j] = x; return true ; } } } return false ; } int main () { scanf ("%d%d%d" , &n1, &n2, &m); memset (h, -1 , sizeof h); for (int i = 0 ; i < m; ++ i){ 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 ; }