图论初步——最短路(2)

Dijkstra算法优化

关于Dijkstra

详见图论初步——最短路(1)
前面的文章中,我们提到了 D i j k s t r a Dijkstra Dijkstra算法的核心就是每次找到剩余的点中的最小值。
这一步用了一个 f o r for for循环,很花时间。
所以如何快速找到剩下的点的最小值,就是我们要解决的问题。

方法1:线段树

每次找到最小值及其编号,然后赋为极大值,以不影响后面取最小值。
时间复杂度: O ( l o g n ) O(logn) O(logn)单次, O ( n l o g n ) O(nlogn) O(nlogn)建树,总复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
代码复杂度:难

方法2:分块

操作方法同线段树
时间复杂度:单次 O ( n ) O(\sqrt n) O(n ),总复杂度 O ( n n ) O(n\sqrt n) O(nn )
代码复杂度:中

方法3:堆

建立小根堆,找到最小值后弹出堆,更新距离时在堆里修改。
时间复杂度:单次操作 O ( l o g n ) O(logn) O(logn),总复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
代码复杂度:中

方法3.5:优先队列

从方法3得到提示,我们可以用 S T L STL STL里面的priority_queue,即优先队列
时间复杂度:同堆
代码复杂度:简单

优先队列实现方式

由于要同时记录距离及编号,所以我们考虑使用 S T L STL STL中的make_pair函数。在优先队列中自动按第一关键字排序。

细节

由于c++优先队列默认为大根堆,而我们需要小根堆,故有2种操作方式:
第一种(不要轻易尝试):

#define pair<int,int> pii
priority_queue<pii,vector<pii>,greater<pii> >;

第二种:插入时插入负数,因为我们只需要点的具体编号,距离只做比较用。

代码
#include<bits/stdc++.h>
using namespace std;
int first[200005],Next[200005],to[200005],w[200005],tot=0,d[200005];
bool vis[200005];
int Read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')  f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+ch-'0';
		ch=getchar();
	}
	return x*f;
}
void Print(int x){
	if(x<0){
		putchar('-');
		x=-x;
	}
	if(x>9)  Print(x/10);
	putchar(x%10+'0');
}
void add(int x,int y,int z){
	Next[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
	w[tot]=z;
}
priority_queue< pair<int,int> > q;
void dijkstra(int x){
	memset(d,0x3f3f3f3f,sizeof(d));
	memset(vis,false,sizeof(vis));
	d[x]=0;
	pair<int,int> p=make_pair(0,x);
	q.push(p);
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		if(vis[u])  continue;
		vis[u]=true;
		int e,v;
		for(e=first[u];e,v=to[e];e=Next[e]){
			if(d[v]>d[u]+w[e]){
				d[v]=d[u]+w[e];
				q.push(make_pair(-d[v],v));
			}
		}
	}
}
int main(){
	int n,m,s;
	cin>>n>>m>>s;
	for(int i=1;i<=m;i++){
		int x,y,z;
		x=Read();
		y=Read();
		z=Read();
		add(x,y,z);
	}
	dijkstra(s);
	for(int i=1;i<=n;i++){
		Print(d[i]);
		putchar(' ');
	}
}

spfa算法

缺点:容易被卡,但随机数据时很快
优点:可以处理负权情况
D i j k s t r a Dijkstra Dijkstra一个点标记后无法被再次更新相比, s p f a spfa spfa允许一个点多次被更新,这使它可以处理有负权的情况,与 D i j k s t r a Dijkstra Dijkstra优先队列不同, s p f a spfa spfa使用普通队列。

实现

先将起点放入队列,然后只要队列不空,就取出队首,用它来更新其余点,如果成功更新,就判断该点是否在队列里,如果不是,则让其入队,这样一直下去,直到队列为空。

拓展

当存在负环时,就不存在最短路了,这时spfa就会展现出其优点。在存在最短路的情况下,一个点最多被其余的点更新一次,也就是更新 n − 1 n-1 n1次。所以记录该点入队的次数,如果达到 n n n次,就存在负环,跳出 s p f a spfa spfa过程。

代码

spfa:

void spfa()
{
	queue<int> q;
	for(int i=1;i<=n;i++)
	{
		d[i]=2147483647;
		inq[i]=false;
	}
	q.push(s);
	inq[s]=true;
	d[s]=0;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		inq[u]=false;
		for(int i=head[u];i;i=next[i])
		{
			int v=to[i];
			if(d[v]>d[u]+dis[i])
			{
				d[v]=d[u]+dis[i];
				if(!inq[v])
				{
					q.push(v);
					inq[v]=true;
				}
			}
		}
	}
}

负环:

bool spfa(int s)
{
	queue<int> q;
	for(int i=1;i<=n;i++)
	{
		d[i]=2147483647;
		inq[i]=false;
		cnt[i]=0;
	}
	q.push(s);
	inq[s]=true;
	d[s]=0;
	cnt[s]++;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		inq[u]=false;
		for(int i=head[u];i;i=next[i])
		{
			int v=to[i];
			if(d[v]>d[u]+dis[i])
			{
				d[v]=d[u]+dis[i];
				if(!inq[v])
				{
					q.push(v);
					inq[v]=true;
					cnt[v]++;
					if(cnt[v]>n)  return false;
				}
			}
		}
	}
	return true;
}

顺便一提,想为一个数组赋特定值时可以使用fill函数,例如将 a [ 1 ] − a [ n ] a[1]-a[n] a[1]a[n]赋为100000,可以

fill(a+1,a+n+1,100000);

练习题
dijkstra 洛谷P4779
spfa 洛谷P3371
负环 洛谷P3385

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值