对一个有向图的深度优先探索有规律地访问它的顶点和走过它的边。走过一条边分为两个阶段,先是在这条边上前进,以后还要在这条边上回退。
维护一个当前点
重复下列三种操作之一直到所有点都访问过,所有边都走过:
我们常把深度优先搜索简称为 DFS。
引理 D1
证明:
我们把一次深度优先探索产生的那些有根树称为 DFS 树。它们一起构成这次探索产生的 DFS 森林。
深度优先探索以先序访问每个 DFS 树的顶点。我们称这些访问为先序访问或初访。此外,我们还考虑深度优先探索对每个顶点
我们给这
一次深度优先探索把除树边和自环之外的边
DFS 维护一条从起点到当前点,由树边构成的路径。在一条树边上前进使此路径多一条边,而在一条树边上回退使它少一条边。我们称此路径为当前路径。
引理 D2
证明:一个点
引理 D3
证明:若
若
引理 D4
证明:
引理 D5
(1) 若
(2) 若
证明:此引理是引理 D3 的推论。
(1) 设
(2) 设
引理 D6
证明:由于

确定是 dag
vector<int> g[maxn];
bool vis[maxn];
vector<int> p;
void dfs(int u) {
if (vis[u]) return;
vis[u] = 1;
for (int v : g[u])
dfs(v);
p.push_back(u);
}
void topo(int n) {
for (int i = 1; i <= n; i++)
dfs(i);
reverse(p.begin(), p.end());
}
可能不是 dag
vector<int> g[maxn];
int vis[maxn];
vector<int> p;
bool dfs(int u) {
if (vis[u] == -1) return false;
if (vis[u] == 1) return true;
vis[u] = -1;
for (int v : g[u])
dfs(v);
p.push_back(u);
}
void topo(int n) {
for (int i = 1; i <= n; i++)
if (!dfs(i)) {
p.clear();
return;
}
reverse(p.begin(), p.end());
}
在有向图
我们常把强连通分量简写为 SCC。
若把
引理 S1
证明:若
引理 S2
证明:此引理是引理 D4 的推论。
我们把一个 SCC 里
换言之,一个 SCC 的首领就是其中最早被 DFS 发现的点。
每个 DFS 树的根一定是它所属的 SCC 的首领,但是首领未必是 DFS 树的根。
引理 S3
证明:设
引理 S4
引理 S5
证明:若
引理 S6
证明:令
充分性。若
注:在引理 S6 的表述中,要求“在
Kosaraju 算法(两次 DFS)
Tarjan 算法(一次 DFS)
首先从
若从
如果我们按 SCC 图的拓扑序的逆序进行 DFS,就能每次只得到一个 SCC。
Kosaraju 算法便是基于这个思想的。
设有向图
Kosaraju 的 SCC 算法有三步:
vector<int> g[maxn];
bool vis[maxn];
vector<int> s;
void dfs(int u) {
if (vis[u]) return;
vis[u] = true;
for (int v :g[u])
dfs(v);
s.push_back(u);
}
//g2是g的反图
vector<int> g2[maxn];
int scc_cnt, sccno[maxn];
void dfs2(int u) {
if (sccno[u]) return;
sccno[u] = scc_cnt;
for (int v : g2[u])
dfs2(v);
}
void find_scc(int n) {
scc_cnt = 0;
s.clear();
memset(sccno, 0, sizeof sccno);
memset(vis, 0, sizeof vis);
for (int i = 1; i <= n; i++)
dfs(i);
for (int i = n - 1; i >= 0; i--)
if (!sccno[s[i]]) {
scc_cnt++;
dfs2(s[i]);
}
}
Tarjan 的 SCC 算法只需要做一次 DFS 就能找出每个 SCC。
利用引理 S6,定义点
可是怎么知道
Tarjan 的 SCC 算法也是以 SCC 的逆拓扑序找出每个 SCC 的,每找出一个 SCC 之后,就把其中的点标记一下,这样如果有从
怎么找出每个 SCC 里的点呢?
准备一个栈。当点
vector<int> g[maxn];
int dfn[maxn], low[maxn], sccno[maxn];
int scc_cnt, num;
stack<int> s;
void dfs(int);
void tarjan_scc(int n) {
for (int i = 1; i <= n; i++)
dfn[i] = sccno[i] = 0;
scc_cnt = num = 0;
for (int i = 1; i <= n; i++)
if (!dfn[i])
dfs(i);
}
void dfs(int u) {
s.push(u);
dfn[u] = low[u] = ++num;
for (int v: g[u])
if (!dfn[v]) {
dfs(v);
low[u] = min(low[u], low[v]);
} else if (!sccno[v])//v和u在同一SCC
low[u] = min(low[u], dfn[v]);
if (low[u] == dfn[u]) {
scc_cnt++;
while (1) {
int v = s.top(); s.pop();
sccno[v] = scc_cnt;//打标记
if (v == u) break;
}
}
}
把上一页的 DFS 做如下改动也是正确的:
解释:对每个点
注意:这种写法算出的 low 值不是上面所说的含义。详见后面的回退路径。
在上一实现的基础上,如果在找到一个 SCC 时,把其中的点的 low 值改为充分大的数,它就影响不到以后找到的 SCC。采用这种想法,代码可以简化
void dfs(int u) {
dfn[u] = low[u] = ++num;
s.push(u);
for (int v: g[u]) {
if (!dfn[v]) dfs(v);
low[u] = min(low[u], low[v]);
}
if (low[u] == dfn[u]) {
scc_cnt++;
while (1) {
int v = s.top(); s.pop();
low[v] = n;
sccno[v] = scc_cnt;
if (v == u) break;
}
}
}
以
void dfs(int u) {
s.push(u);
num += 2; low[u] = num;
for (int v: g[u]) {
if (!low[v]) dfs(v);
if (low[v] < low[u]) low[u] = low[v] | 1;
}
if ((low[u] & 1) == 0) {
scc_cnt++;
while (1) {
int v = s.top(); s.pop();
low[v] = 2 * n;
sccno[v] = scc_cnt;
if (v == u) break;
}
}
}
给你一个有
将此图分解为 SCC 并以拓扑序输出这些 SCC。
const int maxn = 5e5;
vector<int> g[maxn];
int low[maxn], num;
vector<vector<int>> scc;
stack<int> s;
void dfs(int u) {
num += 2;
low[u] = num;
s.push(u);
for (int v : g[u]) {
if (!low[v]) dfs(v);
if (low[v] < low[u])
low[u] = low[v] | 1;
}
if ((low[u] & 1) == 0) {
vector<int> cur;
while (1) {
int v = s.top(); s.pop();
cur.push_back(v);
low[v] = INT_MAX;
if (v == u) break;
}
scc.push_back(cur);
}
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
}
for (int i = 0; i < n; i++)
if (!low[i])
dfs(i);
cout << scc.size() << '\n';
for (int i = scc.size() - 1; i >= 0; i--) {
cout << scc[i].size();
for (int j : scc[i])
cout << ' ' << j;
cout << '\n';
}
}
我们称一条路径为回退路径,若 DFS 在这条路径的边上回退的顺序与边在路径上的顺序相反。
特别地,从一个点到自身的没有边的路径是回退路径。
走若干条树边再走一条非树边
引理 S7
我们可利用上述引理 S7 来找出 SCC 的首领。
如何计算 low 值?
如何判断两点是否在同一 SCC 里?
刚要 or 即将,哪个词更合适?
TODO: 加图