算法设计与分析


单源最短路径

计算机学院    张腾

tengzhang@hust.edu.cn

问题背景

武汉到北京的

  • 最短行驶距离路线
  • 最短行驶时间路线
  • 最少通行费路线
问题描述

给定带权有向图$\Gcal = (\Vcal, \Ecal, w)$

  • 点集$\Vcal$对应城市
  • 边集$\Ecal$对应直接连接两城市的道路
  • 边上的权重由函数$w: \Ecal \mapsto \Rbb$给出,对应距离、时间、路费等

路径$p = \langle v_0, v_1, \ldots, v_k \rangle$的长度$l(p) = \sum_{i=1}^k w(v_{i-1}, v_i)$是路径上所有边的权重之和

问题目标:寻找源点$s$到目的点$t$的最短路径:

$$ \begin{align*} \quad \delta(s,t) = \begin{cases} \min_p \{ l(p) : s \overset{p}{\rightsquigarrow} t \}, & \text{如果存在 } s \text { 到 } t \text{ 的路径} \\ \infty, & \text{其它} \end{cases} \end{align*} $$

权重函数$w: \Ecal \mapsto \Rbb$的值域为$\Rbb$表示边上权重可以为负,对应于走该边可以挣一些钱

问题描述

几乎所有求图上两点间最短路径的问题都会归结为更一般的

单源最短路径问题 (single source shortest path, SSSP)

目标:求源点$s$到其它所有点的最短路径

输出:以源点$s$为根结点的最短路径树 (shortest path tree)

问题约定

最短路径是简单路径,即不包含环路、最多有$|\Vcal|-1$条边

  • 若最短路径包含权重非负的环路,去掉该环依然是最短路径
  • 图中不可能有权重为负的环路,否则最短路径不存在 (沿着环一直转)

path:每个点最多访问一次,否则称之为 walk

Ford 算法框架

对任意点$v$

  • $d_v$表示当前已知的$s \rightsquigarrow v$的最短路径距离,若无路径则为$\infty$
  • $p_v$表示当前已知的$s \rightsquigarrow v$的最短路径上$v$的前驱结点,若无则为空

初始化:$d_s = 0$$d_{v \ne s} = \infty$$p_v = \text{NULL}$

松弛 (relax):若$d_v < d_u + w(u,v)$,令$d_v = d_u + w(u,v)$$p_v = u$

Ford 算法框架:初始化所有点,若还有边可以松弛,松弛该边

  • 如何寻找可松弛的边?
  • 若有多条边可松弛,先松弛哪条?
无向图 BFS

$\Gcal$是无权图,路径长度即为经过的边的数目

Ford 算法框架实现:

  • 源点$s$入队
  • 当队列非空时,点$u$出队,遍历所有边$(u,v)$,若可松弛则松弛,点$v$入队

上述实现即为以$s$为起点的广度优先遍历

有向无环图 DFS

松弛

$$ \begin{align*} \quad d_v = \begin{cases} 0, & v = s \\ \min_u \{ d_v + w(u,v) \}, & \text{其它} \end{cases} \end{align*} $$

Ford 算法框架实现:

  • 若图上有边$(u,v)$,则$d_v$依赖$d_u$
  • 所有边反向,以$v$为起点,边做深度优先遍历 (posrorder),边松弛

必须是无环图,否则会出现死循环

最短路径 最优子结构

设路径$p = \langle v_0, v_1, \ldots, v_k \rangle$是从$v_0$$v_k$的一条最短路径,则其任意子路径$\langle v_i, v_{i+1}, \ldots, v_j \rangle$是从$v_i$$v_j$的最短路径

此时路径$p$分为三部分,$v_0 \overset{p_1}{\rightsquigarrow} v_i \overset{p_2}{\rightsquigarrow} v_j \overset{p_3}{\rightsquigarrow} v_k$

$p_2$不是最短路径,设$p'_2$更短,则$v_0 \overset{p_1}{\rightsquigarrow} v_i \overset{p'_2}{\rightsquigarrow} v_j \overset{p_3}{\rightsquigarrow} v_k$也更短


