再再再谈找负环

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lycheng1215/article/details/78502495

传送门

Update

  世事无常,数据加强。DFS SPFA 直接被 ban 掉了,关键是能过的 5 个点中 WA 掉了一个点(原因:没有按照题意建图,还好代码是对的)-_-||。看来这个专题要重修了 QAQ。

原文

  在学习差分约束的时候曾经了解到,找负环的方法主要是使用 SPFA。BFS 的 SPFA 的判断方法为如果有一个点入队 n 次就说明有负环,时间复杂度为 O(ke)(k 趋近于 n 了)。所以我们需要更快的方法,那就是 DFS。DFS判断负环的方法很简单:如果走到一个点可以更新,而且那个点还在栈中,就说明找到了。这也是 DFS 找负环的优势所在:遍历到负环后它退出得更及时


  DFS 的 SPFA 怎么写?(假设所有变量已经全部正确初始化(如何正确初始化?等会儿再说))

bool inStack[maxn];
INT dis[maxn];
bool bFound;
void dfs(INT node)
{
    inStack[node] = true;
    for (int i = head[node]; i; i = edges[i].next)
    {
        INT to = edges[i].to;
        INT cost = edges[i].cost;
        if (dis[node] + cost < dis[to])
        {
            if (inStack[to])
            {
                bFound = true;
                return;
            }
            dis[to] = dis[node] + cost;
            dfs(to);
            if (bFound)
                return;
        }
    }
    inStack[node] = false;
}

  注意 bFound 在一开始是一定为 false 的,inStack 在一开始也是一定为 false 的。


  遍历哪些点?如何遍历?dis 初值多少?

  一个简单的想法是遍历每个点,计算以每个点作为起点的单源最短路,每次 dis 重置为 INF。代码大概长这样:

for(int i = 1; i <= n; i++)
{
    memset(dis, 0x3f, sizeof(dis));
    dis[i] = 0;
    dfs(i);
    if(bFound) break;
}

  天啦噜。。。等着超时吧。。。

  一个常见的思路是添加超级点,即添加一个 0 号点,向所有其它点连一条边权为 0 的有向边,然后从 0 开始 DFS,这样就只用 DFS 一次了。

  但是一定要注意:如果真的这么做的话,边集数组一定要多开 n!!!

for(int i = 1; i <= n; i++)
    addEdge(0, i, 0);

memset(dis, 0x3f, dis);
dis[0] = 0;
dfs(0);
if(bFound); //...

  然而这么做还是太慢。由于我们要找的是负环,因此 dis 在一开始可以不为 INF,直接设为 0 即可。如果真的有负圈,那么这些点一定会被更新的,否则就省去了不必更新的点,从而使速度大幅提升。

  大概会写出如下代码(有错):

for(int i = 1; i <= n; i++)
    addEdge(0, i, 0); //Wrong!

memset(dis, 0, dis);
dfs(0);
if(bFound); //...

  这么做的话答案始终为 No,因为从 0 出发走一条长度为 0 的点是无法更新 dis 为 0 的点的。

  一种解决方法是把超级点到其它点的边权改成 -1,这样做不影响负环的查找,但是就可以从 0 出发到其它点了:

for(int i = 1; i <= n; i++)
    addEdge(0, i, -1); //one available solution

memset(dis, 0, dis);
dfs(0);
if(bFound); //...

  另一种方法是特判 0,即如果起点是 0,那么无论如何都要遍历下一个点。有兴趣的可以去试试。


  真的有必要吗?

  加了一个超级点,看似把问题简单化了,实际上造成了潜在的漏洞(如忘记把数组开大),还造成了逻辑上的麻烦(如非要把边权改成负的)。考虑刚刚说的最后一个思路,我们为什么不手动模拟这个超级点呢?

memset(dis, 0, dis);
for(int i = 1; i <= n; i++)
{
    dfs(i);
    if(bFound) break;
}

  这个循环就相当于我们在超级点上遍历加的额外的边。仔细将这个代码与第一份代码进行对比,发现除了少了循环中的 memset 之外,其余的都没有变(第一份代码也可以把 dis 初始化为 0,所以这一点忽略不计)。所以,这个循环不需要清空 dis 数组的根本原因是 dis 代表的是超级点到其它所有点的最短路,而不是其中某个点到其它点的最短路。

时间复杂度

  如果有负环,DFS 的时间复杂度为 O(nm)否则将会退化至指数级

BFS 找负环

  还是回归本质用 BFS 算了。我们新建一个超级点,向其它所有点连一条边权为 0 的有向边,显然原图中的负环不受影响。我们以超级点作为源点,显然此时其它所有点都要入队,并且距离变成 0。然后我们继续操作,直到队列为空,或者存在一个点入队至少点数次,即 n+1 次。

bool inQ[maxn];
int counter[maxn];
bool SPFA()
{
    q.clear();
    for (int i = 1; i <= n; i++)
    {
        dis[i] = 0;
        q.push(i);
        inQ[i] = true;
        counter[i] = 0; // 实际上这里应该赋值为 1
    }
    while (!q.empty())
    {
        int from = q.front();
        q.pop();
        inQ[from] = false;
        wander(G, from)
        {
            DEF(G);
            if (dis[from] + cost < dis[to])
            {
                dis[to] = dis[from] + cost;
                if (!inQ[to])
                {
                    if (++counter[to] >= n) return true; // 实际上这里应该写 n + 1,但是前面已经减了 1 了
                    q.push(to);
                    inQ[to] = true;
                }
            }
        }
    }
    return false;
}

没有更多推荐了,返回首页