Algorithm: 负环暴论 - 0

题解 P3385 负环

转载: https://www.luogu.me/article/0betmrit
\(\rightarrow\)题目传送门

我来写篇总结附带求出具体负环的吧。

题解:判断负环


既然判断负环的几种方法其他题解都已经提到过,那我就姑且只大概做个总结吧。

  1. Floyd 算法求负环,时间复杂度 \(\mathcal O (n^3)\)

可以考虑建立原图的反向图记录 \(1\) 向每个点的最短路径的更新来源再从点 \(1\) 开始 dfs 看是否能进入环中。

此外因为 Floyd 的结果中负环上的点到自身距离小于 \(0\),所以可以同时传下闭包(Floyd 一遍以后再从 \(1\) 开始 dfs 一遍判断可达点到自身的距离是否有小于 \(0\) 的也可)。

本题中由于时间复杂度太高而不能通过。

代码(自身距离+闭包):

int dis[MAXN][MAXN];
bool vis[MAXN][MAXN];
bool Floyd(int s)
{
	vis[s][s]=1;
	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
			{
				vis[i][j]|=vis[i][k]&&vis[k][j];
				if(dis[i][j]>dis[i][k]+dis[k][j])
					dis[i][j]=dis[i][k]+dis[k][j];
			}
	for(int i=1;i<=n;i++)
		if(vis[s][i]&&dis[i][i]<0)
			return 1;//存在负环
	return 0;
}
//读入&初始化
memset(dis,0x3f,sizeof(dis));//不能0x7f
memset(vis,0,sizeof(vis));
cin>>n>>m;
for(int i=1;i<=n;i++)
	dis[i][i]=0,
	vis[i][i]=0;
for(int i=0,u,v,w;i<m;i++)
{
	cin>>u>>v>>w;
	dis[u][v]=min(dis[u][v],w);
	vis[u][v]=1;
	if(w>=0)
		vis[v][u]=1,
		dis[v][u]=min(dis[v][u],w);
}
  1. SPFA 算法求负环,时间复杂度 \(\mathcal O (kn)\)

没有负环的图中一个最多被其他 \(n-1\) 个点松弛 \(n-1\) 次,如果一个点被从 \(1\) 出发的 SPFA 松弛了 \(n\) 次及以上,则存在由 \(1\) 可达的负环。

需要注意的一点是(如果有人之前学的 SPFA 像我学的一样),不同于求最短路时,这里如果在松弛完点 \(u\) 的可松弛边后才标记 \(u\) 出队,就会忽略掉由自环形成的负环。

代码:

