Floyd算法及其应用

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

题面:

ZgPxGd.png

此题较为简单,算法流程:

  • Floyd处理出各个点间的最短路径。

  • 计算出每个点到各个Favorites的总距离。

  • 选出总距离最小的点输出即可。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define R register

int 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

题面:

Zg85Js.png

这一题比上一题略难,如何记录中转点?

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

题面:

ZgJVBT.png

这题貌似比上一题更复杂——要进行路径计数。

\(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

题面:

Z2P9oD.png

看似无从下手,但仍然是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\)啊。

转载于:https://www.cnblogs.com/-Wallace-/p/11165803.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值