几天来关于负环问题的一点研究

AzzyZhe

·

2021-08-08 17:57:34

·

题解

题解 P3385 负环

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

### 题解:判断负环

----

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

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

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

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

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

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

```cpp

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;

}

```

```cpp

//读入&初始化

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

{

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);

}

```

2. SPFA 算法求负环,时间复杂度 $\mathcal O (kn)$:

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

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

代码:

```cpp

int cnt[MAXN],dis[MAXN];

queueq;

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;

}

```

3. DFS-SPFA 求负环,时间复杂度 $\mathcal O (n^2)$:

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

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

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

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

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

```cpp

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;

}

```

4. Bellman-Ford 算法求负环,时间复杂度 $\mathcal O (mn)$:

即 SPFA 算法的本质。

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

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

代码:

```cpp

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

{

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$ 形图而不通用?类似下图:(有其他方法欢迎评论指出)

![](https://cdn.luogu.com.cn/upload/image_hosting/k81i8v3u.png)

### 扩展:求出负环

----

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

1. Floyd 算法:

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

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

2. SPFA(Bellman-Ford)算法:

这里不再分开介绍。

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

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

![](https://cdn.luogu.com.cn/upload/image_hosting/rqmavufl.png)

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

此外还在网上看到另一种建立“最短路径树”判环的方法,想了解的可以参考[链接](https://www.zhihu.com/question/264951302)。

### 其他

----

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

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