0%

最短路径算法

前言

最短路算法用来求图中两个点的最短路径。最短路又分为单源最短路、多源汇最短路。

定义:图中顶点数为$n$,边数为$m$。

单源最短路

单源最短路:从一个点到其他所有点的最短距离。根据权值分为两种情况:所有权值为正数、存在权值为负数。

所有权值为正数

Dijkstra算法

1.适用情况:适用于稠密图,$n^2$与$m$为一个级别。

2.时间复杂度:$O(n^2)$

3.图存储方式:邻接矩阵

4.算法描述:$g$为邻接矩阵,$dist$为每个点到源点的距离,集合$s$为已经确定最短路径的顶点集合。初始时,$dist$为正无穷,$s$包含源点。

  a.设 $dist[1] = 0$,表示源点的距离确定为$0$。

  b.从还没有确定的最短路径的点中,选取距离最小的点。

  c.将该点加入到$S$中,用该点更新其它点的距离。

  d.重复b和c,直到$S$中包含所有的顶点。

5.代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static int dijkstra(){
for(int i = 0; i < N; i++)dist[i] = 0x3f3f3f3f;//表示无穷大
dist[1] = 0;

for(int i = 0; i < n; i++){
int t = -1; //t是还没有确定距离, 距离最近的点

for(int j = 1; j <= n; j++){
if(!st[j] && (t == -1 || dist[t] > dist[j]))t = j;//在没有确定的集合中,选一个距离最短的点
}
st[t] = true;

//用t来更新距离
for(int j = 1; j <= n; j++){
dist[j] = Math.min(dist[j], dist[t] + g[t][j]);
}
}

if(dist[n] == 0x3f3f3f3f)return -1;
return dist[n];
}

堆优化版的Dijkstra算法

1.适用情况:适用于稀疏图,$n$与$m$为一个级别。

2.时间复杂度:$O(mlogn)$

3.图存储方式:邻接表

4.算法描述:大体上与Dijkstra算法一致,区别是:堆优化版的Dijkstra算法对Dijkstra算法的b步骤进行优化,用优先队列模拟小根堆,根节点为距离最小的点,这一步优化后将$O(n^2)$的复杂度,降低到$O(n)$。

5.代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static int dijkstra(){
for(int i = 0; i < N; i++)dist[i] = 0x3f3f3f3f;
dist[1] = 0;

heap.offer(new int[]{1, 0}); //1号点,距离为0

while(!heap.isEmpty()){
int[] temp = heap.poll(); //小根堆,距离最小的点
int t = temp[0];
int distance = temp[1];
if(st[t])continue;
st[t] = true;
for(int i = h[t]; i != -1; i = ne[i]){ //稀疏图用邻接表存储
int j = e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
heap.offer(new int[] {j, dist[j]});
}
}
}
return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}

优先队列实现小根堆:

1
public static PriorityQueue<int[]> heap = new PriorityQueue<>(N, (a, b) -> {return a[1] - b[1];});

存在权值为负数

bellman-ford算法

1.适用情况:存在负权边,求$1$号点到$n$号点的最短路,最多经过$k$条边。

2.时间复杂度:$O(nm)$

3.图存储方式:存端点$(a, b)$和边权$w$

4.算法描述:对每条边都执行松弛操作($dist[b] = min(dist[b], dist[a] + w)$),共$k$次。

5.代码实现:

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
class Main{
public static int N = 510, n, m, k, M = 10010;
public static int[] dist = new int[N];
public static int[] backup = new int[N]; //为了防止串联效应, 用更新的点的距离又去更新其他的点

static class Edge{//创建一个边类,包含属性a, b, w
int a, b, w;
public Edge(int a, int b, int w){
this.a = a;
this.b = b;
this.w = w;
}
}
public static Edge[] edge = new Edge[M];//有M条边, 创建Edge类型的数组

public static int bellman_ford(){
Arrays.fill(dist, 0x3f3f3f3f);
dist[1] = 0;

for(int i = 0; i < k; i++){
backup = dist.clone(); //更新前需要拷贝上一次的dist, 防止串联效应
for(int j = 0; j < m; j++){
int a = edge[j].a, b = edge[j].b, w = edge[j].w;
dist[b] = Math.min(dist[b], backup[a] + w); //用上一次a的距离来更新
}
}
return dist[n] > 0x3f3f3f3f / 2 ? -1 : dist[n];//为什么是0x3f3f3f3f/2?若a的距离为无穷,用0x3f3f3f3f表示, 若a-b之间权重为-w, dist[b] = 0x3f3f3f3f-w(w<10000)
}

public static void main(String[] args){
Scanner sc = new Scanner(new BufferedInputStream(System.in));
n = sc.nextInt();
m = sc.nextInt();
k = sc.nextInt();

for(int i = 0; i < m; i++){
int x = sc.nextInt(), y = sc.nextInt(), z = sc.nextInt();
edge[i] = new Edge(x, y, z); //创建一个边类,包含属性a, b, w
}

int t = bellman_ford();
if(t == -1)System.out.println("impossible");
else System.out.println(t);
}
}

spfa算法

1.适用情况:存在负权边,可以用来求最短路以及判断图中是否存在负环。

2.时间复杂度:一般为$O(m)$, 最坏为$O(nm)$.

3.图存储方式:邻接表

4.算法描述:spfa算法是对bellman-ford算法的改进,bellman-ford算法每一次会对所有边都进行松弛操作($dist[b] = min(dist[b], dist[a] + w)$),但是不一定每次$dist[b]$都会改变,若$dist[b]$要变,只有当$dist[a]$变小。spfa对这一步进行优化,不用每一次都进行松弛操作,维护一个队列,队列里的元素是距离被更新的点(若$dist[b]$变小,就把$b$放到队列),用这些距离被更新的点去更新与这个点相邻的其他点。

5.代码实现:

求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
public static int spfa(){
Arrays.fill(dist, 0x3f3f3f3f);
dist[1] = 0;

queue.offer(1); //源点放入队列
st[1] = true; //st标志队列里面的元素,防止队列出现重复元素

while(!queue.isEmpty()){
int t = queue.poll();
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]){//若j的距离变小,并且j不在队列中,则将j放到队列中
queue.offer(j);
st[j] = true;
}
}
}
}
return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}

判断图中是否存在负环:int[] cnt用来记录存1-x最短距离的边数,当$cnt[x] >= n$时,则可以判断图中存在负环(因为$cnt[x] >= n$意味着1-x有$n$条边,$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
public static boolean spfa(){
Arrays.fill(dist, 0x3f3f3f3f);
dist[1] = 0;
// 由于是判断图中是否存在负环,不是从1开始的负环,所以把每个点都加入队列,
for(int i = 1; i <= n; i++){
queue.offer(i);
st[i] = true;
}

while(!queue.isEmpty()){
int t = queue.poll();
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]){
queue.offer(j);
st[j] = true;
}
}
}
}
return false;
}

多源最短路

floyd算法

1.适用情况:求任意两个点之间的最短路径。

2.时间复杂度:$O(n^3)$

3.图存储方式:邻接矩阵

4.算法描述:初始时,$d$为邻接矩阵,经过$d[i, j] = min(d[i, j],d[i, k] + d[k, j])$的$n^3$次计算,最后$d[i, j]$表示$i$到$j$的最短路径长度。

5.代码实现:

1
2
3
4
5
6
7
8
9
public static 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] = Math.min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}