“多次入堆的” Dijkstra在有负边没负环的时候能用吗?

感谢评论,让我知道了我一直以来写的Dijkstra都是“盗版的”。。。。。。

由于在循环过程中我将if(vis[u]) continue;改成了if(cur.d!=dis[u]) continue; 导致我的点可以多次入堆,与经典的“只入堆一次”不同。不过这样改之后应该并没有多大的损害,毕竟这么多年我都没发现。但是既然知道了,以后还是写原版比较好。

如果只入堆一次的话,那毫无疑问原版Dijkstra不能处理有负边无负环的情况了。

因为如果一开始更新了dis[v],而后来由于负边的缘故dis[v]减少了,我们想要的情况是优先队列里记录的dis[v]也相应减少,并重新更新周围点。然而事实是v已经被标记过,无法再重新更新周围点。从这里就开始产生错误了。

下面的内容请读者把Dijkstra自行前面脑补出“多次入堆的”。
啊,现实总是出乎意料啊,还是说我读书不认真?


大部分人应该都应该都想过这个问题: Dijkstra 在有负边但没负环的情况下能用吗?

我看了很多回答,试了一些博客举的例子,但用代码检验的结果都是:Dijkstra 是对的。 最终,我也没有找到能说服我的博客。

我发现找博客是有极限的,所以,CSDN,我不找了!

当然,我最终也无法回答

但我做了个有意思的尝试:对拍。
先亮出结果:
图1 对拍结果:无差异
多次对拍并没有差异!

下面说出我具体的做法。

目录

1. 代码展示

2. 结果展示

3. 没用的总结


1. 代码展示

下面就放出我的代码啦!各位看官(如果有的话)尽可以检验。

Dijkstra 代码

经过 luogu 考验的!

/*

据说只能在无负边的时候使用 \
此时优于 SPFA \ 

*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
#define INF (1e9+7)
const int N=1e5+10, M=2e5+10;

struct Edge { int to, next, w; }e[M];

struct Node
{
	int v, d;
	bool operator<(const Node & a) const{ return d>a.d; }
};
priority_queue<Node> Q;

int n, m, s;
int d[N], head[N];

void add(int u, int v, int w)
{
	static int en=0;
	e[++en]=(Edge){v,head[u],w};
	head[u]=en;
	return;
}

void Dijkstra(int s)
{
	while(!Q.empty()) Q.pop();
	fill(d,d+n+1,INF);
	d[s]=0;
	Q.push((Node){s,0});
	while(!Q.empty())
	{
		Node cur=Q.top(); Q.pop();
		int v=cur.v;
		if(cur.d!=d[v]) continue;
		for(int h=head[v], vv; h; h=e[h].next)
		{
			vv=e[h].to;
			if(d[vv]>d[v]+e[h].w)
			{
				d[vv]=d[v]+e[h].w;
				Q.push((Node){vv,d[vv]});
			}
		}
	}
	return;
}

int main()
{
	freopen("data.txt","r",stdin);
	freopen("Dijkstra.txt","w",stdout);
	scanf("%d%d%d", &n, &m, &s);
	for(int i=0, u, v, w; i<m; ++i)
	{
		scanf("%d%d%d", &u, &v, &w);
		add(u,v,w);
	}
	Dijkstra(s);
	for(int i=1; i<=n; ++i) printf("%d ", d[i]);
	puts("");
	fclose(stdin); fclose(stdout);
	return 0;
} 

SPFA 的 BFS 版

经过 luogu 考验的!

/*

判负环 \
SPFA_BFS原始版 \ 
最复杂 O(nm) \
据说可以优化成 SLF、LLL \ 

*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N=2010, M=3010, INF=1e9+7;
//edge
struct Edge
{
	int to, next, w;
}e[M<<1];

int pn, h[N];
void add(int u, int v, int w)
{
	e[++pn]=(Edge){v,h[u],w};
	h[u]=pn;
	return;
}
//main
int n, m, s;
int d[N];

void init()
{
	memset(h,0,sizeof(h));
	pn=0;
}
//spfa
queue<int> Q;
bool vis[N];
int cnt[N];
bool spfa_bfs(int s)
{
	memset(vis,false,sizeof(vis));
	memset(cnt,0,sizeof(cnt));
	fill(d+1,d+n+1,INF); d[s]=0;
	while(!Q.empty()) Q.pop();
	Q.push(s); vis[s]=true;
	
	while(!Q.empty())
	{
		int u=Q.front(); Q.pop();
		vis[u]=false;
		for(int h1=h[u]; h1; h1=e[h1].next)
		{
			int v=e[h1].to;
			if(d[v]>d[u]+e[h1].w)
			{
				d[v]=d[u]+e[h1].w;
				if(!vis[v])
				{
					Q.push(v);
					vis[v]=true;
					cnt[u]++;
					if(cnt[u]>n) return false;
				}
			}
		}
	}
	return true;
}

int main()
{
	freopen("data.txt","r",stdin);
	freopen("SPFA_BFS.txt","w",stdout);
	init();
	scanf("%d%d%d", &n, &m, &s);
	for(int i=0, u, v, w; i<m; ++i)
	{
		scanf("%d%d%d", &u, &v, &w);
		add(u,v,w);
	}
	if(!spfa_bfs(s)) puts("A"); 
// 对拍需要,若存在能到达的负环,输出 "A" 
	else
	{
		for(int i=1; i<=n; ++i) printf("%d ", d[i]);
		puts("");
	}
	fclose(stdin); fclose(stdout);
	return 0;
}

随机数据生成

可能数据出得并不科学,但起码是有负边没负环的。

/*

该程序随机出的数据大约有 1/20 是负的。
有最多 n=200 个点,最少 2*n 、最多 2400 条边。

*/
#include<cstdio>
#include<cstdlib>
#include<ctime>
using namespace std;
#define M1 200
#define M2 2000
#define M3 2000
#define RAND(x) (rand()%(x)+1)

