武汉到北京的
给定带权有向图$\Gcal = (\Vcal, \Ecal, w)$
路径$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)
对任意点$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 算法框架:初始化所有点,若还有边可以松弛,松弛该边
若$\Gcal$是无权图,路径长度即为经过的边的数目
Ford 算法框架实现:
基于广度优先搜索 (BFS) 实现的时间复杂度为$O(|\Vcal| + |\Ecal|)$
def bfs(g, s): visted = [s] # 初始化已访问列表 q = [s] # 源点入队 while q: # 当队列非空时 u = q.pop(0) # 队首元素出队 for v in g[u]: # 对每条边(u,v) if v not in visted: visted.append(v) q.append(v) # 点v入队 return visted def init_sssp(g, s): dist, pred = dict(), dict() for v in g: dist[v] = float("inf") if v != s else 0 pred[v] = None return dist, pred def bfs_sssp(g, s): dist, pred = init_sssp(g, s) # 初始化距离和前驱 q = [s] # 源点入队 while q: # 当队列非空时 u = q.pop(0) # 队首元素出队 for v in g[u]: # 对每条边(u,v) if dist[v] > dist[u] + 1: # 若可以松弛 dist[v] = dist[u] + 1 # 更新距离 pred[v] = u # 更新前驱 q.append(v) # 点v入队 return dist, pred g = { "s": ["a", "b"], # a e "a": ["s", "c"], # / \ /| "b": ["s", "c", "d"], # / \ / | "c": ["a", "b", "d", "e"], # s c | "d": ["b", "c", "e"], # \ / \ | "e": ["c", "d"], # \ / \| } # b-----d visited = bfs(g, "s") print(visited) # -------------------------------------------- # ['s', 'a', 'b', 'c', 'd', 'e'] dist, pred = bfs_sssp(g, "s") print(dist) print(pred) # -------------------------------------------- # {'s': 0, 'a': 1, 'b': 1, 'c': 2, 'd': 2, 'e': 3} # {'s': None, 'a': 's', 'b': 's', 'c': 'a', 'd': 'b', 'e': 'c'}
问题归约:若$\Gcal$的所有边的权重为正整数,将权重为$l$的边分解成$l$个长度为$1$的边,得到无权图$\Gcal'$,对$\Gcal'$运行bfs_sssp算法
每个权重为$l$的边会引入$l-1$个新点、新边,设$W$为所有边的权重和,则$\Gcal'$会引入$W - |\Ecal|$个新点、新边
松弛
$$ \begin{align*} \quad d_v = \begin{cases} 0, & v = s \\ \min_u \{ d_u + w(u,v) \}, & \text{其它} \end{cases} \end{align*} $$
Ford 算法框架实现:
基于深度优先搜索 (DFS) 实现的时间复杂度为$O(|\Vcal| + |\Ecal|)$
def dfs(g, u): if u not in visited: visited.append(u) for v in g[u]: dfs(g, v) label.append(u) # 后序DFS可以保得到结点的逆拓扑序 g = { "s": {"a": 3, "b": 8, "c": 7}, "a": {"b": 6, "d": 12}, "b": {"c": -2, "d": 5, "e": 0}, "c": {"e": 10}, "d": {"e": 1, "f": -3}, "e": {"f": 1}, "f": {} } visited, label = [], [] dfs(g, "s") label.reverse() # 反转得到拓扑序 print(label) # ------------------------------------------- # ['s', 'a', 'b', 'd', 'c', 'e', 'f'] gg = {} # g记录每个点的出边 gg记录每个点的入边 for u in g: gg[u] = {} for u in g: for v in g[u]: gg[v][u] = g[u][v] dist, pred = {"s": 0}, {"s": None} for v in label: # 按拓扑序遍历求最短路径 if v != "s": dist[v] = float("inf") for u in gg[v]: if dist[v] > dist[u] + gg[v][u]: dist[v] = dist[u] + gg[v][u] pred[v] = u print(dist) print(pred) # ---------------------------------------------------------------------- # {'s': 0, 'a': 3, 'b': 8, 'd': 13, 'c': 6, 'e': 8, 'f': 9} # {'s': None, 'a': 's', 'b': 's', 'd': 'b', 'c': 'b', 'e': 'b', 'f': 'e'}
设路径$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-1$条放宽至$k$条
$$ \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*} $$
$$ \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|)$
$\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}$
对任意边$(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+1}) - \sum_{k=i}^{j-1} \delta(s,v_k) \overset{\text{三角不等式}}{\le} ~ \sum_{k=i}^{j-1} w(v_k,v_{k+1}) \overset{\text{负环}}{<} 0 \end{align*} $$
故图中若有负环,三角不等式不可能成立
负环检测:在求完所有最短路径后,对所有边检测一遍三角不等式,时间复杂度$\Theta(|\Ecal|)$
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}
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$轮
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 算法没要求备份上一轮的$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$值单调递减,最新的就是最好的,用最好的没毛病!
但是当采用即时更新时,点的更新顺序会产生影响
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}
设所有点的最短路径边数最长的为$k$
| 动态规划 | 非动态规划 | |
|---|---|---|
| 使用$d[]$表 | 上一轮 | 即时 |
| 点的更新顺序 | 没影响 | 有影响 |
| 最好情况 | $k$轮 | $1$轮,点的更新顺序恰是最短路径上的顺序 |
| 最坏情况 | $k$轮 | $k$轮,点的更新顺序恰是最短路径上的逆序 |
从$s$出发只有$t$和$y$一步能到,其它点至少需经过其中某个点
注意$w(s,t) = 6 < 7 = w(s,y)$,若做贪心选择,应该先选择$t$
但实际却是从$y$、$x$中转
从$y$出发后续有负权重的边可以再减少路径长度
若想贪心,不能有负权重的边
假设图中不再有负权重的边来减少路径长度
引入已确定最短路径的顶点集合$\Scal$,初始为空
贪心选择:从$\Vcal \setminus \Scal$中选择最短路径估计值最小的结点加入$\Scal$
$d_s = 0$、$d_y = d_t = d_x = d_z = \infty$
将$s$加入$\Scal$,$\Scal = \{ s \}$
$\Scal = \{ s \}$,根据$\delta(s,s) = 0$更新最短路径估计值
将$y$加入$\Scal$,$\Scal = \{ s, y \}$
$\Scal = \{ s, y \}$,根据$\delta(s,y) = 5 ~ (?)$更新最短路径估计值
将$z$加入$\Scal$,$\Scal = \{ s, y , z \}$,如此迭代下去
# python自带的二叉堆库heapq不支持decrease-key操作 # 采用第三方库heapdict https://pypi.org/project/HeapDict/ import heapdict def init_sssp(g, s): dist, pred = dict(), dict() for v in g: dist[v] = float("inf") if v != s else 0 pred[v] = None return dist, pred def dijkstra_no_negative(g, s): dist, pred = init_sssp(g, s) # 初始化距离和前驱 h, in_S = heapdict.heapdict(), dict() for v in g: h[v] = float("inf") if v != s else 0 in_S[v] = False iter = 0 while h: iter += 1 print(f"第{iter}轮") u, d = h.popitem() # 弹出最小元 dist[u] = d # 此时u的值已经是最短路径 in_S[u] = True print(f"({u},{d})出堆,{s}到{u}的最短路径长度确定为{d}") for v in g[u]: # 遍历u的所有出边(u,v) if not in_S[v] and h[v] > dist[u] + g[u][v]: print(f"放松边({u}->{v}),{s}到{v}的最短路径:{h[v]} => {dist[u] + g[u][v]}") h[v] = d + g[u][v] pred[v] = u print() return dist, pred 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 } dist, pred = dijkstra_no_negative(g, "s") print(dist) print(pred) # -------------------------------------- # 第1轮 # (s,0)出堆,s到s的最短路径长度确定为0 # 放松边(s->t),s到t的最短路径:inf => 10 # 放松边(s->y),s到y的最短路径:inf => 5 # # 第2轮 # (y,5)出堆,s到y的最短路径长度确定为5 # 放松边(y->t),s到t的最短路径:10 => 8 # 放松边(y->z),s到z的最短路径:inf => 7 # 放松边(y->x),s到x的最短路径:inf => 14 # # 第3轮 # (z,7)出堆,s到z的最短路径长度确定为7 # 放松边(z->x),s到x的最短路径:14 => 13 # # 第4轮 # (t,8)出堆,s到t的最短路径长度确定为8 # 放松边(t->x),s到x的最短路径:13 => 9 # # 第5轮 # (x,9)出堆,s到x的最短路径长度确定为9 # # {'s': 0, 't': 8, 'y': 5, 'z': 7, 'x': 9} # {'s': None, 't': 'y', 'y': 's', 'z': 'y', 'x': 't'} # dijkstra_no_negative处理带负边的图的会输出错误的结果 # 下图为Bellman-Ford例子中的图 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 } dist, pred = dijkstra_no_negative(g, "s") print(dist) print(pred) # -------------------------------------- # 第1轮 # (s,0)出堆,s到s的最短路径长度确定为0 # 放松边(s->t),s到t的最短路径:inf => 6 # 放松边(s->y),s到y的最短路径:inf => 7 # # 第2轮 # (t,6)出堆,s到t的最短路径长度确定为6 # 放松边(t->x),s到x的最短路径:inf => 11 # 放松边(t->z),s到z的最短路径:inf => 2 # # 第3轮 # (z,2)出堆,s到z的最短路径长度确定为2 # 放松边(z->x),s到x的最短路径:11 => 9 # # 第4轮 # (y,7)出堆,s到y的最短路径长度确定为7 # 放松边(y->x),s到x的最短路径:9 => 4 # # 第5轮 # (x,4)出堆,s到x的最短路径长度确定为4 # # {'s': 0, 't': 6, 'y': 7, 'z': 2, 'x': 4} # {'s': None, 't': 's', 'y': 's', 'z': 't', 'x': 'y'}
不变式:在外层 for 循环每次执行前,对$\forall u \in \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$
下面说明$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)$
若允许每个点多次出堆、入堆,对有负边的图也是可以得到最短路径的,但时间复杂度没有保证,最坏情况下为指数复杂度
# python自带的二叉堆库heapq不支持decrease-key操作 # 采用第三方库heapdict https://pypi.org/project/HeapDict/ import heapdict def init_sssp(g, s): dist, pred = dict(), dict() for v in g: dist[v] = float("inf") if v != s else 0 pred[v] = None return dist, pred def dijkstra(g, s): dist, pred = init_sssp(g, s) # 初始化距离和前驱 h = heapdict.heapdict() h[s] = 0 # 将源点加入二叉堆 # 有负边时堆首弹出的点的最短路径不一定正确 故无需集合S # 引入in_heap判断某个点是否在堆中 in_heap = dict() for v in g: in_heap[v] = False if v != s else True iter = 0 while h: iter += 1 print(f"第{iter}轮") u, d = h.popitem() # 弹出最小元 print(f"({u},{d})出堆") in_heap[u] = False for v in g[u]: # 遍历u的所有出边(u,v) if dist[v] > dist[u] + g[u][v]: # 边(u,v)可以松弛 print(f"放松边({u}->{v}),{s}到{v}的最短路径:{dist[v]} => {dist[u] + g[u][v]}") # 边(u,v)松弛后 v出发的边或许可以继续松弛 故v入堆 if not in_heap[v]: in_heap[v] = True h[v] = dist[v] = dist[u] + g[u][v] print(f"({v},{h[v]})入堆") else: print(f"更新:({v},{h[v]}) => ({v},{dist[u] + g[u][v]})") h[v] = dist[v] = dist[u] + g[u][v] pred[v] = u print() return dist, pred # 下图为Bellman-Ford例子中的图 dijkstra_no_negative无法得到正确结果 dijkstra可以得到正确结果 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 } dist, pred = dijkstra(g, "s") print(dist) print(pred) # -------------------------------------- # 第1轮 # (s,0)出堆 # 放松边(s->t),s到t的最短路径:inf => 6 # (t,6)入堆 # 放松边(s->y),s到y的最短路径:inf => 7 # (y,7)入堆 # # 第2轮 # (t,6)出堆 # 放松边(t->x),s到x的最短路径:inf => 11 # (x,11)入堆 # 放松边(t->z),s到z的最短路径:inf => 2 # (z,2)入堆 # # 第3轮 # (z,2)出堆 # 放松边(z->x),s到x的最短路径:11 => 9 # 更新:(x,11) => (x,9) # # 第4轮 # (y,7)出堆 # 放松边(y->x),s到x的最短路径:9 => 4 # 更新:(x,9) => (x,4) # # 第5轮 # (x,4)出堆 # 放松边(x->t),s到t的最短路径:6 => 2 # (t,2)入堆 # # 第6轮 # (t,2)出堆 # 放松边(t->z),s到z的最短路径:2 => -2 # (z,-2)入堆 # # 第7轮 # (z,-2)出堆 # # {'s': 0, 't': 2, 'y': 7, 'z': -2, 'x': 4} # {'s': None, 't': 'x', 'y': 's', 'z': 't', 'x': 'y'}
| | 方法 | 条件 | 实现 | 一般时间复杂度 |
|---|---|---|---|---|
| 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)$ | $\Theta(\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)$
$$ \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 算法
$$ \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]$就是一个解