源点到不同点的最短路径显然存在公共的子路径

最优化问题 + 最优子结构 + 公共子问题 ?

最短路径 动态规划

如何定义子问题并确定其递推关系?若采用自底向上,何为底?

以有向边确定子问题间的依赖?环会导致死循环

最短路径的边数有上界,不超过$|\Vcal|-1$

$$ \begin{align*} \qquad \qquad \begin{matrix} \text{经过不超过 }|\Vcal|-1\text{ 条边的最短路径的长度} \\ \vdots \\ \text{经过不超过 }2\text{ 条边的最短路径的长度} \\ \text{经过不超过 }1\text{ 条边的最短路径的长度} \\ \end{matrix} \qquad \quad \Bigg \Uparrow \end{align*} $$

最短路径 递推关系式

$d_v^{(k)}$:从$s$$v$经过不超过$k$条边的最短路径的长度

  • 初始$k=0$$s$到其他点都不可达,$d_v^{(0)} = \infty$
  • $k \ge |\Vcal| - 1$$d_v^{(k)}$不再变化,因为最短路径的边数不超过$|\Vcal|-1$

直觉上允许经过的边越多,最短路径越短

当允许经过的边数从$k-1$条放宽至$k$

  • 没有改进,此时$d_v^{(k)} = d_v^{(k-1)}$
  • 有改进,经过$k-1$条边到点$u$后再到$v$$d_v^{(k)} = d_u^{(k-1)} + w(u,v)$

$$ \begin{align*} \quad d_v^{(k)} = \begin{cases} \infty, & k = 0 \\ \min \{ d_v^{(k-1)}, ~ \min_{u \ne v} \{ d_u^{(k-1)} + w(u,v) \} \}, & k \ge 1 \end{cases} \end{align*} $$

Bellman-Ford 填表

$$ \begin{align*} \quad d_v^{(k)} = \begin{cases} \infty, & k = 0 \\ \min \{ d_v^{(k-1)}, ~ \min_{u \ne v} \{ d_u^{(k-1)} + w(u,v) \} \}, & k \ge 1 \end{cases} \end{align*} $$

最终$d_u^{(|\Vcal|-1)} = \delta(s,v)$就是源点$s$$v$的最短路径的长度

def bellman_ford(s):
    d, p = dict(), dict()
    for v in g:  # 距离初始化为无穷大 前驱初始化为空
        d[v], p[v] = float("inf"), None
    d[s] = 0  # 源点到自己的最短路径长度为零

    for _ in range(len(g) - 1):  # 遍历|V|-1次
        for u in g:
            for v in g[u]:  # 内部的二重for循环遍历所有边
                if d[v] > d[u] + g[u][v]:           # 松弛
                    d[v], p[v] = d[u] + g[u][v], u  # 更新当前最短距离和前驱

内层二重 for 循环其实是遍历所有边,时间复杂度$\Theta(|\Vcal||\Ecal|)$

Bellman-Ford 例子

$\quad \small |\Vcal|=5, ~ d_v^{(k)} = \begin{cases} \infty, & k = 0 \\ \min \{ d_v^{(k-1)}, ~ \min_{u \ne v} \{ d_u^{(k-1)} + w(u,v) \} \}, & k \ge 1 \end{cases}$

Bellman-Ford 负环检测

对任意边$(u,v)$应有三角不等式$\delta(s,v) \le \delta(s,u) + w(u,v)$

假如图中有从源点可达的负环$v_i, v_{i+1}, \ldots, v_{j-1}, v_j$,其中$v_j = v_i$

$$ \begin{align*} \quad 0 & = \sum_{k=i}^{j-1} \delta(s,v_k) - \sum_{k=i}^{j-1} \delta(s,v_{k+1}) \overset{\text{三角不等式}}{\le} ~ \sum_{k=i}^{j-1} w(v_k,v_{k+1}) \overset{\text{负环}}{<} 0 \end{align*} $$

故图中若有负环,三角不等式不可能成立

负环检测:在求完所有最短路径后,对所有边检测一遍三角不等式,时间复杂度$\Theta(|\Ecal|)$

