20算法周记之《代码随想录》图论(四)
学习小结
本周跟随代码随想录继续图论相关学习,主要聚焦于从一个点到另一个点的最小距离问题,学习了 dijkstra,bellman_ford,floyd,A* 算法。
相比于此前的算法设计课,更深刻地了解了各个算法的优势和缺点,比如 dijkstra 不能应对 负权 的边,bellman_ford可能会陷入负权回路的陷阱中,A* 不擅长多源距离等。
至此,代码随想录的一刷算是完成力,嘛,明天起就得开始狠狠二刷咯。
图论
dijkstra(朴素版)
题意:给一些节点和节点之间部分路径(有向),求起点到终点的最短路径是多少。
方法:其实bfs/dfs应该也行吧,dijkstra 是求一个点到其余点的最短路径距离。
本质和prim差不多,但prim只要关注点之间的距离就行,而dijkstra要考虑的可就多了,每次更新最短距离不仅要看新纳入点与当前体系的距离,还要看起点到体系边缘的距离。但是该方法不能对有负数权值的图使用,比如下面这个
从节点一出发,如果按照算法,走的路线是 1-3-4,最后看到1-2;但上帝视角可以看到,应该走 1-2-3-4。路线错误的原因是,该算法处理不了多段路程权值小于一段路程的情况。参考代码随想录思路的解法
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 <vector>
#include <climits>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> grid(n+1, vector<int>(n+1, INT_MAX));
for (int i = 0; i < m; i++) {
int s, e, v;
cin >> s >> e >> v;
grid[s][e] = v;
}
vector<bool>visited(n+1, false);
vector<int>min_dist(n+1, INT_MAX);// 到各点最短距离
min_dist[1] = 0;// 起点到自己的距离为零
for (int i = 1; i < n; i++) {
// 寻找与当前节点最近的的节点
int cur = 1; // 默认第一个,解决初始化的问题
int closest_way = INT_MAX;
for (int i = 1; i <= n; i++) {
if (!visited[i] && min_dist[i] < closest_way) {
cur = i;
closest_way = min_dist[i];
}
}
// 加入访问列表中
visited[cur] = true;
// 更新最短距离
for (int i = 1; i <= n; i++) {
if (!visited[i] && grid[cur][i] != INT_MAX && min_dist[cur] + grid[cur][i] < min_dist[i]) {
min_dist[i] = min_dist[cur] + grid[cur][i];
}
}
}
if (min_dist[n] == INT_MAX) {
cout << -1 << endl;
}
else {
cout << min_dist[n] << endl;
}
return 0;
}dijkstra(堆优化版)
题意:与 [[dijkstra(朴素版)]]相同,都是求起点到终点最短距离
方法:此处重点阐述该方法与普通迪杰斯特拉的不同,在[[dijkstra(朴素版)]]中,每次都要遍历所有的边来选取与当前体系距离最短的点,堆优化则是引入了小顶堆来解决这个问题,小顶堆保持堆顶是所有边中最小的那个,每次只需要取堆顶即可。C++中通过优先级队列来实现小顶堆。
参考代码随想录思路的解法:
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#include <iostream>
#include <vector>
#include <climits>
#include <list>
#include <queue>
using namespace std;
class my_comparison {
public:
bool operator()(const pair<int, int> &lhs, const pair<int, int> &rhs) {
return lhs.second > rhs.second; // 队尾到队首降序,取出来就是升序
}
};
priority_queue<pair<int, int>, vector<pair<int,int>>, my_comparison> pq;
struct Edge {
int to;// 连接点号
int val; // 权值
Edge(int a, int b): to(a), val(b) {}
};
int main() {
int n, m;
cin >> n >> m;
vector<list<Edge>> grid(n+1);
// 初始化
for (int i = 0; i < m; i++) {
int from, to, val;
cin >> from >> to >> val;
grid[from].push_back(Edge(to, val));
}
vector<bool> visited(n+1, false);
vector<int> min_dist(n+1, INT_MAX);
// 初始化优先级队列
int start = 1;
int end = n;
pq.push(make_pair(start, 0));
min_dist[start] = 0;
// 开始算法
while(!pq.empty()) {
// 第一步,取到最近点,取优先级队列队首即可
pair<int, int> p = pq.top();
pq.pop();
// 去除已访问的点
if (visited[p.first]) continue;
// 标记取点成功
visited[p.first] = true;
// 开始遍历当前点所连接的其它点
for (Edge &e : grid[p.first]) {
if (!visited[e.to] && e.val + min_dist[p.first] < min_dist[e.to]) {
min_dist[e.to] = e.val + min_dist[p.first];
pq.push(make_pair(e.to, min_dist[e.to]));
}
}
}
if (min_dist[n] != INT_MAX) cout << min_dist[n] << endl;
else cout << "-1" << endl;
return 0;
}Bellman_ford算法应用
题意:本质是有向图中求起点到终点的最短路程,但是可能出现负权值的边
方法:由于有负权值的边,不能用迪杰斯特拉,这里就要用 Bellman_ford 算法,该算法本质是对每条边做 n-1 次的松弛,n是点的数量,松弛指的是根据其它点到该点的距离和权值取小值。与此前的最大不同在于,该算法专注于边,所以grid只要记录每条边的起点、终点和权值即可。详情可查看注释与代码。
但是,该算法不能解决负权回路的问题。
参考代码随想录思路的解法
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>
#include <vector>
#include <climits>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 初始化, 用于存储所有的边
vector<vector<int>> grid;
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
grid.push_back({s, t, v});
}
// 建立最小距离
// 对m条边各做n-1次松弛
vector<int> min_dist(n+1, INT_MAX);
min_dist[1] = 0;
// 之所以要n-1次,是因为1次松弛能获取起点到该点通过一条边的最小值
// 每多松弛一次就能多通过一个边
// n-1 是处理一长条的极端情况
for (int i = 1; i < n; i++) { // n-1次松弛
for (auto &e : grid) {
int from = e[0];
int to = e[1];
int val = e[2];
if (min_dist[from] == INT_MAX) continue; // 该情况说明起点尚未连通
if (min_dist[to] > min_dist[from] + val) { // 松弛就是更新小值
min_dist[to] = min_dist[from] + val;
}
}
}
if (min_dist[n] == INT_MAX) cout << "unconnected" << endl;
else cout << min_dist[n] << endl;
return 0;
}Bellman_ford队列优化算法(SPFA)应用
题意,与 [[Bellman_ford算法应用]]一样,都是在无负权回路的情况下,求起点到终点的最短路径长度。
方法,顾名思义,SPFA通过队列一定程度上减少了松弛的次数,在原先的 Bellman_ford算法中,每条边必须要经过 n-1 次松弛,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#include <iostream>
#include <vector>
#include <climits>
#include <list>
#include <queue>
using namespace std;
struct Edge {
int to; // 目标
int val; // 权值
Edge(int a, int b): to(a), val(b) {}
};
int main() {
// 初始化
int n, m;
cin >> n >> m;
vector<list<Edge>> grid(n+1);
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
grid[s].push_back(Edge(t,v));
}
vector<bool> is_in_queue(n+1, false);
queue<int> q;// 待松弛边
vector<int> min_dist(n+1, INT_MAX);
q.push(1);
is_in_queue[1] = true;
min_dist[1] = 0;
while(!q.empty()) {
int node = q.front();
q.pop();
for (const Edge &e : grid[node]) {
min_dist[e.to] = min(min_dist[e.to], min_dist[node]+e.val);
if (!is_in_queue[e.to]) { // 成功松弛就准备遍历目标点
q.push(e.to);
is_in_queue[e.to] = true;
}
}
is_in_queue[node] = false;
}
if (min_dist[n] == INT_MAX) cout << "unconnected" << endl;
else cout << min_dist[n] << endl;
return 0;
}Bellman_ford判断负权回路
题意,与 [[Bellman_ford算法应用]]类似,但是现在会出现负权回路。
方法,要解决负权回路,就要知道一点,在传统的Bellman_ford算法中,每条边最多 n-1 次松弛即可获取结果,此后再多的松弛也不会改变结果。所以,如果一条边松弛次数达到 n 次,说明存在负权回路。
而对于队列优化版的 Bellman_ford 算法,每个点最多进入队列 n-1 次,毕竟每次进队列,都是因为某条连着这个点的边被成功松弛,如果次数到达 n,也说明存在负权回路。
普通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
46
47
48
49
50
51
52#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 初始化, 用于存储所有的边
vector<vector<int>> grid;
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
grid.push_back({s, t, v});
}
// 建立最小距离
// 对m条边各做n-1次松弛
vector<int> min_dist(n+1, INT_MAX);
min_dist[1] = 0;
bool circle_flag = false;
// 之所以要n-1次,是因为1次松弛能获取起点到该点通过一条边的最小值
// 每多松弛一次就能多通过一个边
// n-1 是处理一长条的极端情况
for (int i = 1; i <= n; i++) { // n-1次松弛,最后一次用来判断负权回路
for (auto &e : grid) {
int from = e[0];
int to = e[1];
int val = e[2];
if (min_dist[from] == INT_MAX) continue; // 该情况说明起点尚未连通
if (min_dist[to] > min_dist[from] + val) { // 松弛就是更新小值
if (i < n) {
min_dist[to] = min_dist[from] + val;
}
else {
circle_flag = true;
break;
}
}
}
if (circle_flag) break;
}
if (circle_flag) cout << "circle" << endl;
else if (min_dist[n] == INT_MAX) cout << "unconnected" << endl;
else cout << min_dist[n] << endl;
return 0;
}SPFA判断负权回路
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 <vector>
#include <climits>
#include <list>
#include <queue>
using namespace std;
struct Edge {
int to; // 目标
int val; // 权值
Edge(int a, int b): to(a), val(b) {}
};
int main() {
// 初始化
int n, m;
cin >> n >> m;
vector<list<Edge>> grid(n+1);
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
grid[s].push_back(Edge(t,v));
}
vector<bool> is_in_queue(n+1, false);
queue<int> q;// 待松弛边
vector<int> min_dist(n+1, INT_MAX);
bool circle_flag = false;
vector<int> count_release(n+1, 0);
q.push(1);
is_in_queue[1] = true;
min_dist[1] = 0;
while(!q.empty()) {
int node = q.front();
q.pop();
for (const Edge &e : grid[node]) {
min_dist[e.to] = min(min_dist[e.to], min_dist[node]+e.val);
if (!is_in_queue[e.to]) {
q.push(e.to);
is_in_queue[e.to] = true;
count_release[e.to]++;
if(count_release[e.to] == n) {
circle_flag = true;
while(!q.empty()) q.pop();
break;
}
}
}
is_in_queue[node] = false;
}
if (circle_flag) cout << "circle" << endl;
else if (min_dist[n] == INT_MAX) cout << "unconnected" << endl;
else cout << min_dist[n] << endl;
return 0;
}Bellman_ford之单源有限最短路
题意:在 [[Bellman_ford判断负权回路]]的基础上,加上了最多经过 k 个节点的限制。
方法:两个重点
- 经过 k 个节点的本质是,对每条边最多k+1次松弛。因为每次松弛就能让起点向外多延申一边;
- 其实以往的 Bellman_ford 的实现中,对所有边遍历 n-1 次后,实际上很可能有边超过了 n-1 次松弛。比如 1 与 3 本没有直接连接,但由于对 1 与 2 的连接,使得 min_dist[3] 可以推出。换句话,就是用了本次遍历的结果推出本应下次遍历的结果。之所以之前的题目都没问题,是因为之前的题目中边松弛过多对结果没有影响。解决方法就是每次遍历时记录上一次的遍历结果,基于这个结果来松弛。
参考代码随想录思路的解法
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// 版本一
#include <iostream>
#include <vector>
#include <list>
#include <climits>
using namespace std;
int main() {
int src, dst,k ,p1, p2, val ,m , n;
cin >> n >> m;
vector<vector<int>> grid;
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2,权值为 val
grid.push_back({p1, p2, val});
}
cin >> src >> dst >> k;
vector<int> minDist(n + 1 , INT_MAX);
vector<int> minDist_copy(n+1);// 记录上一次遍历结果
minDist[src] = 0;
for (int i = 1; i <= k + 1; i++) { // 对所有边松弛 k + 1次
minDist_copy = minDist;
for (vector<int> &side : grid) {
int from = side[0];
int to = side[1];
int price = side[2];
if (minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price) minDist[to] = minDist_copy[from] + price;
}
}
// for (int i = 0; i <=n; i++) {
// cout << minDist[i] << " ";
// }
// cout << endl;
if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点
else cout << minDist[dst] << endl; // 到达终点最短路径
}Floyd算法应用
题目:给一个无向图,要求给出任意起点终点,能够输出存在的最短的路线的长度。
方法:这是多源最短路径求解,之前的 [[dijkstra(朴素版)]]和 [[Bellman_ford算法应用]]都是单源最短路径。Floyd算法不要求边的正负值,其中心思想是动态规划。建立三维数组,dp[i][j][k] val 的含义是,节点 i 到 j 经过 [1,k]的中间节点的最短值为 val。只要从0遍历到 k 层,就能获取所有点到其它点的最短路径。
但是要注意遍历方向,如果去数组按 dp[i][j][k],那么k的循环必须在最外面一层,而 i 和 j 的顺序无关系。因为初始化时在 dp[i][j][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#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(n+1, vector<int>(n+1, 10002)));
// dp[i][j][k] val 的含义是,节点 i 到 j 经过 [1,k]的中间节点的最短值为 val
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
dp[u][v][0] = w;
dp[v][u][0] = w; // 无向图
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j][k] = min(dp[i][j][k-1], dp[i][k][k-1] + dp[k][j][k-1]);
}
}
}
int q;
cin >> q;
for (int i = 0; i < q; i++) {
int s, e;
cin >> s >> e;
if (dp[s][e][n] == 10002) cout << -1 << endl;
else cout << dp[s][e][n] << endl;
}
return 0;
}A*算法应用
题意:给一个起点和终点,从起点只能按一定规则移动,本题走 日(象棋中马的走法),求最短步数。
方法:bfs能解决,但时间消耗比较大,这里就要引入 A* 算法。实际上,A*算法没有定式,本质是 bfs,迪杰斯特拉 等算法的优化,其灵魂在于启发函数,通过这个函数指导高效遍历。
拿本体来说,在 bfs 的基础上,引入优先级队列和启发函数,启发函数用欧拉距离(也可用其它距离表示)衡量目前的点与目标点距离,优先选择离目标更近的点遍历。具体动画可看:A* && BFS
不过,A* 也有缺点,那就是不能像迪杰斯特拉一样对多个点都有遍历距离。如此看来,A*更适合于有明确信息指向目的地且只要单源的情况。
此外,陆爻齐突发奇想,如果点到起点也用启发函数衡量距离会怎样,结果当然有误。其原因在于,对节点统一衡量为 5,相当于对节点有层数的划分。如果也换启发函数,那么算法会趋近于走两点之间连线的点,这不一定是最佳路线,因为有走法规则限制。比如(5,2)和(5,4),在这个错误方法下要可能会走4步才能到。
参考代码随想录思路的解法
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#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
int a1, a2, b1, b2;
int dr[8][2] = {1, 2, 2, 1, 2, -1, 1, -2, -1, -2, -2, -1, -2, 1, -1, 2};
int moves[1001][1001];
struct Knight {
int x, y;
int g, h, f; // f = g + h
Knight(int a, int b) : x(a), y(b) {}
Knight() {}
bool operator < (const Knight &o) const{ return o.f < this->f; }
};
priority_queue<Knight> que;
int dis2begin(Knight &k) {
return (k.x - a1) * (k.x - a1) + (k.y - a2) * (k.y - a2);
}
int dis2end(Knight &k) {
return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2);
}
void bfsastar(Knight &k) {
Knight cur, next;
que.push(k);
while(!que.empty()) {
cur = que.top(); que.pop();
if (cur.x == b1 && cur.y == b2) {
break;
}
for (int i = 0; i < 8; i++) {
next.x = cur.x + dr[i][0];
next.y = cur.y + dr[i][1];
if (next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) {
continue;
}
if (!moves[next.x][next.y]) {
moves[next.x][next.y] = moves[cur.x][cur.y] + 1;
// 计算f
//next.g = dis2begin(next);
next.g = cur.g + 5;
next.h = dis2end(next);
next.f = next.g + next.h;
que.push(next);
}
}
}
}
int main() {
int n;
cin >> n;
while (n--) {
memset(moves, 0, sizeof(moves));
cin >> a1 >> a2 >> b1 >> b2;
Knight k(a1, a2);
k.g = 0;
k.h = dis2end(k);
k.f = k.h;
bfsastar(k);
while(!que.empty()) que.pop();
cout << moves[b1][b2] << endl;
}
return 0;
}