4.2 Floyd-Warshall算法的核心思想
Floyd-Warshall算法利用动态规划的思想,通过迭代更新距离矩阵来逐步逼近所有节点对之间的最短路径,其核心思想是考虑中间节点,利用子问题的最优解来求解整个问题的最优解。
4.2.1 基本思想
Floyd-Warshall算法的基本思想是动态规划,可以将其基本思想归纳为如下所示的几点。
- 利用中间节点:Floyd-Warshall算法通过考虑中间节点来求解所有节点对之间的最短路径。对于每一对节点(i, j),算法尝试通过其他节点k来确定是否存在一条路径比当前已知的路径更短。
- 迭代更新距离矩阵:算法通过迭代地更新距离矩阵来逐步逼近所有节点对之间的最短路径。在每一轮迭代中,对于每一对节点(i, j),算法尝试通过节点k来更新节点i到节点j的距离,如果存在一条路径经过节点k比当前已知的路径更短,则更新距离矩阵中对应的距离值。
- 动态规划转移方程:算法通过动态规划的思想,利用子问题的最优解来求解整个问题的最优解。具体来说,更新节点i到节点j的距离时,算法考虑从节点i到节点j的直接路径以及通过中间节点k的路径,并选择其中距离最短的路径作为节点i到节点j的最短路径。
- 最终结果:经过多轮迭代后,距离矩阵中的值将会收敛到所有节点对之间的最短路径长度。最终,距离矩阵中的值将反映出所有节点之间的最短路径长度。
4.2.2 图的表示方法
在Floyd-Warshall算法中,通常使用邻接矩阵来表示图。邻接矩阵是一个二维数组,其中的元素表示图中节点之间的连接关系和权重。对于有向图,矩阵的(i, j)元素表示从节点i到节点j的边的权重;对于无向图,则可以在(i, j)和(j, i)位置都标记上边的权重。
举例来说,如果有一个带权有向图,其邻接矩阵可能如下所示:
| 1 2 3 4
----------------------------------------------------
1 | 0 3 ∞ 7
2 | ∞ 0 2 ∞
3 | 8 ∞ 0 1
4 | ∞ ∞ ∞ 0
在这个矩阵中,如果存在一条从节点i到节点j的边,则对应的矩阵元素(i, j)的值为该边的权重;如果不存在这样的边,则矩阵元素的值为∞。
4.2.3 Floyd-Warshall算法的实现步骤
Floyd-Warshall算法是一种动态规划算法,用于解决所有节点对之间的最短路径问题。其原理和实现步骤如下所示。
(1)初始化距离矩阵:创建一个二维数组dist来存储任意两个节点之间的最短路径长度。如果节点i到节点j之间没有直接连接的边,则dist[i][j]的值为无穷大(INF);如果存在直接连接的边,则dist[i][j]的值为该边的权重。
(2)动态规划更新距离矩阵:对于每一个节点k,遍历所有节点对(i, j),检查是否存在一条从节点i经过节点k到节点j的路径比已知的路径更短。如果存在这样的路径,则更新dist[i][j]为更短的路径长度dist[i][k] + dist[k][j]。
(3)重复执行步骤(2):重复执行步骤(2)直到所有节点对之间的最短路径长度都已确定。即对每个节点k,都进行一次更新操作。
(4)检查负权环路:最后,检查距离矩阵中对角线上的元素。如果任何一个对角线上的元素为负值,则表示存在负权环路,因为从一个节点到自身的最短路径长度应该为0,而负值表示存在更短的路径。
(5)输出结果:最终,dist矩阵中的值即为所有节点对之间的最短路径长度。
例如下面是Floyd-Warshall算法的伪代码,其中,graph表示输入的图,weight(u,v)表示从节点u到节点v的权重,dist表示距离矩阵,|V|表示图中节点的数量。
procedure FloydWarshall(graph)
// 初始化距离矩阵
for each vertex v
dist[v][v] ← 0
for each edge (u,v) in graph
dist[u][v] ← weight(u,v)
// 动态规划更新距离矩阵
for k from 1 to |V|
for i from 1 to |V|
for j from 1 to |V|
if dist[i][k] + dist[k][j] < dist[i][j]
dist[i][j] ← dist[i][k] + dist[k][j]
// 检查负权环路
for each vertex v
if dist[v][v] < 0
return "Graph contains a negative-weight cycle"
return dist
4.2.4 Floyd-Warshall算法的推导过程
假设我们有一个带权有向图,其中包含了4个节点(A、B、C、D)和对应的边的权重。现在将这个有向图转换为邻接矩阵表示,用∞表示不可达的情况:
A B C D
-------------------------
A | 0 2 ∞ 1
B | ∞ 0 3 ∞
C | ∞ ∞ 0 1
D | ∞ ∞ ∞ 0
接下来,我们将使用Floyd-Warshall算法的迭代步骤,更新距离矩阵中的值。具体迭代的过程如下所示。
(1)第1轮迭代结果如下所示:
A B C D
------------------------------
A | 0 2 ∞ 1
B | ∞ 0 3 ∞
C | ∞ ∞ 0 1
D | ∞ ∞ ∞ 0
以节点A作为中间节点:
dist(B, C) = min(dist(B, C), dist(B, A) + dist(A, C)) = min(∞, ∞ + ∞) = ∞
dist(B, D) = min(dist(B, D), dist(B, A) + dist(A, D)) = min(∞, ∞ + 1) = ∞
dist(C, B) = min(dist(C, B), dist(C, A) + dist(A, B)) = min(∞, ∞ + 2) = ∞
dist(C, D) = min(dist(C, D), dist(C, A) + dist(A, D)) = min(∞, ∞ + 1) = ∞
dist(D, B) = min(dist(D, B), dist(D, A) + dist(A, B)) = min(∞, ∞ + 2) = ∞
dist(D, C) = min(dist(D, C), dist(D, A) + dist(A, C)) = min(∞, ∞ + ∞) = ∞
(2)第2轮迭代结果如下所示:
A B C D
-----------------------------------
A | 0 2 ∞ 1
B | ∞ 0 3 ∞
C | ∞ ∞ 0 1
D | ∞ ∞ ∞ 0
以节点B作为中间节点:
dist(C, A) = min(dist(C, A), dist(C, B) + dist(B, A)) = min(∞, 3 + 2) = 5
dist(C, D) = min(dist(C, D), dist(C, B) + dist(B, D)) = min(∞, 3 + ∞) = ∞
dist(D, A) = min(dist(D, A), dist(D, B) + dist(B, A)) = min(∞, ∞ + 2) = ∞
dist(D, C) = min(dist(D, C), dist(D, B) + dist(B, C)) = min(∞, ∞ + 1) = ∞
(3)第3轮迭代结果如下所示:
A B C D
--------------------------------
A | 0 2 ∞ 1
B | ∞ 0 3 ∞
C | ∞ 5 0 1
D | ∞ ∞ ∞ 0
以节点C作为中间节点:
dist(D, A) = min(dist(D, A), dist(D, C) + dist(C, A)) = min(∞, 1 + 5) = 6
dist(D, B) = min(dist(D, B), dist(D, C) + dist(C, B)) = min(∞, 1 + 3) = 4
(4)第4轮迭代结果如下所示:
A B C D
------------------------------------
A | 0 2 6 1
B | ∞ 0 3 4
C | ∞ 5 0 1
D | ∞ ∞ ∞ 0
最终得到的距离矩阵即为所有节点之间的最短路径长度。例如,从节点A到节点D的最短路径长度为1,从节点B到节点C的最短路径长度为3。
例如在下面的例子中,定义了函数 floyd_warshall,它接受一个图的邻接矩阵作为输入,并返回一个包含每对顶点之间的最短距离的矩阵。然后,定义了一个示例图形 graph,并使用 floyd_warshall 函数计算了最短距离。最后,打印出输出每对顶点之间的最短距离。
实例4-1:使用Floyd-Warshall算法计算最短距离(codes/4/fw.py)
实例文件fw.py的具体实现代码如下所示。
INF = float('inf')
def floyd_warshall(graph):
n = len(graph)
dist = [[0 if i == j else graph[i][j] if graph[i][j] != 0 else INF for j in range(n)] for i in range(n)]
for k in range(n):
for i in range(n):
for j in range(n):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
# Example graph represented as adjacency matrix
graph = [
[0, 5, INF, 10],
[INF, 0, 3, INF],
[INF, INF, 0, 1],
[INF, INF, INF, 0]
]
shortest_distances = floyd_warshall(graph)
# Output shortest distances
print("Shortest distances between every pair of vertices:")
for row in shortest_distances:
print(row)
执行后会输出:
Shortest distances between every pair of vertices:
Shortest distances between every pair of vertices:
[0, 5, 8, 9]
[inf, 0, 3, 4]
[inf, inf, 0, 1]
[inf, inf, inf, 0]
在上面的输出结果中显示了从每个顶点到其他顶点的最短距离,具体说明如下所示:
- 顶点 A 到其他顶点的最短距离为 [0, 5, 8, 9]。
- 顶点 B 到其他顶点的最短距离为 [inf, 0, 3, 4]。
- 顶点 C 到其他顶点的最短距离为 [inf, inf, 0, 1]。
- 顶点 D 到其他顶点的最短距离为 [inf, inf, inf, 0]。
其中,inf 表示不可达的情况,即顶点之间不存在直接连接的边。
4.2.5 Floyd-Warshall算法与其他路径规划算法的对比
Floyd-Warshall算法和其他路径规划算法相比具有一些优势和劣势,下面是它与其他常见路径规划算法的对比。
1. Dijkstra算法
- 适用范围:Dijkstra算法适用于解决单源最短路径问题,即计算从单个源节点到其他所有节点的最短路径。
- 时间复杂度:Dijkstra算法的时间复杂度取决于所采用的数据结构,在最坏情况下可达到O(V^2),或者通过使用优先队列优化后可达到O(E + V log V),其中V是节点数量,E是边数量。
- 负权边:Dijkstra算法不能处理带有负权边的图。
2. Bellman-Ford算法
- 适用范围:Bellman-Ford算法适用于解决单源最短路径问题,且可以处理带有负权边但不含负权环的图。
- 时间复杂度:Bellman-Ford算法的时间复杂度为O(V*E),其中V是节点数量,E是边数量。
- 负权环路:如果图中存在负权环路,Bellman-Ford算法能够检测并报告。
3. Floyd-Warshall算法
- 适用范围:Floyd-Warshall算法适用于解决所有节点对之间的最短路径问题,能够计算任意两个节点之间的最短路径。
- 时间复杂度:Floyd-Warshall算法的时间复杂度为O(V^3),其中V是节点数量。
- 负权环路:Floyd-Warshall算法不能处理带有负权环路的图,但它能够检测到负权环路的存在。
4. A*算法
- 适用范围:A*算法适用于解决单源最短路径问题,且可以利用启发式函数加速搜索。
- 时间复杂度:A*算法的时间复杂度取决于所采用的启发式函数,通常情况下比Dijkstra算法更快,但可能不如Dijkstra算法精确。
- 负权边:A*算法不能处理带有负权边的图。
综上所述,选择合适的路径规划算法取决于问题的具体情况,包括图的规模、边的属性(如是否存在负权边)、搜索的目标等。Floyd-Warshall算法适用于需要计算所有节点对之间的最短路径的情况,但由于其时间复杂度较高,不适合用于大规模图的计算。例如下面是一个简单的例子,演示了使用 Floyd-Warshall、Dijkstra、Bellman-Ford 和 A* 算法计算最短路径的过程。
实例4-2:使用多种路径规划算法计算最短路径(codes/4/bi.py)
实例文件bi.py的具体实现代码如下所示。
from heapq import heappop, heappush
INF = float('inf')
def floyd_warshall(graph):
n = len(graph)
dist = [[0 if i == j else graph[i][j] if graph[i][j] != 0 else INF for j in range(n)] for i in range(n)]
for k in range(n):
for i in range(n):
for j in range(n):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
def dijkstra(graph, start):
n = len(graph)
dist = [INF] * n
dist[start] = 0
visited = [False] * n
pq = [(0, start)]
while pq:
d, u = heappop(pq)
if visited[u]:
continue
visited[u] = True
for v in range(n):
if graph[u][v] != 0 and dist[u] + graph[u][v] < dist[v]:
dist[v] = dist[u] + graph[u][v]
heappush(pq, (dist[v], v))
return dist
def bellman_ford(graph, start):
n = len(graph)
dist = [INF] * n
dist[start] = 0
for _ in range(n - 1):
for u in range(n):
for v in range(n):
if graph[u][v] != 0 and dist[u] + graph[u][v] < dist[v]:
dist[v] = dist[u] + graph[u][v]
return dist
def astar(graph, start, goal):
n = len(graph)
open_list = [(0, start)]
g = {start: 0}
while open_list:
f, u = heappop(open_list)
if u == goal:
return g[u]
for v in range(n):
if graph[u][v] != 0:
new_g = g[u] + graph[u][v]
if v not in g or new_g < g[v]:
g[v] = new_g
heappush(open_list, (new_g + heuristic(v, goal), v))
return INF
def heuristic(u, v):
# 简单的启发式函数,返回节点到目标节点的距离
return abs(u - v)
# Example graph represented as adjacency matrix
graph = [
[0, 5, 0, 10],
[0, 0, 3, 0],
[0, 0, 0, 1],
[0, 0, 0, 0]
]
start = 0
goal = 3
# Floyd-Warshall
print("Floyd-Warshall:")
print(floyd_warshall(graph))
# Dijkstra
print("\nDijkstra:")
print(dijkstra(graph, start))
# Bellman-Ford
print("\nBellman-Ford:")
print(bellman_ford(graph, start))
# A*
print("\nA*:")
print(astar(graph, start, goal)))
上述代码的实现流程如下所示:
(1)首先,定义了一个表示图的邻接矩阵。在这个例子中,图是一个简单的有向图,用一个 4x4 的邻接矩阵表示。图中的每个元素表示从一个节点到另一个节点的距离或权重。如果两个节点之间没有边相连,则距离为 0。如果存在边相连但未指定权重,则距离为无穷大(INF)。
(2)实现Floyd-Warshall算法
- 首先,初始化一个距离矩阵,其中包含了所有节点对之间的最短距离。对角线上的元素为 0,表示节点到自身的距离。
- 接着,使用三重循环遍历所有节点对,并尝试通过中间节点来缩短路径。在每次迭代中,我们更新距离矩阵中的值,以反映新的最短路径。
- 最终,返回更新后的距离矩阵。
(3)实现Dijkstra算法
- 从起始节点开始,将其加入优先队列,并将其距离初始化为 0。然后,我们不断从优先队列中取出节点,并更新与其相邻的节点的距离。
- 对于每个相邻节点,如果通过当前节点到达它的路径比已知路径更短,则更新距离并将其加入优先队列。最终,当所有节点都被访问后,返回起始节点到每个节点的最短距离数组。
(4)实现Bellman-Ford算法
- 首先,初始化一个距离数组,其中包含起始节点到每个节点的距离。起始节点的距离为 0,其余节点的距离为无穷大。
- 然后进行多次松弛操作,遍历图中的每条边,并尝试通过更新距离数组来找到更短的路径。如果在第 n-1 次松弛操作后仍然存在可以更新的路径,则表示图中存在负权环,因此算法将返回错误。
- 最终,我们返回起始节点到每个节点的最短距离数组。
(5)实现A*算法
- 从起始节点开始,在优先队列中加入起始节点,并将其代价初始化为 0。然后,我们不断从优先队列中取出节点,并根据启发式评估函数来决定下一个访问的节点。
- 对于每个相邻节点,我们计算从起始节点经过当前节点到达目标节点的估计代价,并将其加入优先队列。最终,当目标节点被访问时,我们返回从起始节点到目标节点的最小代价。
以上4种路径规划算法在解决不同类型的路径规划问题时具有不同的优势和适用性,执行后会输出:
Floyd-Warshall:
[[0, 5, 8, 9], [inf, 0, 3, 4], [inf, inf, 0, 1], [inf, inf, inf, 0]]
Dijkstra:
[0, 5, 8, 9]
Bellman-Ford:
[0, 5, 8, 9]
A*:
9