Bellman-Ford 实现

def bellman_ford(g, s):
    d, p = dict(), dict()
    for v in g:  # 距离初始化为无穷大 前驱初始化为空
        d[v], p[v] = float("inf"), None
    d[s] = 0  # 源点到自己的最短路径长度为零
    print(0, d)
    for i in range(len(g) - 1):  # 遍历|V|-1次
        dd = {key: value for (key, value) in d.items()}  # 备份上一轮的d
        for u in g:
            for v in g[u]:  # 内部的二重for循环遍历所有边
                if d[v] > dd[u] + g[u][v]:           # 松弛
                    d[v], p[v] = dd[u] + g[u][v], u  # 更新当前最短距离和前驱
        print(i+1, d)

    for u in g:
        for v in g[u]:
            assert (d[v] <= d[u] + g[u][v]), "有负环"

    return d, p


g = {                                # 用集合表示有向图g 元素为字典
    "s": {"t": 6, "y": 7},           # w(s,t) = 6, w(s,y) = 7
    "t": {"x": 5, "z": -4, "y": 8},  # w(t,x) = 5, w(t,z) = -4, w(t,y) = 8
    "y": {"z": 9, "x": -3},          # w(y,z) = 9, w(y,x) = -3
    "z": {"x": 7, "s": 2},           # w(z,x) = 7, w(z,s) = 2
    "x": {"t": -2},                  # w(x,t) = -2
}

d, p = bellman_ford(g, s="s")
# ---------------------------------------------------
# 0 {'s': 0, 't': inf, 'y': inf, 'z': inf, 'x': inf}
# 1 {'s': 0, 't': 6, 'y': 7, 'z': inf, 'x': inf}
# 2 {'s': 0, 't': 6, 'y': 7, 'z': 2, 'x': 4}
# 3 {'s': 0, 't': 2, 'y': 7, 'z': 2, 'x': 4}
# 4 {'s': 0, 't': 2, 'y': 7, 'z': -2, 'x': 4}
Bellman-Ford 说明

Q1:外层循环一定要迭代$|\Vcal| - 1$次吗?

A:不是,当相邻两轮的$d[]$表不再有更新时即可停止

Q2:迭代多少轮$d[]$才会不再有更新呢?

A:若所有点的最短路径的边数最大为$k$,则需迭代$k$

证明:设$s \rightsquigarrow v$的最短路径上$v$的前驱是$u$,即$s \rightsquigarrow u \rightarrow v$

若某轮$d_u$更新为$\delta (s,u)$,则下一轮$d_v = \delta (s,u) + w(u,v) = \delta (s,v)$

初始$d_s = \delta (s,s) = 0$,第一轮后最短路径恰为 1 条边的点就对了

依此类推,第$i$轮后最短路径恰为$i$条边的点就对了

最短路径最长为$|\Vcal| - 1$,最坏情况下需迭代$|\Vcal| - 1$

Bellman-Ford 说明

def bellman_ford(g, s):
    d, p = dict(), dict()
    for v in g:  # 距离初始化为无穷大 前驱初始化为空
        d[v], p[v] = float("inf"), None
    d[s] = 0  # 源点到自己的最短路径长度为零
    print(0, d)
    for i in range(len(g) - 1):  # 遍历|V|-1次
        dd = {key: value for (key, value) in d.items()}  # 备份上一轮的d
        for u in g:
            for v in g[u]:  # 内部的二重for循环遍历所有边
                if d[v] > dd[u] + g[u][v]:           # 松弛
                    d[v], p[v] = dd[u] + g[u][v], u  # 更新当前最短距离和前驱
        print(i+1, d)

    for u in g:
        for v in g[u]:
            assert (d[v] <= d[u] + g[u][v]), "有负环"

    return d, p


g = {  # s -> x -> y -> z -> t
    "s": {"x": 1},  # w(s,x) = 1
    "x": {"y": 1},  # w(x,y) = 1
    "y": {"z": 1},  # w(y,z) = 1
    "z": {"t": 1},  # w(z,t) = 1
    "t": {}
}

