Floyd算法多源最短路
Dk-1[i][j]表示从i到j的中间点不大于k-1的最短路径p:i…j,
哦,也就是我们把图给分裂开来,有多少对i,j就是分成多少图。
在一个分图中,
i到j开始没有中间节点。
最外层for循环一次加一个中间节点k。
第一次加k1,如果经过k1能使i到j变短,就变短了
不管有没有短,都是在中间节点有k1,的情况下i到j最短
由于每一个节点都是让所有的i,j组合有了这个在中间节点有k1时的结果
那k2节点加进来时
任何两点间,已经是在中间节点有k1所能达到的最短距离
——这个让我理解了一下午
再看经过k2是否让i到j更短
也就是有i,j,k1,k2时i到j的最短
直到所有的k都在其中了,就变成一个完整的图
i到j在中间节点有k1,k2,…,kn图里的最短路就出来了
注意到在i,j里恰好加入i点时,是不会更新的,相当于pass,还不用多判断一次k是不是i或j里
for(int k=0; k<n; k++)
for(int i=0; i<n; i++)
//一点点的优化:
//if(dist[i][k]==INF)continue;
for(int j=0; j<n; j++)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
实现:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<queue>
#ifdef LOCAL
FILE*FP=freopen("text.in","r",stdin);
//FILE*fp=freopen("text.out","w",stdout);
#endif
using namespace std;
#define ll long long
#define ld long double
#define pii pair<int,int>
#define piii pair<int,pii>
#define pll pair<ll,ll>
#define plll pair<ll,pll>
#define pdd pair<double,double>
#define pdi pair<double,int>
#define pid pair<int,double>
#define vi vector <int>
#define vii vector <vi>
#define st first
#define nd second
#define pb push_back
#define mem(a,b) memset(a,b,sizeof(a))
#define _forplus(i,a,b) for( register int i=(a); i<=(b); i++)
#define forplus(i,a,b) for( register int i=(a); i<(b); i++)
#define _forsub(i,a,b) for( register int i=(a); i>=(b); i--)
#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x,x.begin())
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define pi (acos(-1))
#define EPS 0.00000001
#define MOD 1000000007
#define fastio std::ios::sync_with_stdio(false);std::cin.tie(0);
#define N 205
int n,m,d[N][N],l,r;
int p[N][N];
//求多源最短Foryd ,要在m基础上直接上
void foryd(){
_forplus(i,1,n){
_forplus(j,1,n){
_forplus(k,1,n){
if(d[j][k]>d[j][i]+d[i][k]){
d[j][k]=d[j][i]+d[i][k];
p[j][k]=i;
}
}
}
}
}
int path[N];
int k;
void retrieve_path(int i,int j){
if(i==j)return;
if(p[i][j] == -1)
path[k++] = j;
else
{
retrieve_path(i, p[i][j]);
retrieve_path(p[i][j], j);
}//二分
}
//n为节点数,m是边数,d是邻接矩阵
int main(){
fastio
mem(d,0x3f);
mem(p,-1);
cin>>n>>m;
_forplus(i,1,m){
cin>>l>>r;
cin>>d[l][r];
}
_forplus(i,1,n){
d[i][i]=0;
}
//_forplus(i,1,n){
//_forplus(j,1,n){
// cin>>m[i][j];
//}
//}
foryd();
_forplus(i,1,n){
_forplus(j,1,n){
cout<<"从"<<i<<"到"<<j<<endl;
cout<<"距离:"<<d[i][j]<<endl;
path[0]=i;
k=1;
retrieve_path(i,j);
forplus(i,0,k){
cout<<path[i]<<"->";
}cout<<"end"<<endl;
}
}
return 0;
}
由下面bellman-fords算法想到的判断是否有负环的方法:
再来一次,碰到有更新就是有
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<queue>
#ifdef LOCAL
FILE*FP=freopen("text.in","r",stdin);
//FILE*fp=freopen("text.out","w",stdout);
#endif
using namespace std;
#define ll long long
#define ld long double
#define pii pair<int,int>
#define piii pair<int,pii>
#define pll pair<ll,ll>
#define plll pair<ll,pll>
#define pdd pair<double,double>
#define pdi pair<double,int>
#define pid pair<int,double>
#define vi vector <int>
#define vii vector <vi>
#define st first
#define nd second
#define pb push_back
#define mem(a,b) memset(a,b,sizeof(a))
#define _forplus(i,a,b) for( register int i=(a); i<=(b); i++)
#define forplus(i,a,b) for( register int i=(a); i<(b); i++)
#define _forsub(i,a,b) for( register int i=(a); i>=(b); i--)
#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x,x.begin())
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define pi (acos(-1))
#define EPS 0.00000001
#define MOD 1000000007
#define fastio std::ios::sync_with_stdio(false);std::cin.tie(0);
#define N 205
int n,m,d[N][N],l,r,t;
int p[N][N];
int flag=0;//是否是负权环,默认不是
//求多源最短Foryd ,要在m基础上直接上
void floyd(int tag){//tag 0是最短路,1是负权环
_forplus(i,1,n){
_forplus(j,1,n){
_forplus(k,1,n){
if(d[j][k]>d[j][i]+d[i][k]){
if(tag){
flag=1;return;
}
d[j][k]=d[j][i]+d[i][k];
p[j][k]=i;
}
}
}
}
}
int path[N];
int k;
void retrieve_path(int i,int j){
if(i==j)return;
if(p[i][j] == -1)
path[k++] = j;
else
{
retrieve_path(i, p[i][j]);
retrieve_path(p[i][j], j);
}//二分
}
//n为节点数,m是边数,d是邻接矩阵
int main(){
fastio
mem(d,0x3f);
mem(p,-1);
cin>>n>>m;
_forplus(i,1,m){
cin>>l>>r;
cin>>t;
t<d[l][r]?d[l][r]=t:t=t;
}
_forplus(i,1,n){
d[i][i]=0;
}
//_forplus(i,1,n){
//_forplus(j,1,n){
// cin>>m[i][j];
//}
//}
floyd(0);
floyd(1);//如果要判断是否负权环就用
if(flag){
cout<<"注意,有负权环,最短路为负无穷。"<<endl;
}else{
_forplus(i,1,n){
_forplus(j,1,n){
cout<<"从"<<i<<"到"<<j<<endl;
cout<<"距离:"<<d[i][j]<<endl;
path[0]=i;
k=1;
retrieve_path(i,j);
forplus(i,0,k){
cout<<path[i]<<"->";
}cout<<"end"<<endl;
}
}
}
return 0;
}
Floyd算法的抽象基础:Warshall算法
如果i能到k,k能到j,
则i能经过k到j。
这样子可以给一个图,得到任意两点是否联通的结果
可以用1表示联通,0表示不联通
还是先i,j,再一个一个加k
for(int k=0; k<n; k++)
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
if(to[i][k]&&to[k][j])to[i][j]=1;
学术上把一个一个加k的过程好像称为Rk 的k从0到n的过程,矩阵阶数增加
这个算法可能算k次重复的1,其实有第一次就可以了
怎么办,判断的话其实也是要做一次的
那就算了吧,节省vis数组节省空间
最短路变体:走k步能到达的最短
——不做优化,可以直接BFS
单源之Bellman-Ford算法
因为起点被固定了,我们现在只需要一个一维数组dist[]来存储每个点到起点的距离。
1为起点,我们初始化时把dist[1]初始化为0,其他初始化为INF。
松弛操作:
dist[y] = min(dist[y], dist[x] + e[x][y]);
//这里的e[x][y]表示x、y之间的距离,具体形式可能根据存图方法不同而改变
松弛操作就相当于考察能否经由x点使起点到y点的距离变短
设起点为S,终点为D,那这条最短路一定是S->P1->P2->…->D的形式,假设没有负权环,那这条路径上的点的总个数一定不大于n。
先松弛S, P1,此时dist[P1]必然等于e[S][P1]。
再松弛P1,P2,因为S->P1->P2是最短路的一部分,最短路的子路也是最短路(这是显然的),所以dist[P2]不可能小于dist[P1]+e[P1][P2],因此它会被更新为dist[P1]+e[P1][P2],即e[S][P1]+e[P1][P2]。
再松弛P2, P3,……以此类推,最终dist[D]必然等于e[S][P1]+e[P1][P2]+…,这恰好就是最短路径。
说得好像很有道理,但是问题来了,我怎么知道这些P1、P2是什么呢?我们不就是要找它们吗?关键的来了,Bellman-Ford算法告诉我们:把所有边松弛一遍!
因为我们要求的是最小值,而多余的松弛操作不会使某个dist比最小值还小。所以多余的松弛操作不会影响结果。把所有边的端点松弛完一遍后,我们可以保证S,
P1已经被松弛过了,现在我们要松弛P1, P2,怎么做呢?再把所有边松弛一遍!
好了,现在我们松弛了P1,
P2,继续这么松弛下去,什么时候是尽头呢?还记得我们说过吗?最短路上的点的总个数一定不大于n,尽管一般而言最短路上的顶点数比n少得多,但反正多余的松弛操作不会影响结果,我们索性:把所有边松弛n-1遍!
其实由Bellman-Ford算法是可以在n-1次有结果的,那么我们在第n次还能通过有的边更新,就有负权环了。
但是由于岔路,不能数组回溯找到该环,只能试图用最短路的树看是否成环。
先来一个只能判断是否有环的
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<queue>
#ifdef LOCAL
FILE*FP=freopen("text.in","r",stdin);
//FILE*fp=freopen("text.out","w",stdout);
#endif
using namespace std;
#define ll long long
#define ld long double
#define pii pair<int,int>
#define piii pair<int,pii>
#define pll pair<ll,ll>
#define plll pair<ll,pll>
#define pdd pair<double,double>
#define pdi pair<double,int>
#define pid pair<int,double>
#define vi vector <int>
#define vii vector <vi>
#define st first
#define nd second
#define pb push_back
#define mem(a,b) memset(a,b,sizeof(a))
#define _forplus(i,a,b) for( register int i=(a); i<=(b); i++)
#define forplus(i,a,b) for( register int i=(a); i<(b); i++)
#define _forsub(i,a,b) for( register int i=(a); i>=(b); i--)
#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x,x.begin())
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define pi (acos(-1))
#define EPS 0.00000001
#define MOD 1000000007
#define fastio std::ios::sync_with_stdio(false);std::cin.tie(0);
#define N 205
#define M 40005
int d[N],p[N];//点距离,来路
int f[M],t[M],e[M];//存边,距离,来,去
int n,m=0;//点数,边数(也可计,这里输入)
int flag=0,sub[N];//判断有无负权环,默认0无,1有 ,sub记录有负权环的点
void loop(int tag){//节省代码,0是最短路1是看负环——后来证明,暂时不能看负环
_forplus(i,1,n){
_forplus(j,1,m){
if(d[t[j]]>d[f[j]]+e[j]){
d[t[j]]=d[f[j]]+e[j];
if(!tag){
if(i==n){//处理负权环
flag=1;
//错误思想:找负权环
//如果我们要找出负权环,要多两个n的复杂度,因为有的枢纽有多方向,难以单个存
//loop(1);
//——如果有负权环,那么任意两点都可以通过负权环转无数次而距离无穷小
//所以判断会是,要么没有负权环,要么都被负权环影响,不要想找环了
break;
}
p[t[j]]=f[j];
}else{
sub[t[j]]=1;
}
}
}
}
}
void bford(int now){
mem(d,0x3f);
mem(p,-1);
mem(sub,0);
d[now]=0;
loop(0);
}
bool jd(int now){
int t=p[now];
while(t!=now&&~t){
if(sub[t])return true;
t=p[t];
}
return false;
}
void print(int now){
if(flag){
cout<<"注意,有负权环,最短路为负无穷。";
//_forplus(i,1,n){
// if(sub[i])cout<<" "<<i;
//}
cout<<endl;
return;
}
cout<<"这不是个有负权环的图"<<endl;
_forplus(i,1,n){
if(sub[i]){
cout<<i<<"点是负权环上的点"<<endl; continue;
}else if(jd(i)){
cout<<"此点是连在负权环上的点"<<endl;continue;
}
cout<<"从"<<now<<"到"<<i<<"的最短距离是:"<<d[i]<<endl;
cout<<"路径是:";
int t=i;
while(~t){
cout<<t<<"<-";
t=p[t];
}
cout<<"begin"<<endl;
}
}
int main(){
fastio
cin>>n>>m;//从1到n个节点
_forplus(i,1,m){
cin>>f[i]>>t[i]>>e[i];
}
int from=1;//起点
bford(from);
print(from);
return 0;
}
关于找负权环
由于枢纽的原因而不能通过数组找路的,请用一棵树做,是常见的
这次是在网上知乎看到的,下次要自己有这个意识
里面说用到LCT(Link Cut Tree),又和树链剖分有密切的联系,是一个大块头
找的时候,有个文章里提到用Tarjan 算法,这是一篇比较好的资料
接下来先学SPFA,再一起讨论怎么找负权环
将在这里可以找到