最短路相关算法

最短路问题难点在于建图,如何把原问题抽象成最短路问题,如何定义点和边

在这里插入图片描述
m和n^2差不多时就是稠密图

1.朴素版Dijkstra算法 O(n*n)
思路

s i s_i si当前已确定最短路距离的点

1.初始化 dis[1]=0 , dis[i]=INF  只有起点是确定的

2. for i : 1~n //每次循环都可以确定一个点到源点的最短距离
       t <- 不在s[i]中的,距离最近的点
       s <- t
       用t来更新其他所有点的距离(dis[x]>dis[t]+w)
例题1

给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
在这里插入图片描述
由数据范围,500个点1e5条边知这是一个稠密图,用邻接矩阵来存储

输入
3 3
1 2 2
2 3 1
1 3 4
输出
3
代码
#include <iostream>
#include <cstring>
#include <queue>
const int maxn=1e3+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m;
int mp[maxn][maxn],dis[maxn];
bool st[maxn];//某个点是否已经更新过其他点,而不是它的最短距离是否已经确定
int dijkstra()
{
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;

	for(int i=1;i<=n;i++)//还剩下n-1个点未处理,所以循环n-1次以后就可以得到所有点的最短路了
	{
		int t=-1;//Dij的使用情况是均为正权边
		for(int j=1;j<=n;j++)//编号从1开始遍历
			if(!st[j]&&(t==-1||dis[t]>dis[j]))//刚开始,或当前不是最短路
				t=j;//寻找还未确定最短路的点中 路径最短的点
		if(t==n)//如果提前找到了可以直接break
			break;
		st[t]=true;//找到了剩余未确定的最短路中路径最短的点
		for(int j=1;j<=n;j++)//用这个点去更新其他点
			dis[j]=min(dis[j],dis[t]+mp[t][j]);
		
	}
	if(dis[n]==INF)//注意如果不存在最短路情况的特判
		return -1;
	return dis[n];
}
int main()
{
	cin>>n>>m;
	int a,b,c;
	memset(mp,0x3f,sizeof(mp));
	for(int i=0;i<m;i++)
	{
		cin>>a>>b>>c;
		mp[a][b]=min(mp[a][b],c);//处理重边的情况
	}
	cout<<dijkstra()<<endl;
	return 0;
}
扩展

如果是问编号a到b的最短距离,初始化的时候改为 dis[a] = 0 ,并且return dis[b]

2.堆优化版Dijkstra算法 O(mlogn)

dij用binary-heap是 O( (N+M)logN )的,即使是fibo堆也是 O( M + NlogN )的,稠密图的 M 能到 N2

在这里插入图片描述
遍历所有点的所有边,就是遍历这个图的所有边

堆的实现方式

1.手写堆      优点:支持直接修改某个数 其中存储的最多就是n个数
2.优先队列   缺点:不支持修改任意一个元素,只能通过不断插入新的数来实现,所以堆里总共的个数可能会有m个,时间复杂度就会变成O(mlogm)

但是,一般来说 m ≤ n n , l o g m ≤ l o g n n = 2 l o g n m≤n^n,logm≤logn^n=2logn mnnlogmlognn=2logn,所以log m和log n是一个级别的,所以一般可以不用手写堆。

但是使用优先队列,有可能会存在冗余数据,因为修改是通过不断加数进去的。

例题 (上题变形)

在这里插入图片描述
采用邻接表来存图