d, p = bellman_ford(g, s="s")
# ---------------------------------------------------
# 0 {'s': 0, 'x': inf, 'y': inf, 'z': inf, 't': inf}
# 1 {'s': 0, 'x': 1, 'y': inf, 'z': inf, 't': inf}
# 2 {'s': 0, 'x': 1, 'y': 2, 'z': inf, 't': inf}
# 3 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': inf}
# 4 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 4}

g = {  # s -> x -> y -> {z, t}
    "s": {"x": 1},          # w(s,x) = 1
    "x": {"y": 1},          # w(x,y) = 1
    "y": {"z": 1, "t": 1},  # w(y,z) = 1, w(y,t) = 1
    "z": {},
    "t": {}
}

d, p = bellman_ford(g, s="s")
# ---------------------------------------------------
# 0 {'s': 0, 'x': inf, 'y': inf, 'z': inf, 't': inf}
# 1 {'s': 0, 'x': 1, 'y': inf, 'z': inf, 't': inf}
# 2 {'s': 0, 'x': 1, 'y': 2, 'z': inf, 't': inf}
# 3 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 3}
# 4 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 3}

g = {  # s -> {x, y}, y -> {z, t}
    "s": {"x": 1, "y": 1},  # w(s,x) = 1, w(s,y) = 1
    "x": {},
    "y": {"z": 1, "t": 1},  # w(y,z) = 1, w(y,t) = 1
    "z": {},
    "t": {}
}

d, p = bellman_ford(g, s="s")
# ---------------------------------------------------
# 0 {'s': 0, 'x': inf, 'y': inf, 'z': inf, 't': inf}
# 1 {'s': 0, 'x': 1, 'y': 1, 'z': inf, 't': inf}
# 2 {'s': 0, 'x': 1, 'y': 1, 'z': 2, 't': 2}
# 3 {'s': 0, 'x': 1, 'y': 1, 'z': 2, 't': 2}
# 4 {'s': 0, 'x': 1, 'y': 1, 'z': 2, 't': 2}
Bellman-Ford 说明

《算法导论》上 Bellman-Ford 算法没要求备份上一轮的$d[]$

$$ \begin{align*} \quad d_v = \begin{cases} \infty, & k = 0 \\ \min \{ d_v, ~ \min_{u \ne v} \{ d_u + w(u,v) \} \}, & k \ge 1 \end{cases} \end{align*} $$

区别:如果本轮$d_u$更新了

  • $d_v$也可以紧接着更新,减少迭代次数,不过算法不能再叫动态规划了
  • 而在动态规划的实现中,$d_v$值必须到下一轮才能更新,因为本轮用的还是上一轮的$d_u$

任意点的$d$值单调递减,最新的就是坠吼的,用坠吼的没毛病!

但是当采用即时更新时,点的更新顺序会产生影响

Bellman-Ford 说明

def bellman_ford_dp(g, s):
    d = dict()
    for v in g:  # 距离初始化为无穷大 前驱初始化为空
        d[v] = float("inf")
    d[s] = 0  # 源点到自己的最短路径长度为零
    print(0, d)
    for i in range(len(g) - 1):  # 遍历|V|-1次
        dd = {key: value for (key, value) in d.items()}  # 备份上一轮的d
        for u in g:
            for v in g[u]:  # 内部的二重for循环遍历所有边
                d[v] = min(d[v], dd[u] + g[u][v])  # 松弛
        print(i+1, d)
    return d


def bellman_ford(g, s):
    d = dict()
    for v in g:  # 距离初始化为无穷大 前驱初始化为空
        d[v] = float("inf")
    d[s] = 0  # 源点到自己的最短路径长度为零
    print(0, d)
    for i in range(len(g) - 1):  # 遍历|V|-1次
        for u in g:
            for v in g[u]:  # 内部的二重for循环遍历所有边
                d[v] = min(d[v], d[u] + g[u][v])  # 松弛
        print(i+1, d)
    return d


