Loading

【题解】Luogu P5905【模板】全源最短路(Johnson)

思路分析

1. 判断负环

这个部分相对来说比较简单,可以用经典的 SPFA 算法判断负环,时间复杂度 \(O(nm)\)。这里给出一种效率更高的负环判断方法,使用 DFS 判断负环。遍历这个图,进行松弛技术,如果一个节点被访问第二次,那么就说明存在负环,直接退出。时间复杂度 \(O(n)\)

void fuhuan(int s){
	vis[s]=1;
	for(int i=h[s];i!=-1;i=e[i].nxt){
		if(d[e[i].to]>d[s]+e[i].w){
			if(vis[e[i].to]||flag){
				flag=1;break;
			}
			d[e[i].to]=d[s]+e[i].w;
			fuhuan(e[i].to);
		}
	}
	vis[s]=0;
}

如果找到了负环直接输出 \(-1\),跳出。

2. 新建节点,找最短路

这一部分是在为后面的算法核心做预处理。新建一个 \(0\) 节点连到所有节点上,然后从 \(0\) 节点跑一遍 SPFA 找到 \(0\) 距离所有节点的最短路。时间复杂度 \(O(nm)\)

void SPFA(int s){
	memset(d,0x3f,sizeof(d));
	memset(vis,0,sizeof(vis));
	d[s]=0;
	q.push(s);
	vis[s]=1;
	while(!q.empty()){
		int t=q.front();
		q.pop();
		vis[t]=0;
		for(int i=h[t];i!=-1;i=e[i].nxt){
			int v=e[i].to;
			if(d[v]>d[t]+e[i].w){
				d[v]=d[t]+e[i].w;
				if(vis[v]==0){
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
}
for(int i=1;i<=n;i++){
	Add(0,i,0);
}
SPFA(0);

3. 重置边权

这一部分是 Johnson 算法的核心,目的是使用更高效的 Dijkstra 算法求带有负边权的最短路。怎么实现呢?我们从 \(0\) 节点跑过一次最短路。设 \(u\) 节点到 \(0\) 节点的最短路是 \(h_u\),与 \(u\) 相连的节点是 \(v\)。因为 \(h_u\)\(h_v\) 都是松弛技术得来的最短路,那么对于原图中的边权 \(w(u,v)\),则有 \(h_u+w(u,v) \ge h_v\)。那么 \(h_u - h_v\) 就大于等于 \(-w(u,v)\),也就是原边权的相反数。于是把每条边的边权全部加上 \(h_u-h_v\),就可以保证边权全部非负,可以使用 Dijkstra 算法求解最短路。

为什么这么处理是正确的呢?我们不妨把这样求解的一条最短路展开,其中 \(s\) 为起点,\(e\) 为终点,\(p_i\) 是中间经过的点,共经过了 \(k\) 个点,可得:

\[(w(s,p_1)+h_s-h_{p_1})+(w(p_1,p_2)+h_{p_1}-h_{p_2})+...+(w(p_k,e)+h_{p_k}-h_e) \]

化简整理,得:

\[w(s,p_1)+w(p_1,p_2)+...+w(p_k,e)+h_s-h_e \]

可以发现,所有中间的值都约掉了,只剩下 \(h_s-h_e\) 这一个值。那么它就相当于一个偏移量,不影响最短路求解。

此部分时间复杂度为 \(O(m)\)

for(int i=1;i<=m;i++){
	e[i].w=e[i].w+d[e[i].from]-d[e[i].to]; 
}

4. 求解最短路

使用 Dijkstra 的堆优化对每个节点分别求解单元最短路,套板子即可。时间复杂度 \(O(nm \log m)\),优于 SPFA 的 \(O(n^2m)\),而且 SPFA 经常会被卡退化成 Bellman-Ford,变成 \(O(n^3m)\)

void dijkstra(int s){
	memset(vis,0,sizeof(vis));
	d2[s][s]=0;
	q2.push((Heapnode){0,s});
	while(!q2.empty()){
		Heapnode front=q2.top();
		q2.pop();
		int w=front.w,u=front.u;
		if(vis[u]) continue;
		vis[u]=1;
		for(int i=h[u];i!=-1;i=e[i].nxt){
			int v=e[i].to;
			if(d2[s][v]>d2[s][u]+e[i].w){
				d2[s][v]=d2[s][u]+e[i].w;
				if(!vis[v]){
					q2.push((Heapnode){d2[s][v],v});
				}
			}
		}
	}
}

5. 统计答案,输出

按题意来即可。记得减去先前的偏移量。

for(int i=1;i<=n;i++){
	ans=0;
	for(int j=1;j<=n;j++){
		if(d2[i][j]==0x3f3f3f3f3f3f3f3f){
			ans+=1000000000*j;
		}else{
			ans+=(d2[i][j]-d[i]+d[j])*j;
		}
	}
	printf("%lld\n",ans);
}

完整代码

#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define int long long
using namespace std;
const int maxn=3e3+10;
const int maxm=6e3+10;
struct Node{
	int from,to,nxt,w;
}e[maxm+maxn];
int n,m;
int h[maxn],tot,ans;
int d[maxn],d2[maxn][maxn],iqcnt[maxn];
bool vis[maxn],flag;
queue<int>q;
struct Heapnode{
	int w,u;
	bool operator <(const Heapnode &x) const{
		return w>x.w;
	}
};
priority_queue<Heapnode>q2;
void Add(int u,int v,int w){
	tot++;
	e[tot].from=u;
	e[tot].to=v;
	e[tot].nxt=h[u];
	e[tot].w=w;
	h[u]=tot;
}
void fuhuan(int s){
	vis[s]=1;
	for(int i=h[s];i!=-1;i=e[i].nxt){
		if(d[e[i].to]>d[s]+e[i].w){
			if(vis[e[i].to]||flag){
				flag=1;break;
			}
			d[e[i].to]=d[s]+e[i].w;
			fuhuan(e[i].to);
		}
	}
	vis[s]=0;
}
void SPFA(int s){
	memset(d,0x3f,sizeof(d));
	memset(vis,0,sizeof(vis));
	d[s]=0;
	q.push(s);
	vis[s]=1;
	while(!q.empty()){
		int t=q.front();
		q.pop();
		vis[t]=0;
		for(int i=h[t];i!=-1;i=e[i].nxt){
			int v=e[i].to;
			if(d[v]>d[t]+e[i].w){
				d[v]=d[t]+e[i].w;
				if(vis[v]==0){
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
}
void dijkstra(int s){
	memset(vis,0,sizeof(vis));
	d2[s][s]=0;
	q2.push((Heapnode){0,s});
	while(!q2.empty()){
		Heapnode front=q2.top();
		q2.pop();
		int w=front.w,u=front.u;
		if(vis[u]) continue;
		vis[u]=1;
		for(int i=h[u];i!=-1;i=e[i].nxt){
			int v=e[i].to;
			if(d2[s][v]>d2[s][u]+e[i].w){
				d2[s][v]=d2[s][u]+e[i].w;
				if(!vis[v]){
					q2.push((Heapnode){d2[s][v],v});
				}
			}
		}
	}
}
signed main(){
	memset(h,-1,sizeof(h));
	memset(d,0x3f,sizeof(d));
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%lld%lld%lld",&u,&v,&w);
		Add(u,v,w);
	}
	for(int i=1;i<=n;i++){
		if(!vis[i]) fuhuan(i);
	}
	if(flag){
		printf("-1");
		return 0;
	} 
	for(int i=1;i<=n;i++){
		Add(0,i,0);
	}
	SPFA(0);
	for(int i=1;i<=m;i++){
		e[i].w=e[i].w+d[e[i].from]-d[e[i].to]; 
	}
	memset(d2,0x3f,sizeof(d2));
	for(int i=1;i<=n;i++){
		dijkstra(i);
	}
	for(int i=1;i<=n;i++){
		ans=0;
		for(int j=1;j<=n;j++){
			if(d2[i][j]==0x3f3f3f3f3f3f3f3f){
				ans+=1000000000*j;
			}else{
				ans+=(d2[i][j]-d[i]+d[j])*j;
			}
		}
		printf("%lld\n",ans);
	}
	return 0;
} 

一些坑点

  • 建完 \(0\) 点的所有边后边的总量是 \(n+m\) 条,空间别开小了(本人曾喜提 \(76\) pts RE);
  • long longmemset 初始化,最后判断的时候记得输对(比如 0x3f 就不是 int 类型的 0x3f3f3f,而是 0x3f3f3f3f3f3f3f3f)。

总结

这个题考察了几个求最短路的模板,以及修改负权的方法。虽然实际情况使用不多,但是仍然给我们提供了一条宝贵的思路。

总时间复杂度 \(O(nm \log m)\)

posted @ 2025-12-12 22:59  Seqfrel  阅读(0)  评论(0)    收藏  举报