专栏文章
三色 dfs
算法·理论参与者 10已保存评论 10
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 10 条
- 当前快照
- 1 份
- 快照标识符
- @miqlm7bj
- 此快照首次捕获于
- 2025/12/04 06:48 3 个月前
- 此快照最后确认于
- 2025/12/04 06:48 3 个月前
初次相遇
CF2043E 官方题解,将这种算法称为 "three-colored DFS"。
作用
在有向图上找出一个简单环,或报告无环。
实现
实现一个递归函数
dfs(u),其中参数 为一个结点的编号,函数返回值为是否有环。定义一个栈 ,实时维护当前在 dfs 栈中的所有结点编号,按被 dfs 访问的时的时间戳排序。若图上存在简单环,则保证函数结束时 从底到顶保存了图上任意一个简单环的所有结点的任意一个环排列。
对于每个结点,定义三种状态:
- 该结点未被 dfs 访问过
- 该结点被 dfs 访问过且在 dfs 栈中
- 该结点被 dfs 访问过且不在 dfs 栈中
每个结点的初始状态为 0。
调用
dfs(u) 时需要保证 的状态为 0。dfs(u) 步骤如下:-
将 加入
-
将 的状态设为 1
-
遍历 的所有出点 。
-
若 的状态为 0:
- 若
dfs(v)返回有环,则返回有环
- 若
-
否则若 的状态为 1:
-
将早于 加入 的所有结点从 中删除
-
返回有环
-
-
-
将 的栈顶(此时一定是 )弹出
-
将 的状态设为 2
-
返回无环
算法主体步骤如下:
-
遍历给定图的所有结点
-
若 状态为 0
-
若
dfs(i)返回有环- 报告有环,stk 即为图上任意一个简单环的所有结点的任意一个环排列
- 退出算法
-
-
-
报告无环
-
退出算法
时空复杂度
设 分别为输入的有向图的点数与边数。
时间复杂度:
空间复杂度:
证明
时空复杂度不用证明。
要证明算法正确性,就是要证明如下命题成立:若图中存在简单环,则必定存在一个简单环由若干条树边与恰好一条返祖边构成。
严谨证明见:xzf_200906 的证明。感谢 xzf_200906的贡献!
例题
简要题意
解法
简单环也叫简单回路,故存在回路则一定存在简单环。所以考虑先使用三色 dfs,求出任意一个简单环的所有结点的任意一个环排列,设求出的一个简单环环长为 。
接着对于 ,遍历求出的环排列的第 个结点的所有出点,若出点为第 个结点,则输出这条边的编号。
因为求出的环排列不会存在重复的结点,所以枚举这些结点的所有出点的时间复杂度为均摊 。
于是就在 的时空复杂度下正确地完成了此题。
代码
CPP#include <bits/stdc++.h>
using namespace std;
constexpr int N = 500000 + 1, M = 500000 + 1;
int edge[M][2], tail[N], stk[N];
char vis[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
++u; ++v;
edge[i][0] = v; edge[i][1] = tail[u];
tail[u] = i;
}
int top = 0;
auto dfs = [&](auto &&self, int u) -> bool {
vis[u] = 1;
stk[++top] = u;
for (int i_ = tail[u]; i_; i_ = edge[i_][1]) {
int v = edge[i_][0];
if (!vis[v]) {
if (self(self, v)) {
return true;
}
} else if (vis[v] == 1) {
for (int i = 1; i <= top; ++i) if (stk[i] == v) {
memmove(stk + 1, stk + i, sizeof(int) * (top -= i - 1));
break;
}
return true;
}
}
--top;
vis[u] = 2;
return false;
};
for (int i = 1; i <= n; ++i) if (!vis[i] && dfs(dfs, i)) {
break;
}
if (!top) {
cout << "-1\n";
return 0;
}
cout << top << '\n';
for (int i = 1; i < top; ++i) {
for (int j_ = tail[stk[i]]; j_; j_ = edge[j_][1]) {
if (edge[j_][0] == stk[i + 1]) {
cout << j_ - 1 << '\n';
break;
}
}
}
for (int j_ = tail[stk[top]]; j_; j_ = edge[j_][1]) {
if (edge[j_][0] == stk[1]) {
cout << j_ - 1 << '\n';
break;
}
}
return 0;
}
优缺点
最主要的优点是不用进行 tarjan 或使用其他算法求出强连通分量就可以找出环,常数较为优秀,实现较为简短。
实际上该算法就是 tarjan 的弱化版,和 tarjan 同样利用了 dfs 生成树上返祖边的性质。
只不过判定是否有环(是否是 dag)常数并不如拓扑排序。
相关推荐
评论
共 10 条评论,欢迎与作者交流。
正在加载评论...