引言
众所周知,Floyd是一个思路和实现都很简单的多源最短路径算法,原理是一种动态规划的思想,这里就不赘述太多,在做《算法竞赛进阶指南》中关于Floyd的题目中遇到的三种Floyd可以解决的问题,建议大家在阅读本文之前先熟悉Floyd算法。
1、传递闭包
什么是传递闭包?
给定一些元素和一些二元关系,且他们具有传递性(也就是假设关系有二元关系 ⨁ \bigoplus ⨁,对于元素 a , b , c a,b,c a,b,c只要有 a ⨁ b a\bigoplus b a⨁b,且 b ⨁ c b\bigoplus c b⨁c,那么满足 a ⨁ c a\bigoplus c a⨁c),传递这些二元关系使得推导出尽可能多的元素之间的关系称为传递闭包。
举个最简单的例子:已知三个实数 a , b , c a,b,c a,b,c,假设已知 a > b , b > c a>b,b>c a>b,b>c,要求 a , c a,c a,c的关系,我们就可以根据已知的关系推导出 a > c a>c a>c,这就是传递闭包。
怎么用Floyd传递闭包?
我们可以将关系映射到邻接矩阵上,设邻接矩阵 a a a表示关系,其中 a [ i , j ] = 1 a[i,j]=1 a[i,j]=1表示 i i i与 j j j有关系, d [ i , j ] = 0 d[i,j]=0 d[i,j]=0表示 i i i与 j j j没有关系,且我们设 d [ i , i ] d[i,i] d[i,i]始终为 1 1 1,那么我们就可以用floyd算法解决问题。
可能还是有点懵,那我们先打一个最朴素的Floyd吧:
设
d
[
i
,
j
]
d[i,j]
d[i,j]为
i
i
i到
j
j
j的最短路径长度,且设k为中转点那么有:
d [ i , j ] = m i n ( d [ i ] [ j ] , d [ i ] [ k ] + d [ k ] [ j ] ) d[i,j]=min(d[i][j],d[i][k]+d[k][j]) d[i,j]=min(d[i][j],d[i][k]+d[k][j])
代码实现
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
由于闭包具有传递性,所以同样的,假设我们用上文提到的邻接矩阵设计方式表示 i i i和 j j j的关系,且设 k k k为中转点那么有:
d [ i , j ] = d [ i ] [ j ] o r ( d [ i ] [ k ] a n d d [ k ] [ j ] ) d[i,j]=d[i][j]\ \ or\ \ (d[i][k]\ \ and\ \ d[k][j]) d[i,j]=d[i][j] or (d[i][k] and d[k][j])
你看这个方程似不似和floyd很像?没错,所以我们也可以用floyd来实现它。
代码实现
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j]|=d[i][k]&d[k][j]
例题:Sorting It All Out (POJ1094)
2、找无向图的最小环
什么是最小环
这个就相对简单易懂了,对于一张带权无向图,求图中至少包含三个节点的环,环上的节点不重复且环上的边权之和最小,这个问题称为无向图最小环问题。
Floyd为什么能找最小环?
上文提到,Floyd实际上是基于一种动态规划的思想,那么动态规划中就有所谓“阶段”的划分,在某些时候的动态规划方程中我们可以通过规划手段将动态规划状态转移方程中的“阶段”一维去掉。
其中最具有代表性的就是背包问题的状态转移方程,将第一维“前i个物品”去掉,只留下第二维“当前放入的总体积”,在外层循环到i的时候我们实际上处理的就是第i个阶段的状态。
那么实际上Floyd也与此同类,原始的floyd方程实际上是有一个阶段k在第一维的,表示为“设
d
[
k
,
i
,
j
]
d[k,i,j]
d[k,i,j]表示经过若干个编号不超过
k
k
k的节点,从
i
i
i到
j
j
j的最短路长度”,那么省略了k之后,我们再来看一下
d
d
d数组。
当外层循环到k的时候:
1、在状态转移之前,
d
d
d数组保存的是
k
−
1
k-1
k−1阶段下的信息
2、在状态转移之后,
d
d
d数组保存的是
k
k
k阶段下的信息
那么正因为Floyd拥有这种动态规划的性质那么我们就可以根据这个性质来设计算法。
Floyd怎么找最小环?
首先设图的信息保存在一个邻接矩阵
a
a
a中,即
a
[
i
,
j
]
=
c
a[i,j]=c
a[i,j]=c表示节点
i
i
i到
j
j
j有边且这条边的权值为
c
c
c。
那么我们把对于状态k的环的模型列出来:
上文提到了:当外层循环到k的时候,状态转移之前, d d d数组(设 d [ i , j ] d[i,j] d[i,j]为 i i i到 j j j的最短路径长度)保存的是 k − 1 k-1 k−1阶段下的信息, a a a数组已经有了图的所有信息,也就是说对于当前状态 k k k那么 a [ j , k ] , a [ k , i ] a[j,k],a[k,i] a[j,k],a[k,i]可以看做保存的是 k k k阶段下的信息,那么我们就可以,列出这样一个式子:
关 于 k 的 最 小 环 长 度 = m i n { d [ i , j ] + a [ j , k ] + a [ i , k ] } ) ( 1 ≤ i < j < k ) 关于k的最小环长度=min \lbrace d[i,j]+a[j,k]+a[i,k] \rbrace) (1\leq i <j<k) 关于k的最小环长度=min{d[i,j]+a[j,k]+a[i,k]})(1≤i<j<k)
这个转移方程中 d [ i , j ] d[i,j] d[i,j]是 k − 1 k-1 k−1阶段的信息,也就是说他不经过 k k k, a [ j , k ] + a [ i , k ] a[j,k]+a[i,k] a[j,k]+a[i,k]更新了当前 k k k阶段的信息,如上图所示构成了一个环,得到当前答案。
在我们求出当前答案之后,我们再转移 d [ i , j ] d[i,j] d[i,j]到下一层状态去,为以后的状态做准备,这样这个算法就完成啦。
代码实现:
for(int k=1;k<=n;k++)
{
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++)
if(d[i][j]+a[j][k]+a[k][i]<ans)
ans=d[i][j]+a[j][k]+a[k][i];
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
有向图的最小环问题怎么办?
对于有向图的最小环问题,可以枚举环的某一个起点
s
t
st
st,用堆优化
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra算法求出单元最短路径,那么
s
t
st
st一定是堆中第一个被取出的节点,扫描st的所有出边,当扩展,更新完成之后,令
d
i
s
[
s
t
]
=
+
∞
dis[st]=+\infty
dis[st]=+∞,然后继续求解。当s第二次从堆中被取出时,现在更新到的
d
i
s
[
s
t
]
dis[st]
dis[st]就是经过节点
s
t
st
st的最小环长度。
(摘自 《算法竞赛进阶指南》 李煜东)
这个算法还是很容易理解的!
例题:Sightseeing trip(POJ1734)
3、多次Floyd可以用快速幂优化
在阅读本chapter前建议先学习一下矩阵乘法。
什么时候需要用到多次Floyd?
我们直接用例题来说明:
Cow Relays (POJ3613):
给定一张由
T
(
2
≤
T
≤
100
)
T(2\leq T \leq 100)
T(2≤T≤100)条边构成的无向图,点的编号为1~1000之间的整数,求从起点
s
t
st
st到终点
e
d
ed
ed恰好经过
N
(
2
≤
N
≤
1
0
6
)
N(2\leq N \leq 10^6)
N(2≤N≤106)条边(可以重复经过)的最短路。
首先可以因为边比点少很多所以可以把点的编号离散化到 2 T 2T 2T范围内(因为最多都是一条边两个端点),然后我们发现可以用 N N N次Floyd来解决经过 N N N条边的最短路的问题。
但是用 N N N次Floyd来解决问题的话复杂度为 O ( T 3 N ) O(T^3N) O(T3N)大到爆炸!,怎么办呢,这就引出了下面的问题。
怎么用快速幂优化多次Floyd?
首先设 A [ i , j ] A[i,j] A[i,j]表示从 i i i到 j j j经过了一条边的最短路, A 2 [ i , j ] A^2[i,j] A2[i,j]表示从 i i i到 j j j经过了两条边的最短路,那么根据Floyd,有转移方程:
A 2 [ i , j ] = m i n { A [ i , k ] + A [ k , j ] } ( 1 ≤ i , j , k ≤ 2 T ) A^2[i,j]=min \lbrace A[i,k]+A[k,j] \rbrace (1\leq i,j,k \leq 2T) A2[i,j]=min{A[i,k]+A[k,j]}(1≤i,j,k≤2T)
那么对于一条边的最短路 A A A我们枚举了中转点 k k k,求出了两条边的 A 2 A^2 A2,那么我们尝试拓展这个式子:
设 A a [ i , j ] A^a[i,j] Aa[i,j]表示从 i i i到 j j j经过了 a a a条边的最短路, A b [ i , j ] A^b[i,j] Ab[i,j]表示从 i i i到 j j j经过了 b b b条边的最短路,则有:
A a + b [ i , j ] = m i n { A a [ i , k ] + A b [ k , j ] } ( 1 ≤ i , j , k ≤ 2 T ) A^{a+b}[i,j]=min \lbrace A^a[i,k]+A^b[k,j] \rbrace (1\leq i,j,k \leq 2T) Aa+b[i,j]=min{Aa[i,k]+Ab[k,j]}(1≤i,j,k≤2T)
我们回忆一下矩阵乘法的式子
摘自 OI-Wiki “矩阵”
我们发现上面最短路的式子和矩阵乘法的式子模式基本相同,只是将加法换成了min运算,乘法换成了加法,所以最短路式子其实可以等价于一个关于min运算和加法的广义的矩阵乘法。
我们知道矩阵乘法是满足结合律的,所以可以用快速幂把多次相乘时间降到
l
o
g
log
log级别,那么对于多次Floyd同样可以这样做在
l
o
g
N
logN
logN的时间内计算出
A
N
A^N
AN,将算法的复杂度降低到
O
(
T
3
l
o
g
N
)
O(T^3logN)
O(T3logN)
具体的实现就是在矩阵运算的过程中把原来的乘法用加法代替,原来的加法用min代替。
代码实现:
//POJ3613
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=1e3+10;
const int M=1e6+10;
const int INF=0x3f3f3f3f;
int n,m,st,ed;
int mp[N][N],ans[N][N];
int tmp1[N][N],tmp2[N][N];
int ha[N],tot=0;
bool v[N];
void floyd(int c[][N],int a[][N],int b[][N])
{
for(int k=1;k<=tot;k++)
for(int i=1;i<=tot;i++)
for(int j=1;j<=tot;j++)
if(c[ha[i]][ha[j]]>a[ha[i]][ha[k]]+b[ha[k]][ha[j]])
c[ha[i]][ha[j]]=a[ha[i]][ha[k]]+b[ha[k]][ha[j]];
}
void copy(int a[][N],int b[][N])
{
for(int i=1;i<=tot;i++)
for(int j=1;j<=tot;j++)
{
a[ha[i]][ha[j]]=b[ha[i]][ha[j]];
b[ha[i]][ha[j]]=INF;
}
}
void solve(int k)
{
while(k)
{
if(k&1)
{
floyd(tmp1,ans,mp);
copy(ans,tmp1);
}
floyd(tmp2,mp,mp);
copy(mp,tmp2);
k>>=1;
}
}
int main()
{
scanf("%d%d%d%d",&n,&m,&st,&ed);
for(int i=0;i<N;i++)
{
for(int j=0;j<N;j++)
{
mp[i][j]=ans[i][j]=tmp1[i][j]=tmp2[i][j]=INF;
}
ans[i][i]=0;
}
memset(v,false,sizeof(v));
tot=0;
for(int i=1;i<=m;i++)
{
int x,y,c;scanf("%d%d%d",&c,&x,&y);
mp[x][y]=mp[y][x]=min(mp[x][y],c);
if(!v[x]){ v[x]=1; ha[++tot]=x; }
if(!v[y]){ v[y]=1; ha[++tot]=y; }
}
solve(n);
printf("%d\n",ans[st][ed]);
return 0;
}
总结
即使Floyd仍是有他不可逾越的立方级时间复杂度以及平方级空间复杂度的鸿沟,当点的数量多起来之后就难以高效率的处理问题了。
但是Floyd具有的特殊性质:可传递,满足动态规划性,满足结合律,所以我们可以利用它来进行传递闭包,求解最小环以及用快速幂优化等花式操作,还是十分有趣的。