经典三大最短路方法(最短路计数,判负环,无向图求最小环)

一.最短路的常用算法

  • f l o y e d : O ( n 3 ) floyed:O(n^3) floyed:O(n3) 经典的多源最短路算法,基本思想为动态规划 ,可用于负权边
  • s p f a : O ( k m ) spfa:O(km) spfaO(km) 单源最短路算法,基本思想为广度优先搜索 使用与负权边,缺点易被特殊数据卡时间复杂度
  • d i j k s t r a : O ( n l o g n ) dijkstra:O(nlogn) dijkstraO(nlogn) 经典单源最短路算法,基本思想为广度优先搜索 不适用于负权边,时间复杂度优于spfa的原因是其每个点只入堆一次,vis只变1,不变0
#include<bits/stdc++.h>
using namespace std;
const int N=1e5,M=1e6;
struct E{
	int u,v,c,next;
}e[M*2];
int vex[N],k,que[M*4],vis[N],dis[N],head,rear;
int n;

void add(int u,int v,int c) {
	k++;
	e[k].v=v;
	e[k].u=u;
	e[k].c=c;
	e[k].next=vex[u];
	vex[u]=k;
	return;
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int> > >q;
void Dij(){
    for(int i=1;i<=n*;i++)dis[i]=INT32_MAX;
    dis[1]=0;
    q.push(make_pair(dis[1],1));
    vis[1]=1;
    while(!q.empty()){
        int u=q.top().second;
        q.pop();
        vis[u]=1;
        for(int i=vex[u];i;i=e[i].next){
            int v=e[i].v;
            if(dis[v]>dis[u]+e[i].w){
                dis[v]=dis[u]+e[i].w;
                q.push(make_pair(dis[v],v));
            }
        }
    }
    for(int i=1;i<=n;i++){
        if(vis[i]==0)dis[i]=-1;
        cout<<dis[i]<<" ";
    }
}
void spfa(int s,int t) {
	for(int i=1; i<=n; i++)dis[i]=1e9;
	dis[s]=0;
	que[rear++]=s;
	while(head<rear) {
		int u=que[head];
		vis[u]=0;
		for(int i=vex[u]; i; i=e[i].next) {
			int v=e[i].v;
			if(dis[v]>dis[u]+e[i].c) {
				dis[v]=dis[u]+e[i].c;
				if(!vis[v]) {
					que[rear++]=v;
					vis[v]=1;
				}
			}
		}
		head++;
	}
	cout<<dis[t];
}
void floyed(int s,int t){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i!=j)dis[i][j]=1e9;
			else dis[i][j]=0;
		}
	}
	for:dis[u][v]=c//初始化距离
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				dis[i][j]=min(dis[i][k]+dis[k][j]); 
			}
		}
	} 
	cout<<dis[s][t];
}
int main() {
	int m,s,t,u,v,c;
	cin>>n>>m>>s>>t;
	for(int i=1; i<=m; i++) {
		cin>>u>>v>>c;
		add(u,v,c);
		add(v,u,c);
	}
	dj(s,t);
}

二.单源最短路例题

例题1:区间dp+单源最短路

例题链接

  • 题目描述: m 个点构成一张无向图,有边权表示距离。起点为 1 ,终点为 m 。d 个限制 ( u , s , t ) (u,s,t) (u,s,t) 表示点 u u u 不能在 s s s t t t 天中的任意一天使用。n 天,每天都要从起点 1 走到终点 m 一次。如果更改路线,则需要增加 p 的距离。求完成这 n 次从 1 到 n 的最短路,至少距离走多少的总距离。例如:前两天走的路径是一种,后两天走的路径是另一种,那么其总距离为:四条路径之和 + p 。 n ∈ [ 1 , 100 ] , m ∈ [ 1 , 20 ] n\in[1,100],m\in[1,20] n[1,100],m[1,20]
  • 状态设置: f [ l ] [ r ] f[l][r] f[l][r] 表示完成第 l 天到第 r 天,一共需要的最小距离。
  • 转移方程: f [ l ] [ r ] = m i n ( f [ l ] [ r ] , f [ l ] [ k ] + f [ k + 1 ] [ r ] + p ) , k ∈ [ l , r ) f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+p),k\in[l,r) f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+p),k[l,r)
  • 初始值: f [ l ] [ r ] = s p f a ( l , r ) f[l][r]=spfa(l,r) f[l][r]=spfa(l,r)
