传送门
世事无常,数据加强。DFS SPFA 直接被 ban 掉了,关键是能过的 5 个点中 WA 掉了一个点(原因:没有按照题意建图,还好代码是对的)-_-||。看来这个专题要重修了 QAQ。
在学习差分约束的时候曾经了解到,找负环的方法主要是使用 SPFA。BFS 的 SPFA 的判断方法为如果有一个点入队 n 次就说明有负环,时间复杂度为 O(ke) O ( k e ) (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) O ( n m ) ,否则将会退化至指数级。
还是回归本质用 BFS 算了。我们新建一个超级点,向其它所有点连一条边权为 0 0 的有向边,显然原图中的负环不受影响。我们以超级点作为源点,显然此时其它所有点都要入队,并且距离变成 0 0 。然后我们继续操作,直到队列为空,或者存在一个点入队至少点数次,即 n+1 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;
}