int main()
{
	freopen("data.txt","w",stdout);
	srand((unsigned)time(NULL));
	int n, m, s;
	n=RAND(M1); m=n*2+RAND(M2); s=RAND(n);
	printf("%d %d %d\n", n, m, s);
	for(int i=0, u, v, w; i<m; ++i)
	{
		u=RAND(n);
		v=RAND(n);
		w=RAND(M3);
		if(w%20==0) w=-w/10;
		printf("%d %d %d\n", u, v, w);
	}
	fclose(stdout);
	return 0;
}

对拍程序

可以直接当模板,真不错。

#include<cstdio>
#include<cstdlib>
#include<ctime>
using namespace std;
#define getTime(a,b) (double)((b)-(a))/CLOCKS_PER_SEC

int main()
{
	int cnt=0;
	while(1)
	{
		system("data.exe");
		
		clock_t startTime1=clock();
		system("SPFA_BFS.exe");
		clock_t endTime1=clock();
		printf("oh,man~\n");
		
		FILE *f1=fopen("SPFA_BFS.txt","r");
		char c=fgetc(f1);
		fclose(f1);
		if(c=='A') continue;
	// 先用SPFA判断是否有负环,若有,重新随机数据
	
		clock_t startTime2=clock();
		system("Dijkstra.exe");
		clock_t endTime2=clock();
		
		printf("run time : %lf  %lf\n", getTime(startTime1,endTime1), \
										getTime(startTime2,endTime2));
		
		if(system("fc SPFA_BFS.txt Dijkstra.txt"))
		{
			printf("failed.");
			system("data.txt");
			return 0;
		}

// 小小地记录一下次数
		cnt++;
		if(cnt%20==0)
		{
			printf("------------------------------第 %d 次了--------\n\n", cnt); 
		}
	}
}

代码放完了。
感觉身体被掏空

3. 结果展示

首先看看数据出得怎么样:
图2 不错,有负边
随便找了其中一个数据,可以看出是有负边的,比较合理。

再看看到各点的最短路如何:
图3 各距离也正常
不错,起点到各终点的最短路也是正常的,只有少数是INF,可以接受。

最后的最后:
图4 第2020次结果正确
2020年新年快乐!!!

总结

在我的这次尝试中,Dijkstra 并没有败下阵来!虽然被无数人说道,但并没有屈服!

但又有什么用呢?

无负边时,还是用 Dijkstra,那年那题 1 100 变 60 的悲剧还在一些人脑海里回荡;有负边时,人们还是优先用 SPFA,毕竟久经考验。

一切好像并没有什么改变。


  1. 2018 NOI D1T1 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值