代码
#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
const int maxn=1e6+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m,idx;
int h[maxn],w[maxn],e[maxn],nex[maxn],dis[maxn];
bool st[maxn];//某个点是否已经更新过其他点,而不是它的最短距离是否已经确定
typedef pair<int,int> pp;
void add(int a,int b,int c)
{
	e[idx]=b;
	w[idx]=c;
	nex[idx]=h[a];
	h[a]=idx++;
}
int dijkstra()
{
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;
	priority_queue<pp,vector<pp>,greater<pp> > heap;//定义一个小根堆
	//注意默认是按first来进行排序的,所以把dis放在前面
	heap.push({0,1});
	
	while(heap.size())
	{
		auto t=heap.top();
		heap.pop();

		int vi,vdis;
		vi=t.second;
		vdis=t.first;
		if(st[vi])//已经访问过了
		continue;

		st[vi]=true;
		for(int i=h[vi];i!=-1;i=nex[i])
		{
			int j=e[i];
			if(dis[j]>dis[vi]+w[i])
			{
				dis[j]=dis[vi]+w[i];
				heap.push({dis[j],j});
			}
		}
	}
	if(dis[n]==INF)
	return -1;
	return dis[n];
}
int main()
{
	cin>>n>>m;
	int a,b,c;
	memset(h,-1,sizeof(h));
	for(int i=0;i<m;i++)
	{
		cin>>a>>b>>c;
		add(a,b,c);
	}
	cout<<dijkstra()<<endl;
	return 0;
}
3.Bellman-Ford算法 O(nm)
基本思路

注意:备份是因为在遍历边进行更新时,要用之前的dis
在这里插入图片描述

for 1~n 迭代n次
    备份一下之前的dis//保证只用上一次迭代的结果
	for 所有边 a,b,w //这里不一定要用邻接表存,只要能遍历到所有边即可 可以用struct
	dis[b]=min(dis[b],dis[a]+w)  //1 -> b 和 1 -> a -> b 哪条路径最短
	//松弛操作

该算法完成之后,就一定有dis[b]≤dis[a]+w(三角不等式)

如果有负权回路,最短路就不一定存在了,当这个环在1->n的最短路径上时,就不存在,如果不在,则可以存在最短路。

判有负环,即出现一个dis[i]=-∞,则说明存在负环
在这里插入图片描述
B-F算法可以求出是否存在负权回路

for n次 迭代k次之后,dis表示的是,从1号点经过不超过k条边,到达所有点的最短距离

   当迭代第n次时还有更新,说明 1->...->x 有n条边构成的最短路,而n条边意味着有n+1个点,
但实际上由只有n个点,说明一定有重复的编号,就一定有环,另外这里更新了,所以就一定是负环。

但是一般找负环不用S-F算法来做,常用SPFA来做

例题

一般B-F能做的SPFA都能做,但是有边数限制的题目只能用B-F来做
在这里插入图片描述

输入
3 3 1
1 2 1
2 3 1
1 3 3
输出
3

本题只能用B-F算法,因为 1.存在负边权(不能用dijstra) 2.存在负权回路(不能用SPFA)
本题如果限制了k步,则不会出现无限次转负环的情况,所以会存在最短路。

#include <iostream>
#include <cstring>
#include <algorithm>
const int maxn=1e4+5;
const int INF=0x3f3f3f3f;
using namespace std;
struct node{
	int a,b,c;
}edges[maxn];
int n,m,k;
int dis[510],pre[510];
void bellman_ford()
{
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;//不要忘记初始化
	for(int i=0;i<k;i++)
	{
		memcpy(pre,dis,sizeof(dis));//效率非常高
		for(int j=0;j<m;j++)//遍历每条边
		{
			node e;
			e=edges[j];
			dis[e.b]=min(dis[e.b],pre[e.a]+e.c);//注意这里的pre
		}
	}
}
int main()
{
	cin>>n>>m>>k;
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		edges[i]={a,b,c};//注意这种使用方式
	}
	bellman_ford();
	if(dis[n]>INF/2)
        cout<<"impossible"<<endl;
	else
		cout<<dis[n]<<endl;
	return 0;
}

在这里插入图片描述

4.SPFA算法 O(m) 最坏:O(nm)
要求这个图中不含负环  一般最短路问题中都不会有负环出现

B-F算法是通过遍历所有边来更新dis,但是每一次迭代并不一定都会更新更小的dis,所以这里可优化。
一个点如果没有被更新,那么由它所更新的那些点都不会有变化,所以可略过

SPFA优化:对于dis[b]=min(dis[b],dis[a]+w)来说,dis[b]变小,一定是因为dis[a]变小,可以通过宽搜来优化

使用的工具:队列、优先队列等

queue <- 1
while(queue不为空)
	1. t <- q.front 
	   q.pop()        w
	2.更新t的所有出边 t-->b   //更新过谁,就拿谁来更新别人
	  queue <- b