int cnt[MAXN],dis[MAXN];
queue<int>q;
bool vis[MAXN];
int spfa(int s)
{
	memset(vis,0,sizeof(vis));
	memset(dis,0x7f,sizeof(dis));
	memset(cnt,0,sizeof(cnt));
	while(!q.empty())
		q.pop();
	vis[s]=1;
	dis[s]=0;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();
		vis[u]=0;
		q.pop();
		for(int i=head[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;
				if(!vis[v])
				{
					if(++cnt[v]>n)
						return 1;
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
	return 0;
}
  1. DFS-SPFA 求负环,时间复杂度 \(\mathcal O (n^2)\)

据说只是判断负环的话很快,但要求解的话很慢。

可以搜索实现,松弛到还未退出的点即存在负环。

也可以像这样把普通 SPFA 里的队列改为栈,模拟深搜,但要注意这时 vis[] 表示的是否正在搜索经过而非是否在栈内。

作为“SPFA”一如既往地容易被卡(此题中被 #9 卡)。

代码(其实这个模拟但没完全模拟,反而显得麻烦了):

int dis[MAXN];
int stk[MAXN],sp;
int pre[MAXN],cnt[MAXN];//从哪个点搜过来的 / 从一个点出发的搜索分支数
bool vis[MAXN],instk[MAXN];
int dfs_spfa(int s)
{
	memset(vis,0,sizeof(vis));
	memset(dis,0x7f,sizeof(dis));
	memset(cnt,0,sizeof(cnt));
	memset(instk,0,sizeof(instk));
	sp=0;
	stk[++sp]=s;
	dis[s]=0;
	instk[s]=1;
	while(sp)
	{
		int u=stk[sp--];instk[u]=0;
		vis[u]=1;
		for(int i=head[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;
				if(vis[v])
					return 1;
				if(!instk[v])
					stk[++sp]=v,
					cnt[u]++,
					pre[v]=u,
					instk[v]=1;
			}
		}
		while(u&&cnt[u]==0)
		{
			vis[u]=0;
			u=pre[u];
			cnt[u]--;
		}
	}
	return 0;
}
  1. Bellman-Ford 算法求负环,时间复杂度 \(\mathcal O (mn)\)

即 SPFA 算法的本质。

Bellman-Ford 算法在没有负环时,最坏情况(一条链)下也只需 \(n-1\) 次即可完成所有松弛操作找到最短路,如果第 \(n\) 次仍有可松弛的边,则图中有负环。

但是它并不能直接判断是否由 \(1\) 可达,可以考虑第 \(n\) 次循环来一次万能的 dfs 或者在松弛时顺带传下闭包。

代码:

int dis[MAXN],vis[MAXN];
int bellman_ford(int s)
{
	memset(dis,0x7f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[s]=0;
	vis[s]=1;
	for(int i=1;i<n;i++)
	{
		bool flag=1;
		for(int j=1;j<=tot;j++)//tot:边数
			if(dis[e[j].v]>dis[e[j].u]+e[j].w)
			{
				dis[e[j].v]=dis[e[j].u]+e[j].w;
				vis[e[j].v]|=vis[e[j].u];
				flag=0;
			}
		if(flag)
			return 0;
	}
	for(int j=1;j<=tot;j++)
		if(dis[e[j].v]>dis[e[j].u]+e[j].w)
		{
			dis[e[j].v]=dis[e[j].u]+e[j].w;
			if(vis[e[j].u])
				return 1;
		}
	return 0;
}

此外,本题中还有一点值得注意的就是,本题是一道个测试点有多组数据点的图论题,因此要重置链式向前星的 head[] 数组和加边计数(之前自己居然犯了这种错误……)。

说起来缩点暴力把边权变点权求负环似乎可能因正环加负环构成的 \(8\) 形图而不通用?类似下图:(有其他方法欢迎评论指出)

扩展:求出负环


虽然好像使用价值不是很大(总不能用负环缩点吧)。

  1. Floyd 算法:

上面求负环的前一种方案显然可以直接求出。

或者由上面提到的结论,哪些点在负环上一目了然。如果要分出每个点所在的负环,考虑 dfs。

  1. SPFA(Bellman-Ford)算法:

这里不再分开介绍。

在松弛时记录最短路径来源(SPFA 的话也可 DFS-SPFA 实现),回溯到环上 dfs。

值得注意的是 SPFA 中 cnt[v]>n 的点 \(v\) 和 Bellman-Ford 算法中第 \(n\) 次可松弛的边 \(i\) 不一定在负环上(如下图),因此需向前回溯 \(n\) 次。

不过上面提到的几种方法,多半可能在两个负环构成的 \(8\) 形图中漏解,这个问题个人暂时无解 emmmm。

此外还在网上看到另一种建立“最短路径树”判环的方法,想了解的可以参考链接

其他


关于上文提到的“Floyd 算法结果中负环上的点到自身距离小于 \(0\)”这个结论,个人的理解是:Floyd 最外层经过 \(k\) 次插点循环的结果是,最多经过 \(1 \cdots k\) 的点下的最短路径,如果存在负环,即便以及不满足动态规划的最优化原理,但自己到自己的距离会被一条经过负环至少一圈(一部分)的路径取代,因此负环上的点到自身距离小于 \(0\)

如有不对或有其他更严谨/更好理解的想法欢迎指出,其他地方也是。

posted @ 2025-08-22 10:57  liumingedwin  阅读(11)  评论(0)    收藏  举报