#include<bits/stdc++.h>
using namespace std;
const int N=105,M=1e5+10;
struct E {
	int u,v,w,next;
} e[M];
int vex[N],vis[N],tot,lim[N],ban[N][N],m;
long long dis[N],f[N][N];
void add(int u,int v,int w) {
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].w=w;
	e[tot].next=vex[u];
	vex[u]=tot;
}
long long spfa(int s,int t) {
	for(int i=1; i<=m; i++) {
		vis[i]=0;
		dis[i]=1e16;
		lim[i]=0;
	}
	dis[1]=0;
	for(int i=1; i<=m; i++) {
		for(int j=s; j<=t; j++) {
			if(ban[i][j])lim[i]=1;
		}
	}
	queue<int>q;
	if(lim[1]==0)q.push(1);
    while(!q.empty()){
    	int u=q.front();
    	vis[u]=0;
    	for(int i=vex[u];i;i=e[i].next){
    		int v=e[i].v;
    		if(lim[v])continue;
    		if(dis[v]>dis[u]+e[i].w){
    			dis[v]=dis[u]+e[i].w;
    			if(vis[v]==0){
    				q.push(v);
    				vis[v]=1;
				}
			}
		}
		q.pop();
	}
	return dis[m]*(t-s+1);
}
int main() {
	int n,p,e,u,v,w,d,s,t;
	cin>>n>>m>>p>>e;
	for(int i=1; i<=e; i++) {
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	cin>>d;
	for(int i=1; i<=d; i++) {
		cin>>u>>s>>t;
		for(int j=s; j<=t; j++) {
			ban[u][j]=1;
		}
	}
	for(int i=1; i<=n; i++)f[i][i]=spfa(i,i);
	for(int len=2; len<=n; len++) {
		for(int l=1,r=l+len-1; r<=n; l++,r++) {
			f[l][r]=spfa(l,r);
			for(int k=l; k<r; k++)f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+p);
		}
	}
	cout<<f[1][n];
}

三.floyd 的深入学习

随着时间新开点

前言

  • floyd的算法精髓,其实就是通过中转点的依次增多,来松弛这张完全图的所有多源最短路的最短路径。
  • 那么floyd就会很适合这样的一个问题:点最初关闭,随着时间的推移依次允许使用。那么我们以时间为顺序,依次根据这些开启的点作为中专点,去松弛最短路径。

例题1:随着时间新开点

  • 题目描述: n 个点,m 条边,构成一张无向连通图。每个点在第 t i t_i ti 天被建造,只有建造后的点才能通过。q个询问,每次查询 u 到 v 的第 t 天的最短路。保证 t 递增, t i t_i ti 也递增。
  • 问题分析: 很显然,我们可以通过当前询问的天数,去开启新的中介点,用这些中介点去松弛最短路径。
while(q--){
	cin>>x>>y>>t;
	for( k=k; tim[k]<=t&&k<n; k++) {
		for(int i=1; i<=n; i++) {
			for(int j=1; j<=n; j++) {
				g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
			}
		}
	}
	if(t<max(tim[x],tim[y]))cout<<-1<<endl;
	else if(g[x][y]==1e9)cout<<-1<<endl;
	else cout<<g[x][y]<<endl;
}

例题2:随着 w i w_i wi 新开点

  • 题目描述: n 个点,m 条边,构成一张无向连通图。每个点有一个 w i w_i wi 值。q 个询问每次查询 u 到 v 的最短路径,且该路径不能经过 w i > w u w_i>w_u wi>wu 的点。
  • 问题分析: 我们对 q 次询问的 w i w_i wi 从小到大排序,依次对询问解决。那么 q 增大的过程,其实就是中介点依次开放的过程。与例题1异曲同工。

floyd求路径数量(运用floyd思想,本质是矩阵ksm)

前言

  • 已知图的邻接矩阵为 A n n A_{nn} Ann(其中 A [ i ] [ j ] = 1 / 0 A[i][j]=1/0 A[i][j]=1/0 表示 i i i j j j 是否有路径)。根据 f l o y d floyd floyd 的思想, A n n k A_{nn}^k Annk A [ i ] [ j ] A[i][j] A[i][j] 就是 i i i k k k 步到 j j j ,一共有多少种方案。

