简介
Tarjan算法可以求出一个有向图中的强连通分量的个数,同时还可以将强连通分量改造为一个点,也就是所谓的“缩点”。
前置知识
强连通分量
强连通分量(或者极大连通子图),指的是在一个子图中,所有点能够两两互达,且再加入新的点时将破坏连通性。特别要注意的是,一个点可以被认为是一个强连通分量。
dfs过程产生的边
在tarjan算法的dfs过程中,会产生以下四种边(默认x->y
):
- 树枝边:x是y的父节点
- 前向边:x是y的祖先结点
- 后向边:y是x的孩子节点
- 横叉边:连接到其他连通分量的边
算法
时间戳
Tarjan算法引入了“时间戳”的概念。具体来讲,在dfs每一个点时,将会赋予每一个点一个“时间点”,也就是维护dfn
。其次,维护一个low
,表示当前这个点和它的子树返祖边和横插边能连到的还没出栈的dfn
最小的点。
过程
在dfs的过程中维护一个栈,每个点第一次访问时就将其加入栈中。当这个点的所有后继节点遍历完毕时,如果这个点的dfn==low
,则这个点是这个强连通分量里面dfs树的最高的点。于是,可以将栈中的点一个一个出栈,直到当前这个点也出栈为止。
在遍历一个点的子树过程中,也需要判断后继顶点是否遍历过。简单来说,当后继顶点遍历过(而且在栈里面时),则将这个点的low
和后继顶点的low
取最小,否则直接dfs下去,再更新low
。
代码实现
vector<int> edge[N];
int low[N], dfn[N], timestamp = 0, scc_cnt = 0, id[N] = 0;
bool inst[N];
stack<int> st;
void tarjin(int u) {
dfn[u] = low[u] = ++ timestamp;
st.push(u);
inst[u] = true;
for (auto v : edge[u]) {
if (!dfn[v]) {
trajin(v);
low[u] = min(low[u], low[v]);
} else if (inst[v]) {
low[u] = min(low[u], low[v]);
}
}
if (dfn[u] == low[u]) {
int v = 0;
++ scc_cnt;
do {
v = st.top();
st.pop();
inst[v] = false;
id[v] = scc_cnt;
/*
* 这里是还有一些其他条件的时候
*/
} while (v != u);
}
}
void solve() {
/*
* 其他的代码
*/
// 考虑到图其实不一定是一个连通图
for (int i = 1; i <= n; ++ i)
if (!dfn[i])
trajan(i);
}
缩点
在上面的代码中,实际上tarjan算法也创建了一个新的图,这个图中每一个顶点都是原来图中的强连通分量。也就是说,现在这个新的图不再存在回路,是一个DAG
。因此,这个新的图可以执行原先不能的操作,比如拓扑排序等等。这样的操作就可以叫做缩点。
缩点的应用
例题 P2746 [USACO5.3]校园网Network of Schools
[题目链接]([P2746 USACO5.3]校园网Network of Schools - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
一个一个问题分解看待。
- 对于A问题,求这个网络中至少需要接受几个新的软件副本。那么如果根据拓扑序的定义的话,遍历完一张图只需要从其入度为0的点开始遍历即可。
- 对于B问题,求至少连多少边,只需要一个副本就可以完成任务。还是考虑拓扑排序。如果能够把入度为0的点和出度为0的点相连的话,那么是不是意味着这两个图是连通的。当入度(或者出度)为0的点在之前的步骤之后还有多余的话,那么是不是意味着这几个点相连的话就可以实现。因此,这个问题的答案就是入度为0的点的个数和出度为0的点的个数中的最大值。
从上面的分析可以得到,我们需要的的是一个DAG
图。但是这个图并不是一个DAG
图。因此需要使用tarjan算法求缩点,将图改造为一个DAG
图。
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 110;
vector<int> edge[N];
void add(int from, int to) {
edge[from].push_back(to);
}
int n;
int low[N], dfn[N], step, scc, id[N];
bool inst[N];
stack<int> st;
int ind[N], outd[N];
void tarjan(int u) {
dfn[u] = low[u] = ++ step;
st.push(u);
inst[u] = true;
for (auto v : edge[u]) {
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[v], low[u]);
} else if (inst[v]) {
low[u] = min(low[v], low[u]);
}
}
if (dfn[u] == low[u]) {
int v = 0;
++ scc;
do {
v = st.top();
st.pop();
inst[v] = false;
id[v] = scc;
} while (v != u);
}
}
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; ++ i) {
int v;
while (cin >> v && v) {
add(i, v);
}
}
for (int i = 1; i <= n; ++ i) {
if (!dfn[i]) {
tarjan(i);
}
}
for (int i = 1; i <= n; ++ i) {
for (auto j : edge[i]) {
if (id[j] != id[i]) {
ind[id[j]] ++;
outd[id[i]] ++;
}
}
}
if (scc == 1) {
cout << 1 << endl;
cout << 0 << endl;
return 0;
}
int in = 0, out = 0;
for (int i = 1; i <= scc; ++ i) {
in += ind[i] == 0;
out += outd[i] == 0;
}
cout << in << endl;
cout << max(in, out) << endl;
}