求图最短路的几个方法
C++和Python3代码。
只是简单提供板子,之后会更新相关题目。
包括Dijkstra、Bellman-Ford、spfa、Floyd。
板子来自ACwing。
有错误欢迎评论私信指正
Dijkstra求最短路
查找每个节点的所连接的所有子节点中的最短路径,即局部最优,再将所有局部最优结合到一起,就是全局最优。
所以步骤分为:
- 找当前节点 i i i连接的所有节点中的最短路径,如果存在该节点,记为 t t t。
- 将
t
t
t到起点的距离更新,对比从起点到
t
t
t的距离和起点到
i
i
i的距离加上
i
i
i到
t
t
t的距离之间的大小,将小的更新为
t
t
t到起点的距离。
这里插一脚,关于稠密图和稀疏图的区别:边的个数远远大于点的个数就是稠密图,边的个数和点的个数差不多就是稀疏图。
步骤一还可以优化,等下给出,先看未优化的。
未优化
Python3
n, m = map(int, input().split())
INF = float('inf')
# 用邻接矩阵存储图,邻接矩阵用于稠密图
g = [[INF for _ in range(n+1)] for _ in range(n+1)]
dist = [INF for _ in range(n+1)] # 存储起点到当前遍历的点的最短距离
st = [False for _ in range(n+1)] # 判断当前节点是否已经有最短路
def Dijkstra():
dist[1] = 0 # 起点到起点的距离为0
for i in range(n):
# t找到距离i最近的点
t = -1
for j in range(1, n+1):
if not st[j] and (t == -1 or dist[t] > dist[j]):
t = j
# 当前t有了最短路径
st[t] = True
# 更新dist
for j in range(1, n+1):
dist[j] = min(dist[j], dist[t] + g[t][j])
# 如果图中出现了断层,没有最短路,则return -1
if dist[n] == INF:
return -1
else:
# 反之则return到n的最短路距离
return dist[n]
while m:
m -= 1
a, b, c = map(int, input().split())
g[a][b] = min(g[a][b], c)
print(Dijkstra())
C++
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int g[N][N], d[N];
bool st[N];
int n, m;
int Dijkstra()
{
memset(d, INF, sizeof d);
d[1] = 0;
for (int i = 1; i <= n; i++)
{
// 步骤1
int t = -1;
for (int j = 1; j <= n; j++)
{
if (!st[j] && (t == -1 || d[t] > d[j]))
{
t = j;
}
}
st[t] = true;
for ( int j = 1; j <= n; j ++ )
{
d[j] = min(d[j], d[t] + g[t][j]);
}
}
if ( d[n] == INF ) return -1;
else return d[n];
}
int main()
{
cin >> n >> m;
memset(g, INF, sizeof g);
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
// min有重边取最小
g[a][b] = min(g[a][b], c);
}
int t = Dijkstra();
cout << t << endl;
return 0;
}
未优化的时间复杂度为 O ( n 2 ) O(n^2) O(n2),查找的n和更新的n。
在查找最短边的过程中,我们发现,可以在更新
d
i
s
t
dist
dist的过程中去顺便找到最短边。
因为只要我们将
d
i
s
t
[
1
]
dist[1]
dist[1]初始化为0,且
d
i
s
t
dist
dist数组的其他节点设置为无穷大,则在遍历1所连接的节点时,1的所有节点的
d
i
s
t
dist
dist都要比
d
i
s
t
dist
dist大,此时会将1所连接的所有的节点的
d
i
s
t
dist
dist都更新,但我们只要距离最短的,所以此时我们可以使用一个小根堆,将更新了
d
i
s
t
dist
dist的所有节点都存放到该小根堆中,按照各节点的
d
i
s
t
dist
dist值来排序,此时我们就完成了查找最短边的任务,例如下图。
因为堆优化与边的数量有关,所以用于稀疏图,用邻接表存储。
优化后
Python3
# 通过小根堆,将查找当前节点距离最近的点的步骤的时间复杂度
import heapq
n, m = map(int, input().split())
INF = float('inf')
N = 100010
M = 2 * N
# 这里利用的是领接表存储图
h = [-1 for _ in range(N)]
e = [0 for _ in range(M)]
ne = [0 for _ in range(M)]
w = [0 for _ in range(M)] # 存放上一个节点到当前节点的距离
idx = 0
d = [INF for _ in range(n+1)]
st = [False for _ in range(n+1)]
def add(a, b, c):
global idx
e[idx] = b
w[idx] = c
ne[idx] = h[a]
h[a] = idx
idx += 1
while m:
m -= 1
a, b, c = map(int, input().split())
add(a, b, c)
ans = []
def dijkstra():
d[1] = 0
heap = []
# 小根堆依据第一个元素排序,所以要将距离放在编号的前面,不然就会找bug找半天
heapq.heappush(heap, (0, 1))
while heap:
t = heapq.heappop(heap)
# 取得编号
var = t[1]
ans.append(var)
# 取得距离
distance = t[0]
# 如果该点已经存在最短路径,则跳过该点
if st[var]:
continue
# 反之则将该点设置为有最短路
st[var] = True
# 遍历t[1]所连接的节点
i = h[var]
while i != -1:
j = e[i]
# 如果当前点j到起点的距离大于起点到t[1]的距离+t[1]到j的距离,则更新d[j]
if d[j] > distance + w[i]:
d[j] = distance + w[i]
# 将当前更新了最短路的点存入小根堆中
heapq.heappush(heap, (d[j], j))
i = ne[i]
if d[n] == INF:
return -1
else:
return d[n]
print(dijkstra())
C++
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 200010;
const int M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
bool st[N];
typedef pair<int, int> PII;
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int dijkstra() // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
{
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;
int 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()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
堆优化后时间复杂度降到了
O
(
(
n
+
m
)
log
2
n
)
O((n + m) \log_2n)
O((n+m)log2n),
O
(
log
2
n
)
O(\log_2n)
O(log2n)为从堆顶取出元素的时间复杂度,最多有n次操作,再加上将更新了
d
i
s
t
dist
dist的节点插入到堆中的
O
(
log
2
n
)
O(\log_2n)
O(log2n),最多有m次操作。
这里就能回答为什么堆优化的Dijkstra要用于稀疏图了,对于稀疏图,因为边数和点数差不多,所以可以将时间复杂度变为
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n),但在稠密图中,因为边的个数远远大于点的个数,所以在某些不好的情况下,会将时间复杂度升级为
O
(
n
2
log
2
n
)
O(n^2\log_2n)
O(n2log2n),还不如不用堆优化。
还能用斐波那契堆来实现小根堆,可以将时间复杂度降到 O ( n log 2 n + m ) O(n\log_2n + m) O(nlog2n+m),感兴趣的可以去了解一下。
Dijkstra求最短路一般用于没有负权边的带权图,所以在遇到有负权边的图时,会考虑使用 B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法。
Bellman-Ford求最短路
Bellman-Ford算法能够处理图中带有负权边的情况,为什么Dijkstra不能够处理这种情况呢?因为Dijkstra算法是选所有的局部最优就为全局最优,但因为有负权边的存在,局部最优不一定为全局最优,所以不能够使用Dijkstra处理。
Bellman-Ford算法对图的存储方式有点随便,并不用邻接表和邻接矩阵,直接开个数组存放即可。
Bellman-Ford算法的遍历过程有点像BFS,一层一层的遍历,更新当前节点到所连接的所有节点的dist。要注意的是,因为有负权边的存在,使得局部最短并不一定为全局最短,所以我们才需要像BFS那样的遍历方式,同时遍历每层节点所连接的所有子节点,不断的去更新对应子节点的dist,使得子节点的dist为全局最短而非局部最短。
比如下图的节点x,它对应的dist值被更新了4次,在这4次中选出最短路。
通过上图也能够大致理解Bellman-Ford算法的遍历顺序,下面是具体代码实现和之中存在的问题。
主要有两个问题:
- 为什么需要backup数组对dist数组进行copy
主要目的是防止串联,因为是依次遍历m条边,如果不copy,则在更新了一条a->b的边后,dist[b]的值被改变,我们的目的是只改变a所连接的所有的节点的dist值,在继续向后遍历时,因为dist[b]的值被改变,则如果遍历到b->c时,dist[c]的值可能被改变,但我们并不希望如此,则需要copy一份dist,使得在遍历到b->c时,dist[b]的值没有改变,即仍为INF,所以dist[c]不会被更新。
- 为什么对结果要 i f ( t > I N F / / 2 ) if (t > INF // 2) if(t>INF//2)而不是直接 i f ( t = = I N F ) if (t == INF) if(t==INF)
因为有负权边,INF加了负数可能比原本的INF小了。因为copy的原因虽然最短路径没有走到这,但是还是能更新dist[n],只不过幅度较小,最后的值肯定比 I N F / / 2 INF // 2 INF//2大。
Python3
n, m, k = map(int, input().split())
g = [0 for _ in range(m+1)]
INF = 0x3f3f3f3f
d = [INF for _ in range(n+1)]
backup = []
def ballman_ford():
d[1] = 0
for i in range(k):
backup = d.copy()
for j in range(m):
a, b, c = g[j]
d[b] = min(d[b], backup[a] + c)
return d[n]
for i in range(m):
a, b, c = map(int, input().split())
g[i] = [a, b, c]
t = ballman_ford()
if t > (INF / 2):
print('impossible')
else:
print(d[n])
C++
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
struct Edge
{
int a, b, c;
}g[N];
int dist[N];
int n, m, k;
int backup[N];
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 ++)
{
auto e = g[j];
dist[e.b] = min(dist[e.b], backup[e.a] + e.c);
}
}
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
g[i] = {a, b, c};
}
bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) cout << "impossible" << endl;
else cout << dist[n] << endl;
return 0;
}
spfa求最短路
spfa是常用的求最短路的方法了,和Dijkstra很像,也能用在有负权边的情况,算是对Bellman-Ford算法用队列优化。
思路也比较简单,就是当前节点更新了最短路,才继续遍历当前节点的子节点,不然不遍历。即通过队列,将更新过dist的节点入队,否则不入队,更有BFS的味了。
有一些疑惑的点:
- 为什么对结果的处理不和Bellman-Ford一样,而是直接判断是否和 f l o a t ( ′ i n f ′ ) float('inf') float(′inf′)相等?
因为spfa不像Bellman-Ford那样,spfa因为队列的原因,只有走到n了才更新dist[n]的值,而Bellman没走到n也可以更新到dist[n]的值,因为每一次都遍历m条边。
Python3
from collections import deque
n, m = map(int, input().split())
# 稀疏图,用邻接表存储
N = 100010
M = N * 2
h = [-1 for _ in range(N)]
e = [0 for _ in range(M)]
ne = [0 for _ in range(M)]
w = [0 for _ in range(M)]
idx = 0
d = [float('inf') for _ in range(n+1)]
def add(a, b, c):
global idx
e[idx] = b
ne[idx] = h[a]
h[a] = idx
w[idx] = c
idx += 1
# st存放每个点是否在队列中
st = [False for _ in range(n+1)]
# 有最短路才继续更新它连接的节点的最短路
def spfa():
queue = deque()
queue.append(1)
d[1] = 0
st[1] = True
while queue:
t = queue.popleft()
i = h[t]
st[t] = False
while i != -1:
j = e[i]
if d[j] > d[t] + w[i]:
d[j] = d[t] + w[i]
if not st[j]:
queue.append(j)
st[j] = True
i = ne[i]
return d[n]
while m:
m -= 1
a, b, c = map(int, input().split())
add(a, b, c)
t = spfa()
if t == float('inf'):
print('impossible')
else:
print(t)
C++
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
int n, m;
const int N = 100010;
const int M = N;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int spfa()
{
queue<int> q;
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
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()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if (t == 0x3f3f3f3f)
{
cout << "impossible" << endl;
}
else
{
cout << t << endl;
}
return 0;
}
Floyd求最短路
暴力点的做法,时间复杂度较高,内有动态规划的味,没学,看看板子就好,感兴趣可以看看算法导论的404页有细讲,这里扣个图下来,我觉得就是核心了。
这里对结果的判断原因和Bellman-Ford差不多,Floyd中会对所有的边都更新dist,所以即使没有最短边,dist中的值也会被小幅度改变,所以要用 i f ( g [ a ] [ b ] > I N F / 2 ) if(g[a][b] > INF / 2) if(g[a][b]>INF/2)的方式判断。
Python
n, m, q = map(int, input().split())
g = [[0 for _ in range(n+1)] for _ in range(n+1)]
INF = 1e9
def floyed():
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 i in range(1, n+1):
for j in range(1, n + 1):
if i == j:
g[i][j] = 0
else:
g[i][j] = INF
while m:
m -= 1
a, b, c = map(int, input().split())
g[a][b] = min(g[a][b], c)
floyed()
while q:
q -= 1
a, b = map(int, input().split())
if g[a][b] > INF // 2:
print('impossible')
else:
print(g[a][b])
C++
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int g[N][N];
int n, m, q;
void floyd()
{
for (int k = 1; k <= n; k ++)
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++)
g[i][j] = min(g[i][j], g[i][k] + g[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) g[i][j] = 0;
else g[i][j] = INF;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
floyd();
while (q--)
{
int a, b;
cin >> a >> b;
if (g[a][b] > INF / 2)
cout << "impossible" << endl;
else
cout << g[a][b] << endl;
}
return 0;
}