例题1:求起点1走 t 步后的方案总数

  • 题目描述: n 个点,m 条无向边。起点为 1 。每步他可能有三次操作:向其连边的点走去;原地不动;自爆。 n ∈ [ 1 , 30 ] , m ∈ [ 1 , 100 ] , t ∈ [ 1 , 1 e 6 ] n\in[1,30],m\in[1,100],t\in[1,1e6] n[1,30],m[1,100],t[1,1e6]
  • 问题分析:
  • A n n t A_{nn}^t Annt 中的 A [ i ] [ j ] A[i][j] A[i][j] 即为 i i i t t t 步到 j j j 的方案数。那么答案就是 ∑ i = 1 n A [ 1 ] [ i ] \sum_{i=1}^n A[1][i] i=1nA[1][i]
  • 原地不动的情况: A [ i ] [ i ] = 1 , i ∈ [ 1 , n ] A[i][i]=1,i\in[1,n] A[i][i]=1,i[1,n]
  • 自爆: A [ i ] [ 0 ] = 1 , i ∈ [ 1 , n ] A[i][0]=1,i\in[1,n] A[i][0]=1,i[1,n],这样走到 0 就走不出来了
  • 注意: A [ 0 ] [ 0 ] = 1 A[0][0]=1 A[0][0]=1
#include<bits/stdc++.h>
using namespace std;
const int N=32;
long long n,m,t,mod=2017;

struct Matrix {
	long long a[N+1][N+1];
	Matrix(){
		for(int i=1;i<=N;i++)a[i][i]=1;
	}
	Matrix operator *(const Matrix &obj){
		Matrix ret;
		for(int i=0; i<=n; i++) {
			for(int j=0; j<=n; j++) {
                ret.a[i][j]=0;
				for(int k=0; k<=n; k++) {
					ret.a[i][j]=(ret.a[i][j]+a[i][k]*obj.a[k][j])%mod;
				}
			}
		}
		return ret;
	}
}a,ans;
void ksm(long long b) {
	while(b) {
		if(b&1)ans=ans*a;
		a=a*a;
		b=b>>1;
	}
}
int main(){ 
    int u,v;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
    	cin>>u>>v;
    	a.a[u][v]=a.a[v][u]=1;
	}
	cin>>t;
	for(int i=1;i<=n;i++)a.a[i][0]=1;
	for(int i=1;i<=n;i++)a.a[i][i]=1;
	ksm(t);
	
	long long sum=0;
	for(int i=0;i<=n;i++)sum=(sum+ans.a[1][i])%mod;
	cout<<sum;
	return 0;
}

floyd判最小环

例题链接

  • 题目描述: 给定一张无向图,求图中一个至少包含 3 个点的环,环上的节点不重复,并且环上的边的长度之和最小,求最小权值和。
  • 问题分析: 使用 Floyd 算法 O ( n 3 ) O(n^3) O(n3) 求最小环。Floyd 在枚举第 k 个点作为中介点时,已经求出了只有前 k-1 个点的最短路。枚举每对 u,v,已知 u,v 的最短路,则 u 再通过 k 到达 v 就可以构成一个环,求这个环的最小值即可。
#include<bits/stdc++.h>
using namespace std;

long long mp[105][105],f[105][105];

int main(){
	int n,m,u,v,c;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			mp[i][j]=1e14;
			f[i][j]=1e14;
		}
	}
	for(int i=1;i<=m;i++){
		cin>>u>>v>>c;
		mp[u][v]=c;
		mp[v][u]=c;
		f[u][v]=c;
		f[v][u]=c;
	}
	long long ans=1e14;
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=i+1;j<=n;j++){
			    ans=min(ans,f[i][j]+mp[i][k]+mp[k][j]);
			}
		}
		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]);
			} 
		}
	}
	if(ans>1e10)cout<<"No solution.";
	else cout<<ans;
	return 0;
}

四.SPFA的深入学习

最短路计数

  • 如果从 u 到 v 的路径和原来到 v 的最短路径一致,则通向 u 的最短路均为通向 v 的最短路,到 v 的最短路累加上到 u 的最短路即可。
  • 如果从 u 到 v 的路径更近,则通向 v 的最短路等于通向 u 的最短路
void spfa(int s) {
	for(int i=1; i<=n; i++)dis[i]=1e9;
	dis[s]=0;
	num[s]=1;
	q.push(s);
	while(!q.empty()) {
		int u=q.top();
		vis[u]=0;
		for(int i=vex[u]; i; i=e[i].next) {
			v=e[i].v;
			if(dis[v]>dis[u]+e[i].c) {
				dis[v]=dis[u]+e[i].c;
				num[v]=num[u];
				if(!vis[v]) {
					vis[v]=1;
					q.push(v);
				}
			} else if(dis[v]==dis[u]+e[i].c)num[v]+=num[u];
		}
		q.pop();
	}
}