我们也用SPFA来做正权图的问题,并且可能比堆优化的dij块,but...能用堆优化的dij就不用spfa,spfa很有可能被出题人构造的数据卡成O(nm),这个效率就很低了

网格形状的图很容易被卡spfa
是个稀疏图,边数是点数的2~3倍

dijkstra和spfa相比

spfa算法的期望的时间复杂度为O(ke) k<=2

  1. DIJ的堆优化可以做到O((V+E)LOG2(V))
    近似为 O(E LOG2(V))
    SPFA的时间复杂度为O(KE),好坏视数据RP而定,在大多数平均情况下为O(2E),为了近似最快可以随机化输入数据

  2. 根据用不用堆,dij的复杂度可以是O(mlogn)和O(n2),前者不一定总是比后者快,因为有时候m=O(n^2)

最基础图论总结(Spfa与Dijkstra)

  • Floyd是先枚举转接点,从而达到更新最小值的目的。到后期好像O(n^3) 像闹着玩一样,但在一些n<=100的环境下还是很好用的
  • Dijkstra在图论问题中主要的优点是较稳定,不会被特殊设计的测试点卡掉,还可以记录路径
  • Spfa是笔者比较青睐的算法。Spfa可以在压队的过程中判断是否存在环,还可以处理负环。

如何卡spfa?

例题1 模板

在这里插入图片描述

输入
3 3
1 2 5
2 3 -3
1 3 4
输出
2
代码
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
const int maxn=1e5+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m;
int h[maxn],w[maxn],e[maxn],nex[maxn],idx;
int dis[maxn];
bool st[maxn];//st数组是用来标记这个数是否存在在队列中,不影响算法正确性,只用于提高算法效率
void add(int a,int b,int c)
{
	e[idx]=b;
	w[idx]=c;
	nex[idx]=h[a];
	h[a]=idx++;
}
int spfa()
{
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;
	
	queue<int> q;
	q.push(1);//存入点的编号
	st[1]=true;//当前点是不是在队列当中,避免队列中存储重复的点

	while(!q.empty())
	{
		int t=q.front();
		q.pop();
		st[t]=false;

		for(int i=h[t];i!=-1;i=nex[i])
		{
			int j=e[i];
			if(dis[j]>dis[t]+w[i])
			{
				dis[j]=dis[t]+w[i];
				if(!st[j])//把更新后的这个点加入队列
				{
					q.push(j);
					st[j]=true;
				}
			}
		}
	}
	return dis[n];
}
int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof(h));//初始化链表头
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	int ans=spfa();
	if(ans==INF)
		cout<<"impossible"<<endl;
	else
		cout<<ans<<endl;
	return 0;
}

这里可以使用 ans==INF 作为判断条件是因为 spfa只会更新所有能从起点走到的点(dis[j]>dis[t]+w[i]),所以如果无解,那么起点就走不到终点,那么终点的距离就是INF

例题2 spfa判负环

dis[x]:1~x 的最短距离
cnt[x]:当前最短路的边数
更新操作:
dis[x]=dis[t]+w[i]
cnt[x]=cnt[t]+1

  一旦出现cnt[x]≥n,即1->x至少经过了n条边,如果经过了n条边,则说明1->x中至少经过了n+1个点,但是最多只有n个点,由抽屉原理知就一定有两个点是一样的。路径上存在一个环,这个环又一定是更新的时候能够使得最短路变小,所以一定是负边权的环。
在这里插入图片描述