g = {  # 顺序遍历所有点 s -> x -> y -> z -> t
    "s": {"x": 1},  # w(s,x) = 1
    "x": {"y": 1},  # w(x,y) = 1
    "y": {"z": 1},  # w(y,z) = 1
    "z": {"t": 1},  # w(z,t) = 1
    "t": {}
}

print("顺序 使用上一轮的d")
d = bellman_ford_dp(g, s="s")
# ---------------------------------------------------
# 顺序 使用上一轮的d
# 0 {'s': 0, 'x': inf, 'y': inf, 'z': inf, 't': inf}
# 1 {'s': 0, 'x': 1, 'y': inf, 'z': inf, 't': inf}
# 2 {'s': 0, 'x': 1, 'y': 2, 'z': inf, 't': inf}
# 3 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': inf}
# 4 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 4}

print("顺序 使用即时的d")
d = bellman_ford(g, s="s")
# ---------------------------------------------------
# 顺序 使用即时的d
# 0 {'s': 0, 'x': inf, 'y': inf, 'z': inf, 't': inf}
# 1 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 4}
# 2 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 4}
# 3 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 4}
# 4 {'s': 0, 'x': 1, 'y': 2, 'z': 3, 't': 4}

g = {  # 逆序遍历所有点 t -> z -> y -> x -> s
    "t": {},
    "z": {"t": 1},  # w(z,t) = 1
    "y": {"z": 1},  # w(y,z) = 1
    "x": {"y": 1},  # w(x,y) = 1
    "s": {"x": 1},  # w(s,x) = 1
}

print("逆序 使用上一轮的d")
d = bellman_ford_dp(g, s="s")
# ---------------------------------------------------
# 逆序 使用上一轮的d
# 0 {'t': inf, 'z': inf, 'y': inf, 'x': inf, 's': 0}
# 1 {'t': inf, 'z': inf, 'y': inf, 'x': 1, 's': 0}
# 2 {'t': inf, 'z': inf, 'y': 2, 'x': 1, 's': 0}
# 3 {'t': inf, 'z': 3, 'y': 2, 'x': 1, 's': 0}
# 4 {'t': 4, 'z': 3, 'y': 2, 'x': 1, 's': 0}

print("逆序 使用即时的d")
d = bellman_ford(g, s="s")
# ---------------------------------------------------
# 逆序 使用即时的d
# 0 {'t': inf, 'z': inf, 'y': inf, 'x': inf, 's': 0}
# 1 {'t': inf, 'z': inf, 'y': inf, 'x': 1, 's': 0}
# 2 {'t': inf, 'z': inf, 'y': 2, 'x': 1, 's': 0}
# 3 {'t': inf, 'z': 3, 'y': 2, 'x': 1, 's': 0}
# 4 {'t': 4, 'z': 3, 'y': 2, 'x': 1, 's': 0}
Bellman-Ford 小结

设所有点的最短路径边数最长的为$k$

动态规划 非动态规划
使用$d[]$ 上一轮 即时
点的更新顺序 没影响 有影响
最好情况 $k$ $1$轮,点的更新顺序恰是最短路径上的顺序
最坏情况 $k$ $k$轮,点的更新顺序恰是最短路径上的逆序
最短路径 贪心加速?

$s$出发只有$t$$y$一步能到,其它点至少需经过其中某个点

注意$w(s,t) = 6 < 7 = w(s,y)$,若做贪心选择,应该先选择$t$

但实际却是从$y$$x$中转

$y$出发后续有负权重的边可以再减少路径长度

若想贪心,不能有负权重的边

Dijkstra 算法

假设图中不再有负权重的边来减少路径长度

引入已确定最短路径的顶点集合$\Scal$,初始为空

贪心选择:从$\Vcal \setminus \Scal$中选择最短路径估计值最小的结点加入$\Scal$

$d_s = 0$$d_y = d_t = d_x = d_z = \infty$

$s$加入$\Scal$$\Scal = \{ s \}$

Dijkstra 算法

$\Scal = \{ s \}$,根据$\delta(s,s) = 0$更新最短路径估计值

  • $d_t = \infty \rightarrow d_s = \delta(s,s) + w(s,t) = 0 + 10 = 10$
  • $d_y = \infty \rightarrow d_y = \delta(s,s) + w(s,y) = 0 + 5 = 5$
  • $d_x = d_z = \infty$

