算法分析与设计CH24:最短路径算法——迪杰斯特拉算法(dijkstra,队列优化)、BellmanFord、弗洛伊德(Floyd),负权边处理Johnson算法

CH24:Shorest Path

MindMap:

在这里插入图片描述

24.1 定义

最短路径问题:

最优解:最短路径,怎么走

最优解的值:最短路径的长度

最短路径的权重定义为:
最短路径的权重定义为: { m i n ( { w ( p ) :if there is a path p from u to v } ∞ o t h e r w i s e 最短路径的权重定义为:\begin{cases}min(\{w(p):\text{if there is a path p from u to v}\} \\ \infty\quad otherwise\end{cases} 最短路径的权重定义为:{min({w(p)if there is a path p from u to v}otherwise
一般使用有向图,无向图视作双向。

最短路径一定满足最优子结构(不可用simple约束)


三角不等式:
σ ( u , v ) ≤ σ ( u , x ) + σ ( x , v ) \sigma (u,v)\leq \sigma(u,x)+\sigma(x,v) σ(u,v)σ(u,x)+σ(x,v)
在这里插入图片描述

如果图中包含负权环,那么最短路径可能不存在。

在这里插入图片描述

24.2 Single-Source shortest path

单源:源点的位置固定,source固定

每对 σ ( u , v ) \sigma(u,v) σ(u,v):每一对结点之间

24.2.1 算法分析

满足最优子结构:一定使用子问题的最优解

  • 二分

k是中间的一个结点,找u->k + k->v,穷举所有的k,挑一个最小的

无法计算:可能存在子问题互相包含。

  • n推n-1

一样的问题。

计算次序的确定是最短路径的关键。

计算次序是否存在?
{ 无顺序:人为的增加一个顺序 有顺序:尝试,分别做子问题的尝试 \begin{cases}无顺序:人为的增加一个顺序\\ 有顺序:尝试,分别做子问题的尝试\end{cases} {无顺序:人为的增加一个顺序有顺序:尝试,分别做子问题的尝试

24.2.2 dijkstra算法

(1)思路分析

顺序是否真的不存在?考虑下面的图:

image-20220609145418264

特点:无负权,可能存在计算次序

无负权值的图有特点:越往后走路径只能越长,所以对于从A到其余的点,C离A更近,只可能C是别的问题的子问题,不可能别的问题是C的子问题。于是:

  • 尝试让C当别的问题的子问题
  • 尝试让E当别的问题的子问题
  • 尝试让B充当别的问题的子问题
  • ……

与前面的DP逻辑的不同:

  • DP:父问题找子问题
  • 现在:子问题找父问题

即dijkstra算法

(2)程序实现

在这里插入图片描述

(3)时间复杂度分析

最坏的情况下,每条边都会用来做松弛,时间复杂度为:
T ( n ) = Θ ( V T E X T R A C T _ M I N + E T D E C R E A S E _ K E Y ) T(n) = \Theta(VT_{EXTRACT\_MIN} +ET_{DECREASE\_KEY}) T(n)=Θ(VTEXTRACT_MIN+ETDECREASE_KEY)

Q T E X T R A C T _ M I N T_{EXTRACT\_MIN} TEXTRACT_MIN T D E C R E A S E _ K E Y T_{DECREASE\_KEY} TDECREASE_KEYTotal
arrayO(V)O(1)O(v2)
Binary HeapO(lgV)O(lgV)O(ElgV)
Fibonacci HeapO(lgV)O(1)O(E+VlgV)

24.2.3 其他边权的情况

24.2.3.1 无权图

使用广度优先搜索,堆数据结构换为队列即可。

image-20220609160619997

时间复杂度 T i m e = O ( V + E ) Time=O(V+E) Time=O(V+E)

image-20220609161024581

24.2.4 Bellman-Ford

如果真的含有负权值边,那么之前的计算次序将不可用,使用Bellman-Ford

其思想在于:若子问题嵌套,那么多使用几次。只要有一条边就拿来尝试更新,但不是只使用一次。

本质:穷举所有的前驱

循环次数:|V|-1

程序实现:

image-20220609161615633

算法实现时,可以为遍历边的次序进行规定。按照次序利用每条边尝试进行松弛。

/***  Bellman-Ford算法  ***/
void Graph::bellman_ford(char s) {
    int source = s - 'A';   // 源点的下标
    vector<int> distance(vexnum, 999999);   // 距离数组的初始化
    distance[source] = 0;
    vector<char> pre(vexnum, '@');
    int iternum = vexnum - 1;
    for (int i = 0; i < iternum; i++) {  // 松弛次数为顶点数-1
        for (int j = 0; j < arcList.size(); j++) {   // 邻接表头
            for (int k = 0; k < arcList[j].size(); k++) {    // 遍历每条边
                int target = arcList[j][k].adjvec;
                if (distance[target] > distance[j] + arcList[j][k].weight) {
                    distance[target] = distance[j] + arcList[j][k].weight;
                    pre[target] = j + 'A';
                }
            }
        }
    }
    // 再松弛一次,若有负环则报错
    for (int i = 0; i < arcList.size(); i++) {
        for (int j = 0; j < arcList[i].size(); j++) {   // 遍历每条边
            int target = arcList[i][j].adjvec;
            if(distance[target] > distance[i] + arcList[i][j].weight) {
                cout << "Error: There exists a negative weight circle in the graph!";
                return;
            }
        }
    }
    // 否则没有负权环,打印最短路径长度
    printDist(distance, s, pre);
}

思考,为什么进行n-1轮即可。

一个无负环的图,最短路径的边的最多条数为n-1条,否则会出现环。

不绕环的最短路径,边数最多是n-1

image-20220609162440441

24.3 All-pairs shorest path

所有结点对之间的最短路径:

  • 无负权:跑n遍dijkstra O ( V 2 l g V + E V l g V ) O(V^2lgV + EVlgV) O(V2lgV+EVlgV)
  • 含负权:跑n遍Bellman-Ford

24.3.1 用边数做约束的DP

(1)算法思想

Dynamic Programming 规模变小:边数变少

限制边的数量——n推n-1

定义:
d i j m : 从 i → j 最多过 m 条边的最短路径 d_{ij}^m:从i→j最多过m条边的最短路径 dijm:ij最多过m条边的最短路径
那么有:
d i j 0 = { 0 i f   i = j ∞ i f   i ≠ j d_{ij}^0 =\begin{cases}0&& if \ i=j\\ \infty&& if\ i\neq j\end{cases} dij0={0if i=jif i=j
松弛为:

求 k —> v的最短路径,过100条边的最短路径,求99条,穷举k
d i j ( m ) = min ⁡ k { d i , k ( m − 1 ) + a k , j } d_{ij}^{(m)}=\min_{k}\{d_{i,k}^{(m-1)} + a_{k,j}\} dij(m)=kmin{di,k(m1)+ak,j}
最终刻画问题为最多过x条边的最短路径。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rgKyWroB-1659619328463)(https://cdn.jsdelivr.net/gh/Holmes233666/blogImage@main/img/image-20220609165216909.png)]

  • 自上而下:推到m=0时就有答案了。
  • 自下而上:从m=0开始推,m从小到大计算

d i j 0 : i 到 j 最多 0 条边 → d i j 1 : i 到 j 最多 1 条边 → … … d i j n − 1 : i 到 j 最多 n − 1 条边 d_{ij}^0:i到j最多0条边\rightarrow d_{ij}^1:i到j最多1条边\rightarrow……d_{ij}^{n-1}:i到j最多n-1条边 dij0ij最多0条边dij1ij最多1条边……dijn1ij最多n1条边

无负环,最短路径最多n-1条边

假设使用迭代的方式:

for m = 1~n-1	// 每个m计算一个二维数组
    for i = 1~n
        for j = 1~n
            for k = 1~n	// 所有前驱

需要使用三维数组,逻辑上是一个二维数组

实际的操作没有标m:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j1F5P774-1659619328464)(https://cdn.jsdelivr.net/gh/Holmes233666/blogImage@main/img/image-20220609170508574.png)]

计算的过程可能被加速:

无负环:
σ ( i , j ) = d i j ( n − 1 ) = d i j ( n ) = d i j ( n + 1 ) … … \sigma(i,j) = d_{ij}^{(n-1)} = d_{ij}^{(n)}=d_{ij}^{(n+1)}…… σ(i,j)=dij(n1)=dij(n)=dij(n+1)……
故m在工程中可以省略。

负环如何检测? 再计算一轮未必能检测出来

解决:i=j的场景,检查对角线是否有负值,并将m算到n

将该算法应用到单源点,即为Bellman-Ford算法。

(2)改进:Matrix multiplication

每次使用上一次计算的结果和邻接矩阵计算一个新的矩阵。上述思想的伪代码表示如下:

for m = 1 ~ n-1
    for i = 1 ~ n
        for j = 1 ~ n
            for k = 1 ~ n
                if d_ij^m >= d_ik^m + a_{k,j}
					d_ij^m = d_ik^m + a_{k,j}

需要穷举所有的k,所以使用的是结果矩阵的第i行去加上邻接矩阵的第j列。——类似于矩阵相乘的逻辑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hdyC9Jlv-1659619328465)(https://cdn.jsdelivr.net/gh/Holmes233666/blogImage@main/img/image-20220609203755221.png)]

C i j = min ⁡ { a i k + b k j } C_{ij} = \min\{a_{ik}+b_{kj}\} Cij=min{aik+bkj}

24.3.2 用顶点数做约束的DP:Folyed Warshell

(1)算法思路

定义:
c i j k 表示从 i 到 j 最多经过前面 k 个顶点的最短路径 c_{ij}^k表示从i到j最多经过前面k个顶点的最短路径 cijk表示从ij最多经过前面k个顶点的最短路径
尝试n推n-1:

过前n个结点 尝试 推给过前n-1个结点,类比0-1背包,那么只有两种情况:
c i j ( k ) { 过第 k 个顶点: c i k ( k − 1 ) + c k j ( k − 1 ) 不过第 k 个顶点: c i j ( k − 1 ) c_{ij}^{(k)}\begin{cases}过第k个顶点:c_{ik}^{(k-1) }+ c_{kj}^{(k-1)}\\ 不过第k个顶点:c_{ij}^{(k-1)}\end{cases} cij(k){过第k个顶点:cik(k1)+ckj(k1)不过第k个顶点:cij(k1)

过0个结点时, a i j a_{ij} aij的权值就是答案
c i j ( k ) = min ⁡ k { c i j ( k − 1 ) , c i k ( k − 1 ) + c k j ( k − 1 ) } c_{ij}^{(k)} = \min_k\{c_{ij}^{(k-1)}, c_{ik}^{(k-1)}+c_{kj}^{(k-1)}\} cij(k)=kmin{cij(k1),cik(k1)+ckj(k1)}
image-20220610105556208

最终过不过第k个结点,比较进行确定。

伪代码:

for k = 1 ~ n
    for i = 1 ~ n
        for j = 1 ~ n
            do if cij > cik + ckj			// Relaxation
                then cij = cik + ckj	

只用一个数组,和上述的用边约束相同,可能会加速计算,但结果不会出错。

Example:

  • 一个结点都不过:原距离矩阵
image-20220610111624976
  • 过第1个结点

    image-20220610112008609
  • 过第2个结点

    image-20220610112050354
  • 过第3个结点

image-20220610112035534
(2)负环检测
image-20220610112325464

24.4 Johnson 算法

要是是正权值,那么可以使用dijkstra算法,如何重赋权

考虑下面的图,若顶点权值如下,使用 w ( u , v ) + h ( u ) − h ( v ) w(u,v)+h(u)-h(v) w(u,v)+h(u)h(v)得到一个新的权值,为正值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTsZOgAe-1659619328465)(https://cdn.jsdelivr.net/gh/Holmes233666/blogImage@main/img/image-20220610114431299.png)]

  • 运行一遍dijkstra,得到的最短路径值做reweighting

  • 选择谁当source

    • 数学性质上任何点都可以成立,但是要用于修改原来的权值
    • source到每个点的距离不能是无穷大
    • 实际工程上,使用一个不存在的源点source,到任何点的距离为0
  • 使用哪个算法?

    Bellman-Ford

算法步骤:

  • Bellman-Ford增加点的情况下跑最短路径值,作为顶点值
  • 重赋权
  • 跑dijkstra
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Blanche117

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值