(4-4)Floyd-Warshall算法:Floyd-Warshall算法的局限性与改进

4.4  Floyd-Warshall算法的局限性与改进

Floyd-Warshall 算法是一种用于解决所有点对最短路径的经典算法,它能够处理带有负权边但没有负权环的加权有向图。然而,Floyd-Warshall 算法也存在一些局限性。在本节的内容中,将详细讲解Floyd-Warshall算法的局限性与改进的知识。

4.4.1  算法复杂度与大规模图的挑战

Floyd-Warshall 算法是一种经典的解决所有点对最短路径的算法,但其在面对大规模图时存在一些挑战和局限性,具体说明如下所示。

  1. 时间复杂度高:Floyd-Warshall 算法的时间复杂度为 O(n3),其中 n 表示图中顶点的个数。对于大规模图,特别是稀疏图,算法的执行时间可能会非常长。
  2. 空间复杂度高:算法需要使用二维数组来存储所有点对之间的最短路径距离,因此其空间复杂度为 O(n2)。在大规模图中,这将占用大量的内存空间。

为了克服这些挑战,可以考虑使用如下所示的改进措施。

  1. 并行计算:可以将 Floyd-Warshall 算法改造为并行计算的形式,利用多核处理器或分布式计算环境,加速算法的执行速度。
  2. 分布式算法:将大规模图分割成小块,并分配给多台计算机进行处理。这样可以降低单个计算机的负载,提高算法的扩展性和效率。
  3. 近似算法:对于特别大的图,可以使用近似算法来求解最短路径问题。近似算法可能无法找到最优解,但可以在可接受的时间内给出一个较好的解决方案。
  4. 基于索引的优化:可以利用索引技术,对图的结构进行优化,加速最短路径的计算过程。例如,可以使用跳表或哈希表来加速查找操作。

通过采取上面介绍的这些改进措施,可以有效地应对大规模图带来的挑战,提高 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算法产生错误的结果。在这种情况下,算法可能会陷入无限循环,或者给出错误的结果。要处理负权回路,可以使用如下所示的几种常见的策略。

  1. 检测负权回路:在运行Floyd-Warshall算法之前,首先检测图中是否存在负权回路。一种方法是使用Bellman-Ford算法,该算法能够检测到图中是否存在负权回路。如果检测到负权回路,可以根据具体情况采取适当的措施,例如终止算法并报告无解。
  2. 限制迭代次数:在实现Floyd-Warshall算法时,可以限制迭代的次数。如果算法在预定次数内没有收敛,可以假设存在负权回路并终止算法。
  3. 动态规划优化:对于Floyd-Warshall算法的改进,可以使用动态规划优化,使得算法能够处理负权回路。这种方法通常需要修改算法的内部逻辑,以处理负权边的情况,例如将无穷大值与负权值进行比较,或者使用其他策略来处理负权值。
  4. 权值调整:有时候,可以对图中的权值进行调整,使得不存在负权回路。例如,可以添加一个足够大的常数到所有边的权值,以确保所有权值都为正数。这种方法可能需要根据具体情况进行权值调整,以确保不会改变最短路径的结果。

无论选择哪种策略,都需要根据具体情况来决定。在某些情况下,检测负权回路可能是最好的选择,而在其他情况下,可能需要修改算法以处理负权边。例如下面是一个使用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算法时,可以采取串行和并行方式,具体说明如下所示。

  1. 串行Floyd-Warshall:串行版本的 Floyd-Warshall 算法是一种单线程执行的算法,它按顺序逐步计算出所有节点对之间的最短路径。在每一步中,算法会更新当前已知的最短路径,并利用这些路径信息来计算更长路径的最短距离。
  2. 并行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 文件的内容,可以得出如下所示的结论。

  1. 随着问题规模的增加,串行版本和并行版本的执行时间都会增加。
  2. 并行版本相对于串行版本,在相同问题规模下,使用多线程能够显著降低执行时间。
  3. 随着线程数量的增加,执行时间有所下降,但是并不是线性的。在某些情况下,增加线程数量可能不会带来明显的性能提升,甚至可能会导致性能下降。
  4. 在选择线程数量时,需要考虑到计算机硬件配置和并行任务的特性,以获得最佳的性能提升效果。

  • 34
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码农三叔

感谢鼓励

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

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

打赏作者

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

抵扣说明:

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

余额充值