$y$加入$\Scal$$\Scal = \{ s, y \}$

Dijkstra 算法

$\Scal = \{ s, y \}$,根据$\delta(s,y) = 5 ~ (?)$更新最短路径估计值

  • $d_t = 10 \rightarrow d_t = \delta(s,y) + w(y,t) = 5 + 3 = 8$
  • $d_x = \infty \rightarrow d_x = \delta(s,y) + w(y,x) = 5 + 9 = 14$
  • $d_z = \infty \rightarrow d_z = \delta(s,y) + w(y,z) = 5 + 2 = 7$

$z$加入$\Scal$$\Scal = \{ s, y , z \}$,如此迭代下去

Dijkstra 例子

Dijkstra 实现

from queue import PriorityQueue


def dijkstra(s):
    q, in_q, d, p = PriorityQueue(), dict(), dict(), dict()

    for v in g:  # 距离初始化为无穷大 前驱初始化为空
        in_q[v], d[v], p[v] = False, float("inf"), None

    in_q["s"], d["s"] = True, 0
    q.put([0, "s"])  # 源点入队

    while not q.empty():
        _, u = q.get()  # 获取队首元素点u
        in_q[u] = False  # 更新点u的在队状态
        for v in g[u]:  # 更新u指向的点的最短路径
            if d[v] > d[u] + g[u][v]:  # 边(u,v)可以松弛
                if in_q[v]:
                    q.queue.remove([d[v], v])
                d[v], p[v] = d[u] + g[u][v], u  # 更新最短距离和前驱
                q.put([d[v], v])
                in_q[v] = True

    return d, p


g = {
    "s": {"t": 10, "y": 5},  # w(s,t) = 6, w(s,y) = 5
    "t": {"x": 1, "y": 2},  # w(t,x) = 1, w(t,y) = 2
    "y": {"t": 3, "z": 2, "x": 9},  # w(y,t) = 3, w(y,z) = 2, w(y,x) = 9
    "z": {"s": 7, "x": 6},  # w(z,s) = 7, w(z,x) = 6
    "x": {"z": 4},  # w(x,z) = 4
}

d, p = dijkstra("s")
print(d)
print(p)
# ---------------------------------------------------
# {'s': 0, 't': 8, 'y': 5, 'z': 7, 'x': 9}
# {'s': None, 't': 'y', 'y': 's', 'z': 'y', 'x': 't'}

g = {  # dijkstra也是可以处理带负边的图的
    "s": {"t": 6, "y": 7},  # w(s,t) = 6, w(s,y) = 7
    "t": {"x": 5, "z": -4, "y": 8},  # w(t,x) = 5, w(t,z) = -4, w(t,y) = 8
    "y": {"z": 9, "x": -3},  # w(y,z) = 9, w(y,x) = -3
    "z": {"x": 7, "s": 2},  # w(z,x) = 7, w(z,s) = 2
    "x": {"t": -2},  # w(x,t) = -2
}

d, p = dijkstra("s")
print(d)
print(p)
# ---------------------------------------------------
# {'s': 0, 't': 2, 'y': 7, 'z': -2, 'x': 4}
# {'s': None, 't': 'x', 'y': 's', 'z': 't', 'x': 'y'}

Dijkstra 正确性

不变式:在外层 for 循环每次执行前,对$\forall u \in \Scal$$d_u = \delta (s,u)$

  • 根据算法流程,点$u$一旦加入$\Scal$,就不会再修正$d_u$
  • 只需证明对$\forall u \in \Scal$,当其被加入到$\Scal$时有$d_u = \delta (s,u)$

初始$\Scal = \emptyset$,之后源点$s$第一个加入$\Scal$,显然$d_s = \delta (s,s) = 0$

$u$第一个加入$\Scal$$d_u \ne \delta (s,u)$的点

此时必存在$s$$u$的路径,否则$d_u = \delta (s,u) = \infty$,与假设矛盾

$s$$u$的最短路径为$p$

Dijkstra 正确性

