Part I-Introduction
Floyd
算法是一种求图上多源最短路径的算法,适用于中小规模的图,思维简单易懂。
Floyd
算法的实质是(区间)动态规划
,在这里做一个简单的概述。
对于一个有\(n\)个结点的图,
令\(dis[i][j]\)为结点\(i\)到结点\(j\)的最短路径长度。
首先,将所有现成的边都存入\(dis\),其余的令其值\(=\infty\),并使\(dis[i][i]=0\)。
接着,枚举中转点(\(k\)),那么:
\[dis[i][j]=\min\{dis[i][k]+dis[k][j]\text{ | }k\in[1,n],k\ne i,k\ne j\}\]
代码实现:
void Floyd()
{memset(dis,0x3f3f3f3f,sizeof(dis));for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(a[i][j]!=0x3f3f3f3f)边(i,j)存在。dis[i][j]=a[i][j];//a为现存的边。for(int i=1;i<=n;i++) dis[i][i]=0;for(int k=1;k<=n;k++)//枚举中转点。for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){if(i==j||j==k||k==i) continue;dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);}
}
Attention:循环时\(k\)必须放在第一层。若将\(i\)置于第一层,就会导致
i->k->j
的距离过早的确定下来,可能会导致答案错误。
注:其实最原始的Floyd中\(dis\)的定义是:\(dis[i][j][k]\)表示结点\(i\)到结点\(j\)只经过结点\(i-k\)的最短路径。 则有:\(dis[i][j][k]=min(dis[i][j][k-1],dis[i][k][k-1]+dis[k][j][k-1])\),降维得到现在的方程。
时间复杂度:\(O(n^3)\)。
Part II-Sevral Simple Problems
Floyd
算法可以解决许多看似无法处理的问题。
Problem[1]:[USACO09JAN] Best Spot
链接:https://www.luogu.org/problemnew/show/P2935
题面:
此题较为简单,算法流程:
用
Floyd
处理出各个点间的最短路径。计算出每个点到各个Favorites的总距离。
选出总距离最小的点输出即可。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define R registerint a[501][501];
int dis[501][501];
int fav[501];
int P,F,C;void Floyd()
{memset(dis,0x3f3f3f3f,sizeof(dis));for(R int i=1;i<=P;i++)for(R int j=1;j<=P;j++)if(a[i][j]!=0x3f3f3f3f)dis[i][j]=a[i][j];for(R int i=1;i<=P;i++) dis[i][i]=0;for(R int k=1;k<=P;k++)for(R int i=1;i<=P;i++)for(R int j=1;j<=P;j++){if(i==j||j==k||k==i) continue;dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);}
}signed main()
{scanf("%d%d%d",&P,&F,&C);memset(a,0x3f3f3f3f,sizeof(a));for(R int i=1;i<=F;i++) scanf("%d",fav+i);for(R int u,v,w,i=1;i<=C;i++){scanf("%d%d%d",&u,&v,&w);a[u][v]=a[v][u]=min(a[u][v],w);}Floyd();int minnum=0x3f3f3f3f,minid;for(R int i=1,sum=0;i<=P;i++,sum=0){for(R int j=1;j<=F;j++)sum+=dis[i][fav[j]];if(sum<minnum) minnum=sum,minid=i;}printf("%d\n",minid);return 0;}
Problem[2]:[JSOI2007]重要的城市
链接:https://www.luogu.org/problemnew/show/P1841
题面:
这一题比上一题略难,如何记录中转点?
Of course,在Floyd
的时候。
令\(c[i][j]\)为结点\(i,j\)之间的最短路的中转点,若无则为0;
在进行对\(dis[i][j]\)的更新之时,我们不直接取min
,而是先判断以避免覆盖。
因为我们还要进行一个更重要的操作,that is,更新\(c[i][j]\)!
分情况讨论:
1.\(dis[i][j]>dis[i][k]+dis[k][j]\)(需要更新):此时结点\(i,j\)之间的最短路的中转点就要发生改变,即\(c[i][j]=k\),并更新\(dis[i][j]\)的值。
2.\(dis[i][j]=dis[i][k]+dis[k][j]\) :这不仅说明原先的\(c[i][j]\)已经失效,而且意味着此时已经不存在\(c[i][j]\)了(并不需要中转点就有最短路了)。因此令\(c[i][j]=0\)。
3.\(dis[i][j]<dis[i][k]+dis[k][j]\):此时的中转点无法得到更优的解,忽略。
这样我们就处理好了\(c\)。对于结果的处理,可以利用桶排序的思想,令\(res[c[i][j]]=1\)。(res[N]:bool)
最后遍历\(res\),输出答案(别忘了无解的处理!!!)。
#include<cstdio>
#include<bitset>
#include<cstring>
using namespace std;const int N=205;
int c[N][N];
int f[N][N];
int n,m;void Solve()
{for(int i=1;i<=n;i++) f[i][i]=0;for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){if(i==j||j==k||i==k) continue;if(f[i][j]>f[i][k]+f[k][j])f[i][j]=f[i][k]+f[k][j],c[i][j]=k;else if(f[i][j]==f[i][k]+f[k][j])c[i][j]=0;}
}bool res[N],flag=0;
signed main()
{memset(f,0x3f3f3f3f,sizeof(f));memset(c,0,sizeof(c));scanf("%d%d",&n,&m);for(int u,v,w,i=1;i<=m;i++){scanf("%d%d%d",&u,&v,&w);f[u][v]=f[v][u]=w;//f:=dis}Solve();for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)res[c[i][j]]=1;for(int i=1;i<=n;i++)if(res[i]) printf("%d ",i),flag=1;if(!flag) puts("No important cities.");return 0;
}
Problem[3]:[NOI2007]社交网络
链接:https://www.luogu.org/problemnew/show/P2047
题面:
这题貌似比上一题更复杂——要进行路径计数。
令\(cnt[i][j]\)为结点\(i,j\)之间的最短路径条数。
还是在处理\(dis\)的时候处理\(cnt\)。
在分类讨论之前,你需要知道一件事情:
假设我们已经处理完了\(cnt[i][k]\)和\(cnt[k][j]\),那么怎么知道\(cnt[i][j]\)的值? 对于\(cnt[i][k]\)中的每条路径,与\(cnt[k][j]\)配对,有\(cnt[k][j]\)条路径。 那么\(cnt[i][k]\)条一起配对就是\(cnt[i][k]\times cnt[k][j]\)条,这就是\(cnt[i][j]\)的值。(说白了就是乘法原理)
那么,再开始分类讨论:
1.\(dis[i][j]=dis[i][k]+dis[k][j]\):又找到了
一坨解,\(cnt[i][j]+=cnt[i][k]*cnt[k][j]\)(注意是+=
!)。2.\(dis[i][j]>dis[i][k]+dis[k][j]\) :这代表有了新的解,原先的答案不能再用了,\(cnt[i][j]=cnt[i][k]*cnt[k][j]\)(注意是
=
!)。3.\(dis[i][j]<dis[i][k]+dis[k][j]\):此时的中转点无法得到更优的解,忽略。
处理完\(cnt\)后,那就意味着\(C_{s,t},C_{s,t}(v)\)什么的都出来了。
P.S.:建议cnt用double
。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;const int N=105;
int dis[N][N];
double cnt[N][N];
int n,m;signed main()
{memset(dis,0x3f3f3f3f,sizeof(dis));memset(cnt,0,sizeof(cnt));scanf("%d%d",&n,&m);for(int u,v,w,i=1;i<=m;i++){scanf("%d%d%d",&u,&v,&w);dis[u][v]=dis[v][u]=min(w,dis[u][v]);cnt[u][v]=cnt[v][u]=1;}for(int i=1;i<=n;i++)dis[i][i]=0;for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){if(i==j||i==k||j==k) continue;if(dis[i][j]==dis[i][k]+dis[k][j])cnt[i][j]+=cnt[i][k]*cnt[k][j];else if(dis[i][j]>dis[i][k]+dis[k][j]){dis[i][j]=dis[i][k]+dis[k][j];cnt[i][j]=cnt[i][k]*cnt[k][j];}}for(int k=1;k<=n;k++){double ans=0;for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){if(i==j||i==k||j==k) continue;if(dis[i][j]==dis[i][k]+dis[k][j])ans+=cnt[i][k]*cnt[k][j]*1.0/cnt[i][j];}printf("%.3lf\n",ans);}
}
Part III-EXT:最小环问题
来看这么一个问题:http://acm.hdu.edu.cn/showproblem.php?pid=1599
题面:
看似无从下手,但仍然是Floyd
!
在更新\(dis\)前,我们已经把\(1-(k-1)\)的情况处理好了。那么当前的最小环就是:
\[\min\{a[i][k]+a[k][j]+dis[i][j]\}\]
先贴代码:
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;const int N=105;
int a[N][N];
int dis[N][N];
int n,m;long long solve()
{long long min_circle=0x3f3f3f3f;for(int k=1;k<=n;k++){for(int i=1;i<k;i++)for(int j=i+1;j<k;j++)min_circle=min(min_circle,1ll*a[i][k]+a[k][j]+dis[i][j]);for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);}return min_circle;
}signed main()
{while(scanf("%d%d",&n,&m)!=EOF){memset(a,0x3f3f3f3f,sizeof(a));for(int u,v,w,i=1;i<=m;i++){scanf("%d%d%d",&u,&v,&w);a[u][v]=a[v][u]=min(a[u][v],w);}for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dis[i][j]=a[i][j];long long res=solve();if(res!=0x3f3f3f3f) printf("%lld\n",res);else puts("It's impossible.");}return 0;
}
值得注意的是,\(i,j\)的循环范围的控制,因为\(i,j,k\)不能相同。
至于为什么先处理最小环在更新路径,当然是为了使\(dis[i][j]\)不经过\(k\)啊。