网络最短路——Floyd算法实现
Floyd算法(Floyd-Warshall算法)是一种用于求解图中所有顶点对之间最短路径的算法,该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。Floyd算法可以应用于许多方面,特别是在交通、物流和通信网络的优化中,譬如城市交通规划:Floyd算法可以帮助规划城市交通网络,确定最短路径和最优路线,以减少交通拥堵和行程时间。物流管理:在物流领域,Floyd算法可以用于确定不同地点之间的最短路径,帮助优化货物运输和配送的路线,降低成本并提高效率。电信网络规划:Floyd算法可应用于电信网络中的路由优化,帮助确定数据包传输的最短路径,提高网络连接的速度和质量。航班路径规划:航空公司可以使用Floyd算法确定不同机场之间的最短路径,优化航班计划和航线,提高飞行效率和旅客体验。金融交易网络:在金融领域,Floyd算法可以用于分析银行间的交易网络,确定最短路径和关键参与者,以便监测风险和进行迅速的资金清算,等等。
一、最短路问题
若网络中的每条边都有一个数值(长度、成本、时间等),则找出两节点(通常是源节点和阱节点)之间总权和最小的路径就是最短路问题。最通用的最短路问题(认为是标准型)可以如此描述:希望在网络中找到一条从源节点(source node)到接收节点(target node)的最小成本路径,这里的最小成本可定义为路径长度、旅行时间、旅行费用等。
设\(G\)是由节点集合\(V\)(元素个数为n)和弧集合\(A\)(元素个数为m)组成的网络,定义节点\(s ∈ V\)为源节点(source),其他节点为非源节点(non-source),路径长度为该路径所包含弧的长度之和。求解单源最短路径问题就是找出源节点\(s\)到每一个非源节点\(t\)的有向最短路径。最短路径问题的数学模型如下:
二、最短路的Floyd算法
求图上两点\(i,j\) 之间的最短距离,可以按“从小图到全图”的步骤,在逐步扩大图的过程中计算和更新最短路。想象图中的每个点是一个灯,开始时所有灯都是灭的。然后逐个点亮灯,每点亮一个灯,就重新计算\(i,j\) 的最短路,要求路径只能经过点亮的灯。所有灯都点亮后计算结束。在这个过程中,点亮第\(k\)个灯时,能用到\(1∼k−1\)个亮灯的结果。
Floyd算法是经典的动态规划算法,基本思想是递推产生一个矩阵序列\(A_1,A_2,..,A_k,...,A_n\)(图有\(n\)个节点),\(A_k=[a_k(i,j)]_{n \times n}\)。其中矩阵\(A_k\)第\(i\)行第\(j\)列表示从顶点\(v_i\)到顶点\(v_j\)的路径上经过的顶点序号不大于\(k\)的最短路径长度。
迭代公式:\(a_{k}(i,j)=min(a_{k-1}(i,j),a_{k-1}(i,k)+a_{k-1}(k,j))]\)
\(k\)是迭代次数,\(i,j,k=1,2,...,n\)。
当最后\(k=n\)是时,\(A_n\)矩阵就是各个顶点之间的最短距离值了。
那么如果需要记录路径,那么需要引入路由矩阵。路由矩阵\(R_k=[r_k(i,j)]_{n \times n}\),用来记录两点之间路径的前驱后继的关系,其中\(r_k(i,j)\)表示从顶点\(v_i\)到顶点\(v_j\)的路径经过编号为\(r_k(i,j)\)的顶点。所以需要注意了,路由矩阵里面不是数值,而是结点。
以上理论听起来挺复杂的。不过没有关系的。真正的算法实现无外乎就是三个循环嵌套,\(i,j\)的循环是任意两个点,而\(k\)则是两个点之间所经过的第三个点,我们就是在循环之中不断比较从\(i\)到\(j\)的距离与从\(i\)到\(k\)距离加上从\(k\)到\(j\)距离的大小,如果经过这个点,路径变短了,我们就接受这个点,认为可以经过这个点;否则就不经过这个点,就是从\(i\)到\(j\)最短。
如果我们得到了路由矩阵,那怎样得到路径呢?我们得到的路由矩阵\(R_n\),其中\(r(i,j)\)(就是路由矩阵第\(i\)行\(j\)列)表示第\(i\)个节点到第\(j\)个节点最短路径经过的中间点\(p_1\)。那么我们先向起始顶点\(v_i\)反向追踪,得到\(r(i,p_1)=p_2,r(i,p_2)=p_3...,\)最终\(r(i,p_n)=0\),就结束了;再正向追踪,\(r(p_1,j)=q_1,r(q_1,j)=q_2.....r(q_t,j)=0\),完成正向追踪。路径就是\(v_i,p_n,...,p_2,p_1,q_1,q_2,...,q_t,v_j\)。
三、最短路Python求解
案例:考虑下图程序所作图的最短路。
3.1 作网络图
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
A = np.mat([[0, 3, 0, 2, 0],
[3, 0, 2, 1, 0],
[0, 2, 0, 0, 5],
[2, 1, 0, 0, 4],
[0, 0, 5, 4, 0]]) # Adjacency matrix
G = nx.Graph(A) # Undirected weighted graph
pos = nx.shell_layout(G) # Set the layout for node positions
w = nx.get_edge_attributes(G, "weight") # Get the edge weights
nx.draw_networkx(G=G, pos=pos, node_size=260) # Draw the network graph
nx.draw_networkx_edge_labels(G=G, pos=pos, font_size=12, edge_labels=w) # Label the edge weights
plt.show()
3.2 最短路计算
import numpy as np
from numpy import inf
A = [[inf, 3, inf, 2, inf],
[3, inf, 2, 1, inf],
[inf, 2, inf, inf, 5],
[2, 1, inf, inf, 4],
[inf, inf, 5, 4, inf]]
n = len(A) # Calculate the number of vertices in the graph.
dis = A # Initialize the dis matrix with the given adjacency matrix A.
path = np.zeros((n, n)) # Initialize the path matrix with zeros.
for k in range(n):
for i in range(n):
for j in range(n):
if dis[i][k] + dis[k][j] < dis[i][j]:
dis[i][j] = dis[i][k] + dis[k][j] # Update the shortest distance between i and j through k.
path[i][j] = k # Record the intermediate vertex k in the shortest path between i and j.
print("Shortest path distances (dis):")
print(np.array(dis)) # Display the matrix containing the shortest path distances between all pairs of vertices.
print("Intermediate vertices in shortest paths (path):")
print(np.array(path, dtype=int)) # Display the matrix representing the intermediate vertices in the shortest paths.
Shortest path distances (dis):
[[4 3 5 2 6]
[3 2 2 1 5]
[5 2 4 3 5]
[2 1 3 2 4]
[6 5 5 4 8]]
Intermediate vertices in shortest paths (path):
[[3 0 1 0 3]
[0 3 0 0 3]
[1 0 1 1 0]
[0 0 1 1 0]
[3 3 0 0 3]]
我们以0到4 点为例,最短距离是6。path[0][4]=3,该路径经过点3,反向追踪:path[0][3]=0,结束;正向追踪:path[3][4]=0,结束。最短路径就是0->3->4。看一看前面的图,的确符合事实。但是,实际上,这五个点最好用a,b,c,d,e等表示,或是1,2,3,4,5。这里的用做终止追踪的0其实是初始时的0,而这个矩阵当中也有作为节点标记的0,看的时候很容易混淆。如果我们用字母标记,最终会看到路由矩阵中有节点标记,也有数字0,看起来会方便很多。
四、最短路R求解
实例:求下图所示网络各顶点间的最短路。
4.1 作网络图
library(igraph)
data <- matrix(c(
0, 30, 15, 0, 0, 0,
5, 0, 0, 0, 20, 30,
0, 10, 0, 0, 0, 15,
0, 0, 0, 0, 0, 0,
0, 0, 0, 10, 0, 0,
0, 0, 0, 30, 10, 0
), nrow = 6, byrow = TRUE)
rownames(data) <- c("S1", "S2", "s3", "s4", "s5", "s6")
colnames(data) <- c("S1", "S2", "s3", "s4", "s5", "s6")
# Create the graph
graph <- graph.adjacency(data, mode = "directed", weighted = TRUE, diag = FALSE)
# Plot the graph with row names as labels
plot(graph, vertex.label = rownames(data), vertex.size = 30, vertex.color = "lightblue", edge.label = E(graph)$weight)
4.2 最短路计算
# 准备数据
data <- matrix(c(
0, 30, 15, Inf, Inf, Inf,
5, 0, Inf, Inf, 20, 30,
Inf, 10, 0, Inf, Inf, 15,
Inf, Inf, Inf, 0, Inf, Inf,
Inf, Inf, Inf, 10, 0, Inf,
Inf, Inf, Inf, 30, 10, 0
), nrow = 6, byrow = TRUE)
path <- matrix(0, nrow = 6, ncol = 6)
for (k in 1:6) {
for (i in 1:6) {
for (j in 1:6) {
if (data[i, j] > data[i, k] + data[k, j]) {
data[i, j] <- data[i, k] + data[k, j]
path[i, j] <- k
}
}
}
}
# 定义函数找出x到y的具体路径
show_trace <- function(x, y) {
trace <- c()
add_trace <- function(x, y) {
if (path[x, y] != 0) {
add_trace(x, path[x, y])
trace <<- c(trace, path[x, y])
add_trace(path[x, y], y)
}
}
add_trace(x, y)
if (length(trace) > 0) {
trace_str <- paste(c(x, trace, y), collapse = "-->")
cat(paste("从", x, "到", y, "的最短路径为:", trace_str))
} else {
cat(paste("从", x, "到", y, "不存在路径"))
}
}
print(data)
show_trace(1, 5) # 求S1到S5的最短路径
show_trace(1, 4) # 求S1到S4的最短路径
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 0 25 15 50 40 30
[2,] 5 0 20 30 20 30
[3,] 15 10 0 35 25 15
[4,] Inf Inf Inf 0 Inf Inf
[5,] Inf Inf Inf 10 0 Inf
[6,] Inf Inf Inf 20 10 0
从 1 到 5 的最短路径为: [1-->3-->6-->5]
从 1 到 4 的最短路径为: [1-->3-->6-->5--> 4]
总结
弗洛伊德算法的算法实现比较简单,过程容易理解,但原理不是很容易消化透彻。同学们在学习的时候可以根据算法过程,找一个具体的图仔细捋一捋,画一下距离矩阵与路由矩阵变化过程,以及当中第三点的选取、距离的比较等,体会当中的动态规划思想。该算法可以求最短路径及其长度,但是对于复杂的网络,该算法还是有局限性的。这个时候我们便需要选择其它的自然算法,例如蚁群算法等来进行求解。虽然那些相对“高级”的算法不一定在众多的路径中找到最短的那一条,但是却可以在较短时间内找到比较短的一条路径,而且会很接近最短路径的。