Floyd算法的应用:
1.最短路
2.传递闭包
3.找最小环
4.恰好经过k条边的最短路(倍增)
1125. 牛的旅行(活动 - AcWing)
思路:这道题的图差不多就是有若干块,块儿内部是连通的,块与块儿之间没有边。我们要选择两块中的两点 ,在它们之间连一条边,也即将两块合并成一块,定义块中距离最远的两点的距离作为直径,要找出所有块中的最大直径。
那么该如何写呢?首先我们要将每一块儿内部点与点之间的距离算出来,然后才能找最大值,然后要选两点连边,很显然连完之后的新直径就是这两点原来的直径加上新的直径。所以我们要把原来每块中的每个点出去的最大直径作为一个属性附加到这个点上。
由于我们需要知道任意两点之间的距离,所以用Floyd算法。
#include<bits/stdc++.h>
using namespace std;
const int N=200;
typedef pair<double,double> pdd;
pdd q[N];
string g[N];
int n;
double d[N][N],mx[N];
const double inf=1e20;
double getd(pdd a,pdd b)
{
double dx=a.first-b.first,dy=a.second-b.second;
return sqrt(dx*dx+dy*dy);
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%lf%lf",&q[i].first,&q[i].second);
for(int i=0;i<n;i++) cin>>g[i];
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
if(i==j) d[i][j]=0;
else if(g[i][j]=='1') d[i][j]=getd(q[i],q[j]);
else d[i][j]=inf;
}
}
for(int k=0;k<n;k++)
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
double r1=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
if(d[i][j]<inf/2)
mx[i]=max(mx[i],d[i][j]);
}
r1=max(r1,mx[i]);
}
double r2=inf;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
if(d[i][j]>inf/2) r2=min(r2,mx[i]+mx[j]+getd(q[i],q[j]));
}
}
double ans=max(r1,r2);
printf("%.6lf",ans);
}
343. 排序(343. 排序 - AcWing题库)
思路:上述的推导关系式是闭包运算,很容易想到Floyd算法,因为Floyd算法也是这样借助中间某个点进行转化。这题还需要知道是第几次出现矛盾的,所以我们可以每新增一种关系就进行一遍Floyd,反正数据范围很小。
然后考虑一下细节,Floyd怎么更新,d[i][j]=1表示i<j,d[i][k]&d[k][j]==1的话能够推出来d[i][j]=1,如果原本就有d[i][j]==1,那么就无所谓,如果没有这个关系的话把它加上即可。所以这里可以用或运算(|).
检查的时候,如果出现矛盾,就是d[i][i]==1,那么就返回2,如果不能确定即
如果是有序的,那么顺序该如何找,显然我们可以写个嵌套循环,对于每个i,去判断是否没有任何一个没输出过的j小于它,如果是,那么它就是当前最小的。
#include<bits/stdc++.h>
using namespace std;
const int N=30;
int d[N][N],st[N];
int n,m;
int g[N][N];
void floyd()
{
memcpy(d,g,sizeof g);
for(int k=0;k<n;k++)
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
d[i][j] |= d[i][k]&d[k][j];
}
}
}
}
int check()
{
for(int i=0;i<n;i++)
if(d[i][i]) return 2;
for(int i=0;i<n;i++)
for(int j=0;j<i;j++)//下面确实是双向的,所以这里要有限制
if(!d[i][j]&&!d[j][i]) return 0;
return 1;
}
void get_min()
{
for(int i=0;i<n;i++)
{
if(st[i]) continue;
int flag=1;
for(int j=0;j<n;j++)
{
if(!st[j]&&d[j][i])
{
flag=0;
break;
}
}
if(flag)
{
st[i]=1;
char c='A'+i;
cout<<c;
break;
}
}
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
if(!n&&!m) break;
memset(g,0,sizeof g);
int t,type=0;
for(int i=1;i<=m;i++)
{
string s;
cin>>s;
int a=s[0]-'A',b=s[2]-'A';
if(type) continue;
g[a][b]=1;
floyd();
type=check();
if(type) t=i;
}
if(type==0) printf("Sorted sequence cannot be determined.");
else if(type==2) printf("Inconsistency found after %d relations.",t);
else
{
memset(st,0,sizeof st);
printf("Sorted sequence determined after %d relations: ",t);
for(int i=0;i<n;i++) get_min();
printf(".");
}
printf("\n");
}
}
344. 观光之旅(344. 观光之旅 - AcWing题库)
思路:题目的意思很清楚,需要求一个环,环的要求是边的长度之和最小。
这里我们可以从集合的角度来考虑这个问题,我们首先就是要进行分类,我们可以按照环中编号最大点的编号来分。然后只需求出每一类的最小值,然后在这些最小值中取一个min即可。思路不是很麻烦,但是这个思路如何实现呢?
这里我们来从Floyd的角度来考虑:
Floyd算法最核心的一点就是利用三重循环去更新d[i][j]
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
那么我们仔细想一想,当循环到第k层的时候,d[i][j]的意思是什么呢,显然是从i到j只经过前k-1个点的最短路,如下图:
如果我们加进第k个点建环会是什么样的呢?
我们将这个环归结到第k类, 那么便可由此求出1-n所有的点作为第k个点的环,然后就可求最小值。
这题还有一个难点在于需要将环输出,这个其实可以由最短路的特性得到,我们来想一下,虽然是分了n类来看,但是我们实际上只要维护一个变量来存最小值即可,最小值每次更新的时候,就说明有一个环了,我们可以用一个数组来存这个环,显然环上有i,j,k三点,那么问题就转化成中间的那些路径该怎么求,中间的那些是如何得到的呢,显然是在更新d[i][j]的时候得到的,那么我们只需要记录一下d[i][j]是被哪个中间点更新的即可还原路径(用递归实现)。
对了这里还有一点,我们更新res的循环和更新d[][]的循环要在k循环中分开写,另外为了保证k是编号最大的节点,我们在更新res时i和j的范围不应该超过k。
#include<bits/stdc++.h>
using namespace std;
const int N=120,M=10010;
int g[N][N],pos[N][N],d[N][N],path[M];
int n,m,cnt;
void getpath(int i,int j)
{
if(pos[i][j]==0) return;
int k=pos[i][j];
getpath(i,k);
path[cnt++]=k;
getpath(k,j);
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
for(int i=1;i<=n;i++) g[i][i]=0;
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b]=g[b][a]=min(g[a][b],c);
}
memcpy(d,g,sizeof g);
int res=0x3f3f3f3f;
for(int k=1;k<=n;k++)
{
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++)
if((long long)d[i][j]+g[k][i]+g[j][k]<res)
{
res=d[i][j]+g[k][i]+g[j][k];
cnt=0;
path[cnt++]=k;
path[cnt++]=i;
getpath(i,j);
path[cnt++]=j;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(d[i][k]+d[k][j]<d[i][j])
{
d[i][j]=d[i][k]+d[k][j];
pos[i][j]=k;
}
}
if(res==0x3f3f3f3f) printf("No solution.\n");
else
{
for(int i=0;i<cnt;i++) printf("%d ",path[i]);
printf("\n");
}
}
ps:这题有几个细节需要注意一下,0x3f3f3f3f=1061109567,而int的最大值是(2^31)-1=2147483647,d[i][j]+g[j][k]+g[k][i]这一步可能会有三个0x3f3f3f3f相加,故而会爆int,我们可以强制转换成long long类型,在int和long long比较的时候,会自动给int升级,但是不会直接改变int的类型。
345. 牛站(活动 - AcWing)
思路:本题考查的是Floyd的倍增算法 .
首先看到题目是有边数限制的最短路,很容易想到bellman_ford算法,但是我们计算一下时间复杂度应该为O(N*2*T),那么最坏会到2e8,但是题目给的时间只有1s,一般oj的效率差不多是1s内计算1e7-1e8次,这里会超时,所有bellman——ford虽好,抵不住时间限制太严。
所以我们另辟蹊径,用Floyd倍增算法来实现,这个算法和常规的Floyd算法区别很大,常规算法d[k,i,j]中k表示的是经过点k来中转(k的这一维一般可以不写),这里d[a+b,i,j]表示的是从i到j经过a+b条边的最短路。那么答案就是d[N,S,E]。现在的问题就是这个该如何更新,这里有一个特别巧妙地地方,如下图:
a和b的值可以任取,因为我们由上图发现,i经过a条边到k,k经过b条边到j,前后是互不影响的,所以相当于满足结合律,也就是我们先算前a条边再算后b条边,与先算前a+1条边和后b-1条边是等价的。满足结合律的话,我们就可以使用倍增算法(快速幂的原理)。
这里的倍增实际上十分巧妙,和快速幂那里的倍增实际上很像。那么需要考虑的就是mul函数怎么写。并不是用已经更新过的点来更新别的点,而是类似于bellman_ford算法,用指定的长度数组去更新别的数组,这样就能保证,每次更新只扩展出我们想要的边数。
然后还有一点需要注意,这里实际上点数最多200,但是编号却到1000,所以需要映射一下,可以用map来实现。
#include<bits/stdc++.h>
using namespace std;
const int N=210;
int res[N][N];
int k,n,m,s,t;
int g[N][N];
void mul(int c[][N],int a[][N],int b[][N])
{
int tmp[N][N];
memset(tmp,0x3f,sizeof tmp);
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
tmp[i][j]=min(tmp[i][j],a[i][k]+b[k][j]);
}
}
}
memcpy(c,tmp,sizeof tmp);
}
void qmi()
{
memset(res,0x3f,sizeof res);
for(int i=1;i<=n;i++) res[i][i]=0;
while(k)
{
if(k&1) mul(res,res,g);
mul(g,g,g);
k >>= 1;
}
}
int main()
{
scanf("%d%d%d%d",&k,&m,&s,&t);
map<int,int>mp;
if(!mp.count(s)) mp[s]=++n;
if(!mp.count(t)) mp[t]=++n;
s=mp[s],t=mp[t];
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>c>>a>>b;
if(!mp.count(a)) mp[a]=++n;
if(!mp.count(b)) mp[b]=++n;
a=mp[a],b=mp[b];
g[a][b]=g[b][a]=min(c,g[a][b]);
}
qmi();
cout<<res[s][t];
}
ps:这里总结一下,我们在更新的时候,可以通过更新数组的设置得到不同边数的更新。