4.4 Floyd-Warshall算法的局限性与改进
Floyd-Warshall 算法是一种用于解决所有点对最短路径的经典算法,它能够处理带有负权边但没有负权环的加权有向图。然而,Floyd-Warshall 算法也存在一些局限性。在本节的内容中,将详细讲解Floyd-Warshall算法的局限性与改进的知识。
4.4.1 算法复杂度与大规模图的挑战
Floyd-Warshall 算法是一种经典的解决所有点对最短路径的算法,但其在面对大规模图时存在一些挑战和局限性,具体说明如下所示。
- 时间复杂度高:Floyd-Warshall 算法的时间复杂度为 O(n3),其中 n 表示图中顶点的个数。对于大规模图,特别是稀疏图,算法的执行时间可能会非常长。
- 空间复杂度高:算法需要使用二维数组来存储所有点对之间的最短路径距离,因此其空间复杂度为 O(n2)。在大规模图中,这将占用大量的内存空间。
为了克服这些挑战,可以考虑使用如下所示的改进措施。
- 并行计算:可以将 Floyd-Warshall 算法改造为并行计算的形式,利用多核处理器或分布式计算环境,加速算法的执行速度。
- 分布式算法:将大规模图分割成小块,并分配给多台计算机进行处理。这样可以降低单个计算机的负载,提高算法的扩展性和效率。
- 近似算法:对于特别大的图,可以使用近似算法来求解最短路径问题。近似算法可能无法找到最优解,但可以在可接受的时间内给出一个较好的解决方案。
- 基于索引的优化:可以利用索引技术,对图的结构进行优化,加速最短路径的计算过程。例如,可以使用跳表或哈希表来加速查找操作。
通过采取上面介绍的这些改进措施,可以有效地应对大规模图带来的挑战,提高 Floyd-Warshall 算法在实际应用中的效率和可扩展性。请看下面的实例,使用多线程技术实现了Floyd-Warshall 算法,以在计算最短路径时并行处理矩阵。
实例4-5:多线程Floyd-Warshall 算法(codes/4/duo.py)
实例文件duo.py的具体实现代码如下所示。
import numpy as np
import matplotlib.pyplot as plt
import concurrent.futures
INF = float('inf')
def floyd_warshall_parallel(graph):
n = len(graph)
dist = np.copy(graph)
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = []
for k in range(n):
futures.append(executor.submit(update_distances, dist, k))
for future in concurrent.futures.as_completed(futures):
pass
return dist
def update_distances(dist, k):
n = len(dist)
for i in range(n):
for j in range(n):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
def visualize(graph, shortest_distances):
plt.figure(figsize=(8, 6))
plt.imshow(shortest_distances, cmap='viridis', interpolation='nearest')
plt.colorbar(label='Shortest Distance')
plt.xlabel('Destination')
plt.ylabel('Source')
plt.title('Shortest Distances Between Cities')
plt.xticks(ticks=np.arange(len(graph)), labels=range(len(graph)))
plt.yticks(ticks=np.arange(len(graph)), labels=range(len(graph)))
plt.show()
# Example graph represented as adjacency matrix
graph = np.array([
[0, 5, INF, 10],
[INF, 0, 3, INF],
[INF, INF, 0, 1],
[INF, INF, INF, 0]
])
# Using Floyd-Warshall algorithm with parallel processing
shortest_distances = floyd_warshall_parallel(graph)
print("Shortest distances between every pair of vertices:")
print(shortest_distances)
# Visualize the shortest distances
visualize(graph, shortest_distances)
在上述代码中,首先定义了一个floyd_warshall_parallel函数,该函数使用多线程技术实现了Floyd-Warshall算法。然后,通过visualize函数将计算得到的最短路径可视化,利用matplotlib库绘制了一个热图来显示城市之间的最短距离。执行后会输出如下最短路径的结果,并绘制城市之间的最短距离热图,如图4-3所示。
Shortest distances between every pair of vertices:
[[ 0. 5. 8. 9.]
[inf 0. 3. 4.]
[inf inf 0. 1.]
[inf inf inf 0.]]
图4-3 城市之间的最短距离热图
4.4.2 负权回路的处理策略
Floyd-Warshall算法有一些局限性,其中一个主要的局限性是它无法处理负权回路。负权回路是一个环路,通过该环路可以无限次地降低路径的总权值,这使得最短路径的概念变得模糊,并导致Floyd-Warshall算法产生错误的结果。在这种情况下,算法可能会陷入无限循环,或者给出错误的结果。要处理负权回路,可以使用如下所示的几种常见的策略。
- 检测负权回路:在运行Floyd-Warshall算法之前,首先检测图中是否存在负权回路。一种方法是使用Bellman-Ford算法,该算法能够检测到图中是否存在负权回路。如果检测到负权回路,可以根据具体情况采取适当的措施,例如终止算法并报告无解。
- 限制迭代次数:在实现Floyd-Warshall算法时,可以限制迭代的次数。如果算法在预定次数内没有收敛,可以假设存在负权回路并终止算法。
- 动态规划优化:对于Floyd-Warshall算法的改进,可以使用动态规划优化,使得算法能够处理负权回路。这种方法通常需要修改算法的内部逻辑,以处理负权边的情况,例如将无穷大值与负权值进行比较,或者使用其他策略来处理负权值。
- 权值调整:有时候,可以对图中的权值进行调整,使得不存在负权回路。例如,可以添加一个足够大的常数到所有边的权值,以确保所有权值都为正数。这种方法可能需要根据具体情况进行权值调整,以确保不会改变最短路径的结果。
无论选择哪种策略,都需要根据具体情况来决定。在某些情况下,检测负权回路可能是最好的选择,而在其他情况下,可能需要修改算法以处理负权边。例如下面是一个使用Bellman-Ford算法检测负权回路的例子,如果检测到负权回路,则会输出提示信息,否则会继续执行后续的Floyd-Warshall算法。
实例4-6:使用Bellman-Ford算法检测负权回路(codes/4/jian.py)
实例文件jian.py的具体实现代码如下所示。
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
# 实现 Bellman-Ford 算法检测负权回路
def bellman_ford(graph, source):
# 初始化距离和前驱节点数组
n = len(graph)
dist = [float('inf')] * n
pred = [None] * n
dist[source] = 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]
pred[v] = u
# 检查是否存在负权回路
for u in range(n):
for v in range(n):
if graph[u][v] != 0 and dist[u] + graph[u][v] < dist[v]:
return True, None
return False, dist
# 实现 Floyd-Warshall 算法计算最短距离
def floyd_warshall(graph):
n = len(graph)
dist = np.array(graph)
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 visualize(graph):
G = nx.from_numpy_array(np.array(graph), create_using=nx.DiGraph)
# 使用圆形布局
pos = nx.circular_layout(G)
nx.draw(G, pos, with_labels=True, node_size=1000, node_color="skyblue", font_size=12, font_weight="bold",
arrowsize=20)
labels = nx.get_edge_attributes(G, "weight")
nx.draw_networkx_edge_labels(G, pos, edge_labels=labels)
plt.title("Graph Visualization")
plt.show()
# 例子中的图以邻接矩阵表示
graph = [
[0, -1, 4, 0, 0],
[0, 0, 3, 2, 2],
[0, 0, 0, 0, 0],
[0, 1, 5, 0, 0],
[0, 0, 0, -3, 0]
]
# 使用 Bellman-Ford 算法检测负权回路
has_negative_cycle, _ = bellman_ford(graph, 0)
if has_negative_cycle:
print("The graph contains a negative weight cycle.")
else:
print("The graph does not contain a negative weight cycle.")
# 如果没有负权回路,则进行 Floyd-Warshall 算法
if not has_negative_cycle:
shortest_distances = floyd_warshall(graph)
print("Shortest distances between every pair of vertices:")
print(shortest_distances)
# 可视化图形
visualize(graph)
上述代码的实现流程如下所示:
(1)首先,导入了所需的库,包括 NumPy、NetworkX 和 Matplotlib。
(2)然后,定义了两个函数:bellman_ford 和 floyd_warshall,分别用于实现 Bellman-Ford 算法和 Floyd-Warshall 算法。其中Bellman-Ford 算法用于检测图中是否存在负权回路,并返回从源节点到其他节点的最短路径距离。Floyd-Warshall 算法用于计算图中每对节点之间的最短路径距离。
(3)接着,定义了一个用于可视化图形的函数 visualize。在此函数中,使用 NetworkX 创建图形,并使用 Matplotlib 进行绘制。我们使用圆形布局将节点布置在圆周上,节点的颜色和大小都进行了设置,边的权重也标记在了图上。
(4)在主程序中使用了一个示例图 graph,然后调用 bellman_ford 函数检测图中是否存在负权回路。如果不存在负权回路,则调用 floyd_warshall 函数计算图中每对节点之间的最短路径距离,并输出结果。
(5)最后,调用 visualize 函数视化展示对应的图形。
在本实例中,Bellman-Ford算法没有检测到负权回路,因此程序继续执行后续的Floyd-Warshall算法。这表明图中不存在负权回路,可以安全地继续进行其他操作。执行后会输出下面的结果,
The graph does not contain a negative weight cycle.
Shortest distances between every pair of vertices:
[[ -72 -73 -72 -81 -126]
[ -72 -73 -72 -81 -126]
[ -75 -76 -75 -84 -129]
[ -81 -82 -81 -90 -135]
[-126 -127 -126 -135 -180]]
本实例根据给定的图数据绘制了可视化图,如图4-4所示。图中的节点代表交叉路口,边代表道路,边的权重表示两个交叉路口之间的距离。图中的节点通过边相连接,同时还显示了边的权重。通过这个可视化图,可以直观地了解图的结构和节点之间的连接关系,以及各个节点之间的距离。
图4-4 可视化图
4.4.3 并行Floyd-Warshall
在实现Floyd-Warshall算法时,可以采取串行和并行方式,具体说明如下所示。
- 串行Floyd-Warshall:串行版本的 Floyd-Warshall 算法是一种单线程执行的算法,它按顺序逐步计算出所有节点对之间的最短路径。在每一步中,算法会更新当前已知的最短路径,并利用这些路径信息来计算更长路径的最短距离。
- 并行Floyd-Warshall:并行版本的 Floyd-Warshall 算法利用多线程或多进程的方式并行处理计算任务,从而提高计算效率。在并行版本中,通常将计算任务分配给多个处理单元,每个处理单元负责计算一部分节点对之间的最短路径,然后将结果合并以得到最终的最短路径信息。
串行版本和并行版本的选择取决于计算机系统的性能、问题规模以及对计算效率的要求。通常情况下,并行版本可以显著缩短计算时间,特别是对于大规模的图或者计算密集型的任务。例如在下面的实例中,首先定义了一些函数来生成权重矩阵和数据初始化功能。然后,实现了串行版本的Floyd-Warshall算法(floyd_serial函数),并且使用多维数组存储了距离矩阵和路径矩阵。接着,定义了并行版本的Floyd-Warshall算法(floyd_parallel函数),通过多线程并行处理不同的区域来提高计算效率。最后,在主程序中对不同规模的图进行了性能测试,并将结果写入文件中进行记录。
实例4-7:比较串行Floyd-Warshall和并行Floyd-Warshall的效率(codes/4/bing.py)
实例文件bing.py的具体实现代码如下所示。
import random # 导入随机数模块
import sys # 导入系统模块
import time # 导入时间模块
from multiprocessing import Process, Array, Queue # 导入多进程相关模块
from threading import Lock, Thread # 导入线程相关模块
import math # 导入数学模块
numThreads = 4 # 并行线程数
"""
W = 权重矩阵 (w(i,j))
n = |V|
V = {1, ..., n}
i = 起始顶点
j = 终止顶点
P = 最短路径
d[k][i][j] = 通过 k 个中间顶点从 i 到 j 的路径权重
如果 k = 0,则 d[i][j][k] = w[i][j]
如果 k > 0,则 d[i][j][k] = min(d[i][j][k-1], (d[k-1][i][j], d[k-1][i][k] + d[k-1][k][j]))
对于 pi:
如果 d[k-1][i][j] <= d[k-1][i][k] + d[k-1][k][j],则 pi[k][i][j] = pi[k-1][i][j]
如果 d[k-1][i][j] > d[k-1][i][k] + d[k-1][k][j],则 pi[k][i][j] = pi[k-1][k][j]
"""
def weight_serial(size):
"""生成权重矩阵"""
dd = []
for i in range(size):
ddd = []
for j in range(size):
if i == j:
ddd.append(0)
else:
ddd.append(random.randint(2, 30))
dd.append(ddd)
return dd
def pi_start_serial(weights, size):
"""生成初始 pi 矩阵"""
dd = []
for i in range(size):
ddd = []
for j in range(size):
if i == j or weights[i][j] == sys.maxsize:
ddd.append(None)
elif i != j and weights[i][j] < sys.maxsize:
ddd.append(i)
else:
ddd.append(-1)
dd.append(ddd)
return dd
def dist_serial(weights, size):
"""生成初始 d 矩阵"""
d = [[[(0 if __ == ___ else int(sys.maxsize)) for __ in range(size)] for __ in range(size)] for ___ in range(size)]
d[0] = list(weights)
return d
def pi_serial(weights, size):
"""生成初始 pi 矩阵"""
d = [[[(None if __ == ___ else 0) for __ in range(size)] for __ in range(size)] for ___ in range(size)]
d[0] = list(pi_start_serial(weights, size))
return d
def floyd_serial(weights, size):
"""串行 Floyd-Warshall 算法"""
d = dist_serial(weights, size)
p = pi_serial(weights, size)
for k in range(1, size):
for i in range(size):
for j in range(size):
d[k][i][j] = min(d[k - 1][i][j], d[k - 1][i][k] + d[k - 1][k][j])
if d[k - 1][i][j] <= d[k - 1][i][k] + d[k - 1][k][j]:
p[k][i][j] = p[k - 1][i][j]
else:
p[k][i][j] = p[k - 1][k][j]
return d, p
def min_p_parallel(dist, pi, k, ir, size, queue):
"""并行计算 Floyd-Warshall 中的最小路径"""
darr = []
parr = []
for i in ir:
darr.append([(min(dist[i][j], dist[i][k] + dist[k][j])) for j in range(size)])
parr.append([(pi[i][j] if dist[i][j] <= dist[i][k] + dist[k][j] else pi[k][j]) for j in range(size)])
queue.put((ir, darr, parr))
def floyd_parallel(weights, size):
"""并行 Floyd-Warshall 算法"""
dist = dist_serial(weights, size)
pi = pi_serial(weights, size)
for k in range(1, size):
thr = []
queue = Queue()
for pro in range(numThreads):
imin = math.floor(pro * (size / numThreads))
imax = math.floor((pro + 1) * (size / numThreads))
ir = range(imin, imax)
t = Process(target=min_p_parallel, args=(dist[k - 1], pi[k - 1], k, ir, size, queue), daemon=True).start()
thr.append(t)
for t in thr:
try:
t.join()
except AttributeError:
pass
for v in range(numThreads):
veci, vecd, vecp = queue.get()
ct = 0
for i in veci:
dist[k][i] = list(vecd[ct])
pi[k][i] = list(vecp[ct])
ct += 1
return dist, pi
if __name__ == "__main__":
fis = open('serial.txt', 'a')
fip = open('parallel.txt', 'a')
for n in range(24, 460, 24):
w = weight_serial(n)
start = time.process_time()
ds, ps = floyd_serial(w, n)
end = time.process_time()
fis.write('For %s: Elapsed time: %ss\n' % (n, str(end-start)))
print('S: For %s: Elapsed time: %ss' % (n, str(end-start)))
for numThreads in [1, 2, 3, 4, 6, 8]:
start = time.process_time()
dp, pp = floyd_parallel(w, n)
end = time.process_time()
fip.write('For %s with %s threads: Elapsed time: %ss\n' % (n, numThreads, str(end-start)))
print('P: For %s with %s threads: Elapsed time: %ss' % (n, numThreads, str(end-start)))
print('Same dist matrix: %s' % (dp == ds))
print('Same pi matrix: %s' % (pp == ps))
# print(dist)
# print(pi)
执行上述代码厚会打开两个文件,分别命名为 serial.txt 和 parallel.txt。这两个文件用于记录程序的执行结果,其中 serial.txt 记录串行版本的运行时间,而 parallel.txt 记录并行版本的运行时间。
文件serial.txt的内容示例如下:
For 24: Elapsed time: 0.012s
For 48: Elapsed time: 0.031s
For 72: Elapsed time: 0.065s
For 96: Elapsed time: 0.132s
...
文件parallel.txt的内容示例如下:
For 24 with 1 threads: Elapsed time: 0.017s
For 24 with 2 threads: Elapsed time: 0.013s
For 24 with 3 threads: Elapsed time: 0.012s
For 24 with 4 threads: Elapsed time: 0.011s
For 24 with 6 threads: Elapsed time: 0.015s
For 24 with 8 threads: Elapsed time: 0.019s
For 48 with 1 threads: Elapsed time: 0.034s
For 48 with 2 threads: Elapsed time: 0.027s
...
根据上述serial.txt 和 parallel.txt 文件的内容,可以得出如下所示的结论。
- 随着问题规模的增加,串行版本和并行版本的执行时间都会增加。
- 并行版本相对于串行版本,在相同问题规模下,使用多线程能够显著降低执行时间。
- 随着线程数量的增加,执行时间有所下降,但是并不是线性的。在某些情况下,增加线程数量可能不会带来明显的性能提升,甚至可能会导致性能下降。
- 在选择线程数量时,需要考虑到计算机硬件配置和并行任务的特性,以获得最佳的性能提升效果。