下面说明$u$是路径$p$上第一个 (也是唯一一个) 不在$\Scal$中的点

否则设$u$前还有$y \not \in \Scal$,由于$d_y < d_u$,应该加入的是$y$不是$u$

$u$的前驱$x \in \Scal$$x$$u$之前加入$\Scal$,因此$d_x = \delta(s,x)$

$x$加入$\Scal$时会更新$x$指向的所有点的最短路径估计值

$d_u = d_x + w(x,u) = \delta(s,x) + w(x,u)$

根据最优子结构性,$d_u = \delta(s,u)$

单源最短路径 小结

方法 条件 实现 一般时间复杂度
Bellman-Ford 动态规划 可以有负边 $\Theta(\shu\Vcal\shu \shu\Ecal\shu)$
Dijkstra 贪心法 必须无负边 线性数组 $\Theta(\shu\Vcal\shu^2 + \shu\Ecal\shu)$
二叉堆 $\Theta((\shu\Vcal\shu + \shu\Ecal\shu) \lg \shu\Vcal\shu)$
斐波那契堆 $\Theta(\shu\Vcal\shu \lg \shu\Vcal\shu + \shu\Ecal\shu)$

Bellman-Ford 算法可检测负环,进一步考虑图的稀疏性有

实现 一般时间复杂度 稠密图 稀疏图
Bellman-Ford $\Theta(\shu\Vcal\shu \shu\Ecal\shu)$ $\Theta(\shu\Vcal\shu^3)$ $O(\shu\Vcal\shu^2)$
Dijkstra 线性数组 $\Theta(\shu\Vcal\shu^2 + \shu\Ecal\shu)$ $\Theta(\shu\Vcal\shu^2)$ $\Theta(\shu\Vcal\shu^2)$
二叉堆 $\Theta((\shu\Vcal\shu + \shu\Ecal\shu) \lg \shu\Vcal\shu)$ $\Theta(\shu\Vcal\shu^2 \lg \shu\Vcal\shu)$ $\Theta(\shu\Vcal\shu \lg \shu\Vcal\shu)$
斐波那契堆 $\Theta(\shu\Vcal\shu \lg \shu\Vcal\shu + \shu\Ecal\shu)$ $\Theta(\shu\Vcal\shu^2)$ $\Theta(\shu\Vcal\shu \lg \shu\Vcal\shu)$
差分约束系统

假设一个生产工序有$n$个步骤,在时刻$x_i$进行第$i$个步骤

步骤的执行时间会有一些约束

在时刻$x_i$使用一种需要$2$个小时才能风干的粘贴剂材料

下一个步骤需要$2$小时后等粘贴剂干了才能在时刻$x_{i+1}$安装

这样就有约束条件$x_i - x_{i+1} \le -2$

差分约束系统

把所有的约束条件写到一起就是差分约束系统

$$ \begin{align*} \quad \begin{cases} x_1 - x_2 \le 0 \\ x_1 - x_5 \le -1 \\ x_2 - x_5 \le 1 \\ x_3 - x_1 \le 5 \\ x_4 - x_1 \le 4 \\ x_4 - x_3 \le -1 \\ x_5 - x_3 \le -3 \\ x_5 - x_4 \le -3 \end{cases} \quad \Longleftrightarrow \quad \underbrace{\begin{bmatrix} 1 & -1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & -1 \\ 0 & 1 & 0 & 0 & -1 \\ -1 & 0 & 1 & 0 & 0 \\ -1 & 0 & 0 & 1 & 0 \\ 0 & 0 & -1 & 1 & 0 \\ 0 & 0 & -1 & 0 & 1 \\ 0 & 0 & 0 & -1 & 1 \end{bmatrix}}_{\Av ~ \in ~ \{ \pm 1, 0 \}^{m \times n}} \underbrace{\begin{bmatrix} x_1 \\ x_2 \\ x_3 \\ x_4 \\ x_5 \end{bmatrix}}_{\xv ~ \in ~ \Rbb^n} \le \underbrace{\begin{bmatrix} 0 \\ -1 \\ 1 \\ 5 \\ 4 \\ -1 \\ -3 \\ -3 \end{bmatrix}}_{\bv ~ \in ~ \Rbb^m} \end{align*} $$

