主页 图的5种常用的最短路算法
Post
取消

图的5种常用的最短路算法

1. 简介

图的5种常用的最短路算法为:朴素Dijkstra、 堆优化Dijkstra、Bellman-Ford、SPFA、 Floyd。 以下为这5种算法的应用场景以及时间复杂度:

  • 多源汇最短路(任意两点间的最短路)
    • Floyd O(n^3)
  • 单源最短路
    • 无负边权(所有边长都为正数)
      • 稠密图: 朴素Dijkstra O(n^2)
      • 稀疏图: 堆优化Dijkstra O(mlogn)
    • 有负边权
      • SPFA 一般O(m),最坏O(nm)
      • 有边数限制:Bellman-Ford O(nm)

2. 算法实现:

2.1 朴素Dijkstra

算法思想:

朴素DijKstra的思想是通过一个距离起点A最近的点B,缩短起点A通过点B到达其他点的距离,因为只有通过距离起点最近的点,才有可能缩短起点到达其他点的距离。 所以一共分两步: (1)找到距离起点最近的点 (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
function dijkstra($n, $edges)
 {
     $dist = [];
     $state = [];

     // 绘图
     $graph = [];
     for ($i = 1; $i <= $n; $i++) {
         $state[$i] = false;
         $dist[$i] = PHP_INT_MAX;
         for ($j = 1; $j <= $n; $j++) {
             $graph[$i][$j] = PHP_INT_MAX;
         }
     }
     foreach ($edges as [$u, $v, $w]) {
         $graph[$u][$v] = $graph[$v][$u] = $w;
     }

     // 朴素迪杰斯特拉算法
     $dist[1] = 0;
     for ($i = 1; $i <= $n; $i++) { // 循环n次, 每次找出一个离起点最近的点。
         $t = -1;
         for ($j = 1; $j <= $n; $j++) {
             if(!$state[$j] && ($t == -1 || $dist[$j] < $dist[$t])) {
                 $t = $j;
             }
         }
         $state[$t] = true;
         for ($j = 1; $j <= $n; $j++) {
             if($dist[$j] > $dist[$t] + $graph[$t][$j]) {
                 $dist[$j] = $dist[$t] + $graph[$t][$j];
             }
         }
     }

     return $dist[$n] >= PHP_INT_MAX ? -1 : $dist[$n];
 }

2.2 堆优化Dijkstra

算法思想:

堆优化其实就是使用优先队列对图BFS一遍,在搜索的同时更新点的距离。朴素算法需要遍历所有点找出距离最近的点,使用优先队列使这一步的时间复杂度从N变为了logN。 对于走迷宫问题,我们很容易可以使用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
function heapDijkstra($n, $edges)
{
    // 绘图
    $graph = [];
    foreach ($edges as [$u, $v, $w]) {
        $graph[$u][$v] = $graph[$v][$u] = $w;
    }
    $dist = array_fill(1, $n, PHP_INT_MAX);
    $dist[1] = 0;

    // 堆优化迪杰斯特拉算法
    $priorityQueue = new SplPriorityQueue;
    $priorityQueue->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
    $priorityQueue->insert(1, 0);
    while ($priorityQueue->count()) {
        $ins = $priorityQueue->extract();
        $data = $ins['data'];
        $priority = -$ins['priority']; // SplPriorityQueue 是大根堆所以加负号
        foreach ($graph[$data] as $node => $w) {
            if ($w + $priority < $dist[$node]) {
                $dist[$node] = $w + $priority;
                $priorityQueue->insert($node, -$dist[$node]);
            }
        }
    }

    return $dist[$n] >= PHP_INT_MAX ? -1 : $dist[$n];
}

2.3 Bellman-Ford

算法思想:

Bellman-Ford算法是先将每个点到达起点的距离设为正无穷,然后直接暴力枚举所有边,如果一个点的距离可以被更新,就更新该点的距离。 枚举一次至少可以得到一个点到达起点的距离,直接无脑枚举n次,就可以得到所有点到起点的最短距离。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function bellmanFord($n, $edges)
{
    $dist = array_fill(1, $n, PHP_INT_MAX);
    $dist[1] = 0;

    $m = count($edges);
    for ($i = 1; $i <= $n; $i++) {
        for ($j = 0; $j < $m; $j++) {
            $u = $edges[$j][0];
            $v = $edges[$j][1];
            $w = $edges[$j][2];
            if ($dist[$u] + $w < $dist[$v]) {
                $dist[$v] = $dist[$u] + $w;
            }
        }
    }

    return $dist[$n] >= PHP_INT_MAX ? -1 : $dist[$n];
}

2.4 SPFA((Shortest Path Faster Algorithm))

算法思想:

我们在一次循环中做了很多不必要的操作,如果我们可以只更新那些前驱结点被更新过的点,就可以大大提升效率。因为只有前驱结点到达起点的距离减小了,它的后继结点到达起点的距离才能减小。 所以我们用宽搜的思想,每次将被更新过的点入队,然后遍历队列中每个点的后继结点,重复更新操作和入队操作。 只要不存在负环,起点到达任意点所经过的边数一定小于n,反之一定大于等于n因为只有n个点,不经过环的话最多经过n-1条边,只有在存在负环的情况下,SPFA才会经过负环,经过负环后,到达其他点边数就会大于等于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
function SPFA($n, $edges)
{
    // 绘图
    $graph = [];
    foreach ($edges as [$u, $v, $w]) {
        $graph[$u][$v] = $graph[$v][$u] = $w;
    }
    $dist = array_fill(1, $n, PHP_INT_MAX);
    $dist[1] = 0;

    $visited = array_fill(1, $n, false);
    $visited[1] = true;
    $cnt[1] = 1;
    $finish = false;

    $queue = new SplQueue;
    $queue->enqueue(1);
    while (!$finish && $queue->count()) {
        $value = $queue->dequeue();
        $visited[$value] = false;
        foreach ($graph[$value] as $node => $w) {
            if ($w + $dist[$value] < $dist[$node]) {
                $dist[$node] = $w + $dist[$value];
                // 最多经过边数为$n-1
                $cnt[$node] = $cnt[$value] + 1;
                if ($cnt[$node] > $n) {
                    $finish = true;
                    break;
                }
                if (!$visited[$node]) {
                    $queue->enqueue($node);
                    $visited[$node] = true;
                }
            }
        }
    }

    return $dist[$n] >= PHP_INT_MAX ? -1 : $dist[$n];
}

2.5 Floyd

算法思想:

对于朴素Dijkstra算法来说,起点通过距离它最近的点去缩短其到达其他点的距离,Floyd算法本质上也是这样的思想。 (1)通过一个点,遍历所有点对,更新距离。 (2)通过n个点,遍历所有点对n次,更新距离。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function floyd($n, $edges)
{
    $dist = [];
    for ($i = 1; $i <= $n; $i++) {
        for ($j = 1; $j <= $n; $j++) {
            $dist[$i][$j] = PHP_INT_MAX;
        }
        $dist[$i][$i] = 0;
    }
    foreach ($edges as [$u, $v, $w]) {
        $dist[$u][$v] = $dist[$v][$u] = $w;
    }

    for ($i = 1; $i <= $n; $i++) {
        for ($j = 1; $j <= $n; $j++) {
            for ($k = 1; $k <= $n; $k++) {
                $dist[$i][$j] = min($dist[$i][$j], $dist[$i][$k] + $dist[$k][$j]);
            }
        }
    }

    return $dist[1][$n] >= PHP_INT_MAX ? -1 : $dist[1][$n];
}
该博客文章由作者通过 CC BY 4.0 进行授权。