最短路各方法总结&&变体思考(未完成)

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算法

Pecco的
更容易懂

因为起点被固定了,我们现在只需要一个一维数组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,再一起讨论怎么找负权环
将在这里可以找到

单源最短路之SPFA
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值