满足$\Av \xv \le \bv$$\xv$称为差分约束系统的解

差分约束系统的解不唯一,若$\xv$是解,则$\xv + c \onev$也是解

约束图

给定差分约束系统$\Av \xv \le \bv$,其约束图是带权有向图$\Gcal = (\Vcal, \Ecal)$

  • $\Vcal = \{ v_0, v_1, \ldots, v_n \}$,每个$x_i$对应一个$v_i$,此外引入一个额外的$v_0$
  • $\Ecal = \{ (v_i, v_j) \mid x_j -x_i \le b_k \} \cup \{ (v_0, v_1), (v_0,v_2), \ldots, (v_0, v_n) \}$,每个约束对应一条边,权重为$b_k$$v_0$指向其它所有点的边权重为$0$

$$ \begin{align*} \qquad \begin{cases} x_1 - x_2 \le 0 \\ x_1 - x_5 \le -1 \\ x_2 - x_5 \le 1 \\ x_3 - x_1 \le 5 \\ x_4 - x_1 \le 4 \\ x_4 - x_3 \le -1 \\ x_5 - x_3 \le -3 \\ x_5 - x_4 \le -3 \end{cases} \quad \Longrightarrow \end{align*} $$

根据约束图求解

给定差分约束系统$\Av \xv \le \bv$,其约束图是带权有向图$\Gcal = (\Vcal, \Ecal)$

$\Gcal$包含负环,系统无解,否则有解$\xv = [\delta(v_0, v_1), \ldots, \delta(v_0, v_n)]$

设负环为$v_i, v_{i+1}, \ldots, v_{j-1}, v_j$,其中$v_j = v_i$,则

$$ \begin{align*} \quad (v_i, v_{i+1}) & \Longleftrightarrow x_{i+1} - x_i \le w(v_i, v_{i+1}) \\ (v_{i+1}, v_{i+2}) & \Longleftrightarrow x_{i+2} - x_{i+1} \le w(v_{i+1}, v_{i+2}) \\ & \quad \vdots \\ (v_{j-1}, v_j) & \Longleftrightarrow x_j - x_{j-1} \le w(v_{j-1}, v_j) \end{align*} $$

注意$x_j = x_i$,累加可得$0 \le \sum_{k=i}^{j-1} w(v_k,v_{k+1}) \overset{\text{负环}}{<} 0$

根据约束图求解

给定差分约束系统$\Av \xv \le \bv$,其约束图是带权有向图$\Gcal = (\Vcal, \Ecal)$

$\Gcal$包含负环,系统无解,否则有解$\xv = [\delta(v_0, v_1), \ldots, \delta(v_0, v_n)]$

$\forall (v_i, v_j) \in \Ecal$,对应约束$x_j - x_i \le w(v_i, v_j)$,根据三角不等式

$$ \begin{align*} \quad \delta(v_0, v_j) \le \delta(v_0, v_i) + w(v_i, v_j) \Longrightarrow \delta(v_0, v_j) - \delta(v_0, v_i) \le w(v_i, v_j) \end{align*} $$

$x_j = \delta(v_0, v_j)$$x_i = \delta(v_0, v_i)$即可

综上,可对其约束图以$v_0$为源点运行 Bellman-Ford 算法

  • 若检测到负环,则原差分约束系统无解
  • 若无负环,则$v_0$到其它点的最短路径就是原差分约束系统的一个解
根据约束图求解

$$ \begin{align*} \qquad \begin{cases} x_1 - x_2 \le 0 \\ x_1 - x_5 \le -1 \\ x_2 - x_5 \le 1 \\ x_3 - x_1 \le 5 \\ x_4 - x_1 \le 4 \\ x_4 - x_3 \le -1 \\ x_5 - x_3 \le -3 \\ x_5 - x_4 \le -3 \end{cases} \quad \Longrightarrow \end{align*} $$

$\xv = [-5, -3, 0, -1, -4]$就是一个解