图—动态规划—Floyd算法—O(V^3)


与其他算法的比较

在这里插入图片描述
Floyd算法的目的是同时算出任意两点间的最短路径,适用于:

  • 节点数量在100左右的图
  • 需要在同一张图上多次查询最短路径

图的表示:

采用邻接矩阵的方式表示,其中不能到达的边用 ∞ \infty 表示,这里一般取 1 0 7 10^7 107,而非 2 31 − 1 2^{31}-1 2311。为了防止越界。
在这里插入图片描述
在这里插入图片描述

基本思想:

整个算法的构思源自于动态规划。
为了便于理解,我们首先引入:

m [ i ] [ j ] = 从i到j这条边的距离 m[i][j]=\text{从i到j这条边的距离} m[i][j]=ij这条边的距离

d p [ k ] [ i ] [ j ] = 从i到j且经过{0,1,...,k-1} 中 若 干 点 的 最 短 路 径 dp[k][i][j]=\text{从i到j且经过\{0,1,...,k-1\}}中若干点的最短路径 dp[k][i][j]=ij且经过{0,1,...,k-1}

此时,我们所希望的就是 d p [ n − 1 ] dp[n-1] dp[n1],假设一共有 n n n个节点。
则通过观察我们有以下的转移方程
d p [ k ] [ i ] [ j ] = m [ i ] [ j ] , ∀ i , j ∈ { 0 , 1 , . . . , n − 1 } , k = 0 dp[k][i][j]=m[i][j] ,\forall i,j \in \{0,1,...,n-1\}, k=0 dp[k][i][j]=m[i][j],i,j{ 0,1,...,n1},k=0

d p [ k ] [ i ] [ j ] = m i n ( d p [ k − 1 ] [ i ] [ j ] , d p [ k − 1 ] [ i ] [ k ] + d p [ k − 1 ] [ k ] [ j ] ) dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]) dp[k][i][j]=min(dp[k1][i][j],dp[k1][i][k]+dp[k1][k][j])

下对后一个式子进行解释:它的意思是如果要经过 { 0 , 1 , . . , k } \{0,1,..,k\} { 0,1,..,k} i i i j j j的最短路径 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j]
可以分成2种情况:

  • 一种是不经过k,即保持原样
  • 一种是经过k
    例如,在k=1的时候,
    d p [ 1 ] [ i ] [ j ] = m i n ( d p [ 0 ] [ i ] [ j ] , d p [ 0 ] [ i ] [ 1 ] + d p [ 0 ] [ 1 ] [ j ] ) = m i n ( m [ i ] [ j ] , m [ i ] [ 1 ] + m [ 1 ] [ j ] ) dp[1][i][j]=min(dp[0][i][j],dp[0][i][1]+dp[0][1][j])=min(m[i][j],m[i][1]+m[1][j]) dp[1][i][j]=min(dp[0][i][j],dp[0][i][1]+dp[0][1][j])=min(m[i][j],m[i][1]+m[1][j])
1
i
j

Rq:
如果i,j不能通过{0,…,k-1}联通,则暂时会为 ∞ \infty ,等到 ∃ m , n ∈ { 0 , . . . , k − 1 } \exist m,n \in \{0,...,k-1\} m,n{ 0,...,k1}使得 i i i j j j联通且 m , n m,n m,n联通即可。

我们还可以进一步对空间优化,即从 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j] d p [ i ] [ j ] dp[i][j] dp[i][j]
d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k ] [ j ] ) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
因为若 i  or  j = k i \text{ or } j=k i or j=k
d p [ k ] [ k ] [ j ] = m i n ( d p [ k − 1 ] [ k ] [ j ] , d p [ k − 1 ] [ k ] [ k ] + d p [ k − 1 ] [ k ] [ j ] ) = d p [ k − 1 ] [ k ] [ j ] dp[k][k][j]=min(dp[k-1][k][j],dp[k-1][k][k]+dp[k-1][k][j])=dp[k-1][k][j] dp[k][k][j]=min(dp[k1][k][j],dp[k1][k][k]+dp[k1][k][j])=dp[k1][k][j]是不会更改原来的值的。
换言之,如果我们不小心先算了 d p [ i ] [ k ] , d p [ k ] [ j ] dp[i][k],dp[k][j] dp[i][k],dp[k][j]但是它的值跟上一次相同,所以不会影响此次更新。

伪代码:

n = 节点的数量
dp = 用来存储记录两点之间最短距离的邻接矩阵
next = 用来帮助重构最短路径的矩阵next[i][j]表示要找从i到j的最短路径,下一步应该去节点next[i][j];
m = 用来表示图的邻接矩阵

function floydWarshall(m){
	//第一步:用m初始化 dp和next
	dp = n*n的空2维数组
	next = n*n的空2维数组
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++){
			dp[i][j]=m[i][j];
			if m[i][j]!= 10^7: //即若i通向j
				next[i][j]=j;
		}
		
	//第二步:Floyd
	for(int k=0;k<n;k++)
		for(int i=0;i<k;i++)
			for(int j=0;j<k;j++)
				if(dp[i][k]+dp[k][j]<dp[i][j]){
					dp[i][j]=dp[i][k]+dp[k][j];
					next[i][j]=next[i][k]; //因为我们是从i->k->j,所以下一步和从i到k的最小值路径的第一步相同
				}
	// 第三步:(可选)检测Negative Cycle
	propagateNegativeCycles(dp,n);
	
	return dp;
}

检测Negative Cycles

什么是Negative Cycles

如下图中红色部分的,一个元素或者多个元素都有可能形成。
在这里插入图片描述
在这里插入图片描述
我们注意到它的一个特点就是会使得结果任意小,【而Floyd单次结果不会受到Negative Cycles的影响】
因此,我们产生了一个检测它们的方法:
再来一次Floyd,如果结果改进了,那就产生了Negative Cycle

检测Negative Cycle伪代码

function propagateNegativeCycles(dp,n){
	for(int k=0;k<n;k++)
		for(int i=0;i<k;i++)
			for(int j=0;j<k;j++)
				if(dp[i][k]+dp[k][j]<dp[i][j]){
					dp[i][j]= -infty //表明i到j的最短路径属于或者经过一个Negative Cycle
					next[i][j]=-1; //标记这条边
				}
}

重新构建路径

function reconstructPath(start,end){
	path[]
	// 检测这两点是联通的
	if dp[start][end] == +infty:
		return path
	tmp = start; //从start开始还原路径
	for(;tmp!=end;tmp=next[tmp][end]){
		if tmp==-1:
			return -1 //说明最短路径属于或者经过一个Negative Cycle
		path.add(tmp) //注意这里加的是起点,而非next[start][end]
	}

	//因为有可能一个元素(如end)自己陷入Negative  Cycle
	if next[tmp][end]==-1:
		return null
	path.add(end)
	return path;
}

习题:

LeetCode 399.除法求值

猜你喜欢

转载自blog.csdn.net/weixin_44495738/article/details/112301397