有向图
我们知道在一张 有向无环 图(也叫 DAG)中,肯定存在拓扑序。拓扑序的特殊顺序性质,能够允许我们在 O ( n + m ) O(n + m) O(n+m) 遍历这张图,还可以在图上用 dp 处理问题。
但大部分给的 有向图 都不是 DAG,用 Tarjan 算法,我们可以尝试转化,把环缩成点 —— 强连通分量。
在任意一个强连通分量中,内部的点两两之间都可以通过有向边到达。
预处理缩点的时间是 O ( n + m ) O(n + m) O(n+m)
无向图
无向图本身两点之间就可以成环,所以没有环的说法。
分为两种:
在任意一个边双连通分量中,删去任意一条边不会使图不连通。
在任意一个点双连通分量中,删去任意一个点不会使图不连通;
双连通缩点后的图都长得像一棵树
针对有向图
int h[N], rh[N], e[M], re[N], ne[M], idx;
void add(int h[], int a, int b) {
e[idx] = b, re[idx] = a, ne[idx] = h[a], h[a] = idx++;
}
// Tarjan模板
int dfn[N], low[N], tim;
int stk[N], top;
bool v[N];
int scc_cnt, id[N], siz[N];
//分量个数,属于哪个分量,分量大小
void tarjan(int x) {
dfn[x] = low[x] = ++tim;
stk[++top] = x;
v[x] = true;
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j);
low[x] = min(low[x], low[j]);
}
else if (v[j])low[x] = min(low[x], dfn[j]);
//也在栈中
}
if (dfn[x] == low[x]) {
int y;
scc_cnt++;
do
{
y = stk[top--];
v[y] = false;
id[y] = scc_cnt;
siz[scc_cnt]++;
} while (y != x);
}
}
for (int i = 1; i <= n; i++)
if (!dfn[i])tarjan(i);
//缩点后重建图模板,去重
unordered_map<ll, int> mp;
for (int i = 0; i < idx; i++) {
int b = e[i], a = re[i];
//二维压成一维
ll hash = a * N + b;
if (id[b] != id[a] && !mp.count(hash)) {
mp[hash] = 1;
add(rh, a, b);
}
}
//按拓扑序遍历图,直接反向遍历scc_cnt即可,无需记录入度
for (int i = scc_cnt; i; i--) {
//起点初始
for (int g = rh[i]; ~g; g = ne[g]) {
int j = e[g];
//维护信息
}
}
针对无向图
int dfn[N], low[N], tim;
int stk[N], top;
bool is_bridge[M];
int dcc_cnt, id[N], siz[N];
//分量个数,属于哪个分量,分量大小
void tarjan(int x, int from) {
dfn[x] = low[x] = ++tim;
stk[++top] = x;
//双连通不需要维护 v
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j, i);
low[x] = min(low[x], low[j]);
//记录桥
if (dfn[x] < low[j])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if (i != (from ^ 1))low[x] = min(low[x], dfn[j]);
//防止跟父节点更新就行
}
if (dfn[x] == low[x]) {
int y;
dcc_cnt++;
do
{
y = stk[top--];
id[y] = dcc_cnt;
} while (y != x);
}
}
记录起来较为麻烦,需要特判根节点。
int dfn[N], low[N], tim;
int stk[N], top;
int dcc_cnt, root;
vector<int> dcc[N];//存储每个分量里有哪些点
bool cut[N];//记录割点
void tarjan(int x) {
dfn[x] = low[x] = ++tim;
stk[++top] = x;
if (x == root && h[x] == -1) { //如果只有单独一个点
dcc_cnt++;//单独一个分量
dcc[dcc_cnt].push_back(x);
return;
}
int cnt = 0;//记录该点连接的割点个数
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j);
low[x] = min(low[x], low[j]);
if (dfn[x] <= low[j]) {
cnt++;
if (x != root || cnt > 1)cut[x] = true;
//不是根节点时,因为肯定存在父节点,删去 x 分离了父节点与子节点
//是根节点时,就要保证有两个子节点连到 x 上
//这样才能说明 x 是割点
dcc_cnt++;
int y;
do
{
y = stk[top--];
dcc[dcc_cnt].push_back(y);
} while (y != j);//到 j 就停止!
dcc[dcc_cnt].push_back(x);//割点也放入
//所以点双连通分量是有交集的
}
}
else low[x] = min(low[x], dfn[j]);
//点双连通可以随意取min
}
}
for (root = 1; root <= n; root++)
if (!dfn[root])
tarjan(root);