• [总结]Floyd算法及其应用



    一、Floyd算法

    如何求任意两点最短路?我们可以运行n次SPFA或Dijkstra求得,
    而Floyd算法能在(O(N^3))的时间复杂度内求出图中任意两点的最短路(多源最短路),且代码十分简短。

    Floyd算法(弗洛伊德算法)的本质是动态规划。设(f(k,i,j))表示"由若干个编号不超过k的节点中转后"(i)(j)的最短路。
    该"动态规划"有两个决策,一是经过编号不超过(k-1)的节点由(i)(j),二是先由(i)(k),再由(k)(j)
    我们很容易写出此时的转移方程:

    [f(k,i,j)=min(f(k-1,i,j),f(k-1,i,k)+f(k-1,k,j)) ]

    初始(f)数组所有值均为正无穷,随后令(f(0,i,j)=maps(i,j)),其中(map(i,j))为邻接矩阵。
    由于(k)是动态规划的阶段,因此(k)为最外层循环,可以得到如下代码:

    inline void floyd(){
    	for(int k=1;k<=n;k++)
    		for(int i=1;i<=n;i++)
    			for(int j=1;j<=n;j++)
    				f[k][i][j]=min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]);
    }
    

    算法最终(f(n,i,j),iin [1,n],jin [1,n])为最终答案。
    显然三维数组(f)在高强度数据下爆内存,使得该算法失去用途。我们发现,求出(f(k,i,j))只与(f)(k-1)层有关,因此我们可以用滚动数组的方式将第一维滚去,此时的转移方程为:

    [f(i,j)=min(f(i,j),f(i,k)+f(k,j)) ]

    最终(f(i,j),iin [1,n],jin [1,n])就保存了由(i)(j)的最短路。
    最终Floyd的主体部分:

    for(int k=1;k<=n;k++)
    	for(int i=1;i<=n;i++)
    		for(int j=1;j<=n;j++)
    			f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
    

    模板:

    #include<bits/stdc++.h>
    using namespace std;
    int f[100][100],n,m;
    int main()
    {
    	
    	scanf("%d%d",&n,&m);
    	memset(f,127,sizeof(f));
    	for(int i=1,u,w,v;i<=m;i++){
    		scanf("%d%d%d",&u,&v,&w);
    		f[u][v]=min(w,f[u][v]);
    	}
    	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++)
    					f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
    	for(int i=1;i<=n;i++){
    		for(int j=1;j<=n;j++)
    			printf("%d ",f[i][j]);
    		printf("
    ");
    	}
    	return 0;
    }
    

    二、Floyd算法的应用

    1. 传递闭包

    给出若干个元素以及他们的两两关系,如果这些元素具有传递性,我们就可以推出尽可能多的元素之间的关系。
    解决"利用元素的传递性求出尽可能多的元素的关系"这类问题的算法就叫做传递闭包。
    此时(f(i,j))数组的意义(这里为link数组)变为((i,j))是否具有关系,有关系(f(i,j)=1),无关系(f(i,j)=0)

    例如,利用传递闭包可以快速地求出图中两点是否可以直接/间接到达。

    #include<bits/stdc++.h>
    using namespace std;
    int link[1000][1000];
    int main()
    {
    	int m,n,u,v,w,ans=0;
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=m;i++){
    		scanf("%d%d%d",&u,&v,&w);
    		link[u][v]=1;//u->v可达
    	}
    	for(int i=1;i<=n;i++) link[i][i]=1;//自己到自己可达
    	for(int k=1;k<=n;k++)
    		for(int i=1;i<=n;i++)
    			for(int j=1;j<=n;j++){
    				link[i][j]=link[i][j]||(link[i][k]&&link[k][j]);
    				//link[i][j]=link[i][j]|(link[i][k]&link[k][j]);
    				//这两种写法都可行,具体解释为i,j既可以经过不大于k-1的节点连通,也可以由i经过k中转到j来连通。
    			}
    	for(int i=1;i<=n;i++){//某两点是否可达 
    		for(int j=1;j<=n;j++){
    			printf("%d ",link[i][j]);
    		}
    		printf("
    ");
    	}
    	return 0;
    }
    

    除了这种方法,我们还可以使用(bitset)代替这个二维数组。
    设:(bitset<1000> link[1000]),为长度为1000的一维数组,其中每一格都有长度为1000的二进制串。
    其中link可以直接调用这个二进制串的下标,例如(link[3][5];)
    此外需要注意的是二进制串中只能存储0或1。

    以下列举一些可能会用到的函数:
    bitset<100> bs;
    bs.count();//返回bs串中1的个数
    bs.size()//返回bs串的位数
    bs.any()//返回bs串是否有1; bs.none()//返回bs串是否没有1
    bs.set()//将bs串全部位变为1; bs.set(p)//将p+1位变为1
    bs.reset()//将bs串全部位变为0; bs.reset(p)//将p+1位变为0
    bs.flip()//将bs串取反(flip v.快速翻转); bs.flip(p)//只将p+1位取反

    利用(bitset)实现的传递闭包(核心代码):

    int m,n,u,v;
    int ans=0;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
    	scanf("%d%d",&u,&v);
    	link[u][v]=1;
    }
    for(int i=1;i<=n;i++) link[i][i]=1;//自身到自身可达 
    for(int i=1;i<=n;i++)//Floyd 
    	for(int j=i+1;j<=n;j++){
    		if(link[i][j])//如果i-->j有连接 
    			link[i]=link[i]|link[j];//那么与i连通的点与j也会连通
    	}
    

    例1:P2881 [USACO07MAR]排名的牛Ranking the Cows

    相当于给定一张有向图,求出还需要加多少条边才能使这个图连通。
    设x强于y,y强于z,那么一定x强于z,由于具有传递性,因此可以用传递闭包来做。
    Code:

    #include<bits/stdc++.h>
    using namespace std;
    bitset<1010> link[1010];
    int main()
    {
    	int m,n,x,y,ans=0;
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=m;i++){
    		scanf("%d%d",&x,&y);
    		link[x][y]=1;
    	}
    	for(int i=1;i<=n;i++) link[i][i]=1;
    	for(int k=1;k<=n;k++)
    		for(int i=1;i<=n;i++){
    			if(link[i][k])
    				link[i]=link[i]|link[k];
    		}
    	for(int i=1;i<=n;i++)	ans=ans+link[i].count();
    	ans-=n;//减去自己与自己连通的边数
    	printf("%d",n*(n-1)/2-ans);
    	return 0;
    }
    

    例2:P2419 [USACO08JAN]牛大赛Cow Contest

    每头牛编程强度同样具有传递性,因此可以确定尽量多的每头牛之间的两两关系。
    如果一头牛与其他的任意一头牛都能确定关系,那么我们就能知道这头牛的能力排名。
    Code:

    #include<bits/stdc++.h>
    using namespace std;
    int f[120][120];
    int main()
    {
    	int m,n;
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=m;i++){
    		int u,v;
    		scanf("%d%d",&u,&v);
    		f[u][v]=1;
    	}
    	for(int k=1;k<=n;k++)
    		for(int i=1;i<=n;i++)
    			for(int j=1;j<=n;j++){
    				f[i][j]=f[i][j]|(f[i][k]&f[k][j]);
    		}
    	int ans=0;
    	for(int i=1;i<=n;i++){
    		int cnt=1;
    		for(int j=1;j<=n;j++){
    			if(i==j) continue;
    			if(!f[i][j]&&!f[j][i]){//与其他牛的关系都能确定
    				cnt=0;break;
    			}
    		}
    		ans+=cnt;
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    2.快速求出多源最短路

    例1:P1522 牛的旅行 Cow Tours

    利用Floyd算法快速求出任意两点的最短路,以便求出任一点到其他点的最远距离。
    此时对不连通的牧场增添一条边来更新直径。

    #include<bits/stdc++.h>
    #define INF 0x3f3f3f3f
    #define N 200010
    using namespace std;
    double f[200][200],x[200],y[200];
    double len[200],glen,glen2=INF;
    int n;char ch;
    inline double calc_dis(int a,int b){
    	return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));
    }
    int main()
    {
    	scanf("%d",&n);
    	for(int i=1;i<=n;i++) scanf("%lf%lf",&x[i],&y[i]);
    	for(int i=1;i<=n;i++)//初始化f数组 
    	 	for(int j=1;j<=n;j++){
    	 		cin>>ch;
    			if(ch=='1') f[i][j]=calc_dis(i,j);
    			else if(i!=j) f[i][j]=INF; 
    	 	}
    	for(int k=1;k<=n;k++)//最短路 
    		for(int i=1;i<=n;i++)
    	 		for(int j=1;j<=n;j++){
    	 			f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
    	 		}
    	for(int i=1;i<=n;i++){ 
    		for(int j=1;j<=n;j++) if(f[i][j]<INF) len[i]=max(len[i],f[i][j]);//求出该点到其他点的最大距离 
    		glen=max(glen,len[i]);//求出原图的直径 
    	} 
    	for(int i=1;i<=n;i++)
    		for(int j=1;j<=n;j++)//对不连通的牧场连一条边 
    			if(f[i][j]>=INF) glen2=min(glen2,len[i]+calc_dis(i,j)+len[j]);//求出连边后最小的直径 
    	cout<<fixed<<setprecision(6)<<max(glen2,glen);
    	return 0;
    }
    
    

    3.解决双权值问题

    在解决双权值问题时,如果我们同时考虑两个权值会显得很麻烦。
    此时应该枚举一个权值再计算另外一个权值。

    利用Floyd算法解决双权值问题的核心在于巧妙地变化最外层循环k的含义。
    我们之前已经提到过,Floyd求出(i)(j)的最短路是不断由节点(k)中转更新得到的,即(i)(j)的最短路经过节点不断(1,2,...,k-1,k)松弛得到。
    此时相当于不断枚举节点k,来更新i,j的最短路。

    例1:P1119 灾后重建

    每个村庄都有建成的时间,同时询问节点(x)到节点(y)通车最短距离。
    我们知道原Floyd算法中(k)的意义是:

    • 对于此时枚举的(k),任意节点(i)(j)只允许通过编号不大于(k)的节点更新最短路。

    因为本题中有只有在询问的时间内修好的村庄才允许通车,此时(k)的意义为:

    • 对于此时枚举的(k),任意节点(i)(j)只允许通过建成时间小于询问时间的节点更新最短路。

    这与Floyd算法中(k)循环的思路相同,因此可以使用Floyd算法解决。
    剩余需要注意的地方:

    1. 本题包含节点0。
    2. 由于题目的询问时间具有单调性,所以只要不断枚举此时可用于更新的节点(k)即可。
    3. (i)(j)经过节点(k)更新,则节点(k)需要打上标记避免下次重复无意义地进入循环(会TLE)。

    Code:

    #include<bits/stdc++.h>
    #define INF 0x3f3f3f3f 
    using namespace std;
    int vis[201],n,m,q;;
    int t[201],f[201][201],tot;
    int from[50001],to[50001],day[50001];
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++) scanf("%d",&t[i]);//第i个村庄修好的日期
    	for(int i=0;i<n;i++) for(int j=0;j<n;j++) f[i][j]=INF;
    	for(int i=1,u,v,w;i<=m;i++){
            scanf("%d%d%d",&u,&v,&w);
            f[u][v]=f[v][u]=w;
        }
        for(int i=0;i<n;i++) f[i][i]=0;
        scanf("%d",&q);
        for(int i=1;i<=q;i++)//询问 ,第day[i]天从from[i]到to[i]的最短路
            scanf("%d%d%d",&from[i],&to[i],&day[i]);
        for(int num=1;num<=q;num++){//对于每个询问
            for(int k=0;k<n;k++)
                if(t[k]<=day[num]&&!vis[k]){//i,j经由建成时间小于询问时间的节点k来更新最短路
                    vis[k]=1;//做标记
                    for(int i=0;i<n;i++)
                        for(int j=0;j<n;j++)
                            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);//求得最短路
                }
            if(f[from[num]][to[num]]!=INF&&t[from[num]]<=day[num]&&t[to[num]]<=day[num])
            //此时询问的节点连通,且起点与终点的建成时间不大于询问的时间才有答案
                printf("%d
    ",f[from[num]][to[num]]);
            else printf("-1
    ");//不能通车
        }
        return 0;
    }
    

    pic.png

  • 相关阅读:
    Androidstudio 使用git插件提交代码
    androidstudio上传代码到git上
    tcpdump的简单使用
    使用nmap工具查询局域网某个网段正在使用的ip地址
    网段的划分
    jenkins配置源码管理git
    shell条件测试test
    shell简单用法笔记(shell中数值运算)二
    shell简单用法笔记(一)
    如何解决audiodg占用内存高(停止与重启audiodg服务)
  • 原文地址:https://www.cnblogs.com/cyanigence-oi/p/11826560.html
Copyright © 2020-2023  润新知