spfa判负环

  • 要知道,dij是不能跑负权边的。因此判负环只能用 spfa
  • 如果存在负环,该图是没有最短路的,并且 bfs 会进入死循环
  • 原理: 如果图中不存在负权环,则从 i 到每个点的最短路经过的点数均不会大于n。
  • 因此,只需要在 spfa 的过程中判断最短路经过的点数是否大于 n 即可判断是否有负权环。
  • 注意: 由于图不一定连通,若要判从某个点出发是否可能经过负环只需将该点入队即可;若要判整张图中是否有负环只需将所有点入队即可。
int spfa(int s) {
	for(int i=1; i<=n; i++)dis[i]=1e9;
	for(int i=1; i<=n; i++)vis[i]=0;
	for(int i=1;i<=n;i++)cnt[i]=0;
	dis[s]=0;
	cnt[s]=1;
	queue<int>q;
	q.push(s);
	while(!q.empty()) {
		int u=q.front();
		vis[u]=0;
		for(int i=vex[u]; i; i=e[i].next) {
			int v=e[i].v;
			if(dis[v]>dis[u]+e[i].c) {
				dis[v]=dis[u]+e[i].c;
				cnt[v]=cnt[u]+1;
				if(cnt[v]>n)return 1;
				if(!vis[v]) {
					vis[v]=1;
					q.push(v);
				}
			}
		}
		q.pop();
	}
	return 0;
}

五.其他例题

构造非负权图,使用 Dij

例题链接

  • 题目描述: n个点; a a a 条无向边,边权非负; b b b 条有向边,边权可能为负,但是保证:若该条边是 u u u v v v 的,则 v v v u u u 没有通路。球起点 s s s 到所有点的距离。 n < = 2 e 4 , a , b < = 5 e 4 n<=2e4,a,b<=5e4 n<=2e4,a,b<=5e4
  • 问题分析: 卡普通 s p f a spfa spfa ,但是优化的 s p f a spfa spfa 仍然能过去,但是这里不用这个方法。显然,这 a + b a+b a+b 条路径可以变成一张缩完点后:点内无负权边;点间有负权边的图。可以在点内 D i j Dij Dij,点外拓扑序即可。
  • 如何构建拓扑序:先建完无向边,缩点;再连有向边添加度。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
#define inf 1000000000

struct E {
	int u,v,w,next;
} e[N*2];
int vex[N],color[N],co,k,in[N],vis[N];
long long dis[N];
vector<int>node[N];
vector<pair<int,int> >G[N];
void add(int u,int v,int w) {
	k++;
	e[k].u=u;
	e[k].v=v;
	e[k].w=w;
	e[k].next=vex[u];
	vex[u]=k;
}
void dfs(int u) {
	color[u]=co;
	node[co].push_back(u);
	for(int i=vex[u]; i; i=e[i].next) {
		int v=e[i].v;
		if(color[v]==0)dfs(v);
	}
}
int main() {
	int n,a,b,s,u,w,v;
	cin>>n>>a>>b>>s;
	for(int i=1; i<=a; i++) {
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	for(int i=1; i<=n; i++) {
		if(color[i]==0) {
			co++;
			dfs(i);
		}
		dis[i]=inf;
	}
	dis[s]=0;
	for(int i=1; i<=b; i++) {
		cin>>u>>v>>w;
		G[u].push_back(make_pair(v,w));
		in[color[v]]++;
	}
	queue<int>q;
	for(int i=1; i<=co; i++) {
		if(in[i]==0)q.push(i);
	}
	while(!q.empty()) {
		priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > dij;
		int u=q.front();
		q.pop();
		for(int i=0; i<node[u].size(); i++) {
			int v=node[u][i];
			if(dis[v]<inf)dij.push(make_pair(dis[v],v));
		}
		while(!dij.empty()) {
			int u=dij.top().second;
			dij.pop();
			if(vis[u])continue;
			vis[u]=1;
			for(int i=vex[u]; i; i=e[i].next) {
				int v=e[i].v;
				if(dis[v]>dis[u]+e[i].w){
					dis[v]=dis[u]+e[i].w;
					dij.push(make_pair(dis[v],v));
				}
			}
			for(int i=0;i<G[u].size();i++){
				int v=G[u][i].first,w=G[u][i].second;
				if(dis[v]>dis[u]+w)dis[v]=dis[u]+w; 
			} 
		}
		for(int i=0;i<node[u].size();i++){
			int v=node[u][i];
			for(int j=0;j<G[v].size();j++){
				if(--in[color[G[v][j].first]]==0)q.push(color[G[v][j].first]);
			}
		}
	}
    for(int i=1;i<=n;i++){
    	if(dis[i]==inf)cout<<"NO PATH "<<endl;
    	else cout<<dis[i]<<endl;
	}
	return 0;
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值