输入
3 3
1 2 -1
2 3 4
3 1 -4
输出
Yes
代码
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
const int maxn=1e5+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m;
int h[maxn],w[maxn],e[maxn],nex[maxn],idx;
int dis[maxn],cnt[maxn];
bool st[maxn];//st数组是用来标记这个数是否存在在队列中,不影响算法正确性,只用于提高算法效率
void add(int a,int b,int c)
{
	e[idx]=b;
	w[idx]=c;
	nex[idx]=h[a];
	h[a]=idx++;
}
int spfa()
{
    //这里也可以不要初始化,因为你不关心最短距离是多少
	queue<int> q;
	//注意这里要判断的是负环,而不是从1开始的负环了
	//这个途中有可能存在负环,但是从1开始到不了
	//q.push(1);//存入点的编号
	//st[1]=true;//当前点是不是在队列当中,避免队列中存储重复的点
	//所以应该把所有点放到队列中
	for(int i=1;i<=n;i++)
	{
		q.push(i);
		st[i]=true;
	}
	while(!q.empty())
	{
		int t=q.front();
		q.pop();
		st[t]=false;

		for(int i=h[t];i!=-1;i=nex[i])
		{
			int j=e[i];
			if(dis[j]>dis[t]+w[i])
			{
				dis[j]=dis[t]+w[i];
				cnt[j]=cnt[t]+1;
				if(cnt[j]>=n)
				return true;

				if(!st[j])//把更新后的这个点加入队列
				{
					q.push(j);
					st[j]=true;
				}
			}
		}
	}
	return false;
}
int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof(h));//初始化链表头
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	if(spfa())
	cout<<"Yes"<<endl;
	else
	cout<<"No"<<endl;
	return 0;
}
5.Floyd算法 求多源汇最短路
基本思路  (基于动态规划)
可以处理负权,但不能有负权回路

用邻接矩阵存储所有的边
d[k,i,j]=d[k-1,i,k]+d[k-1,k,j] 第一维可以去掉
dp[k,i,j]表示只经过1~k这些中间点,到达j的最短距离

d[i][j]存储图的边权
for (k=1;k<=n;k++)
	for (i=1;i<=n;i++)
		for (j=1;j<=n;j++)
		d[i][j]=min(d[i][j],d[i][k]+d[k][j])

算法完成之后,d[i][j]中存储的就是i到j的最短路长度

如果了解算法原理,可以看算法导论or百度。

例题

在这里插入图片描述
题目中保证不存在负权回路,所以自环的权值一定是正的,在求最短路时没有用,可直接删除

输入
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出
impossible
1
代码
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
const int maxn=205;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m,q;
int d[maxn][maxn];
void floyd()
{
	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
			d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
int main()
{
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(i==j)
				d[i][j]=0;
			else
				d[i][j]=INF;
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		d[a][b]=min(d[a][b],c);
	}
	floyd();

	while(q--)
	{
		int a,b;
		cin>>a>>b;
		if(d[a][b]>INF/2)
		cout<<"impossible"<<endl;
		else
		cout<<d[a][b]<<endl;
	}
	return 0;
}

使用INF还是INF/2作为判断是否存在最短路,需要具体情况具体分析:

  本题中可能存在如下情况:不能走到终点,但由于负数边权的存在,终点的距离可能被其他长度是正无穷的距离更新,此时会稍小于INF。INF/2比较可以处理这种情况。

  • 假设d[i][j]=7,d[i][k]=5,d[k][j]=8,在第k层时会因为d[i}[j]小于后者,而不更新d[i][j]。但万一此时第k+1个点与点k还有点j之间有负权边(点k+1与点i不直接相连),或者因为各种原因d[i][k]+d[k][k+1]+d[k][j]小于d[i][j],floyd算法会不会因为没有选择走点k而错失i->k->k+1->j这条最短路的机会?

  floyd本质上是个动态规划,每次循环完第k层后,会将所有中间点的编号不超过k的最短路径全部找出来。d[i][j]如果在第k层没有被更新,这也是合理的,因为在从i到j的最短路径中需要经过k+1这个点,那么这条最短路径要在第k+1层循环完之后才可能被找到,即d[i][j]是会被d[i][k+1] + d[k+1][j]来更新,那么此时就会找到d[i][k] + d[k][k+1] + d[k+1][j]这条路径了。

  • 为什么计算状态的时候 for (int k = 1; k <= n; k++)要放到最外层呢?

  floyd本身是个动态规划算法,在代码实现的时候省去了一维状态。原状态是:f[i, j, k]表示从i走到j的路径上除了i, j以外不包含点k的所有路径的最短距离。那么f[i,j,k] = min(f[i,j,k-1),f[i,k,k-1]+f[k,j,k-1]。因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值