Tarjan:强连通分量 割点
(他羊)
Tarjan是一种dfs算法。
有向图的强联通分量
如果一个有向图的子图中,任意两点可以相互到达,那么这就组成了一个强联通分量。
在Tarjan算法中,需要维护一个栈stk,
每个节点有两个值:dfn[],即dfs序;low[],表示这个节点最多经过一条横叉边能到达的dfn最小的点的dfn。
流程:
dfs到当前的节点为u,枚举u能到达的点v。
若v没有被dfs过,递归dfs,并用low[v]更新low[u];
若v被更新过且在栈中,则说明u->v是一条横叉边,并用dfn[v]更新low[u]。
遍历完u的子节点后,若dfn[u] == low[u],则说明构成了一个强联通分量。将栈中u及以后的节点都染成相同的颜色并弹出。
- 各个强联通分量出栈的顺序是缩点形成的图中拓扑序的逆序。
几道模板题:
Luogu P3387 【模板】缩点 缩点+记忆化搜索
#include<cstdio> #include<iostream> #include<cmath> #include<cstring> #define MogeKo qwq using namespace std; const int maxn = 2e5+10; int n,m,ans; int cnt,num,now,top; int a[maxn],x[maxn],y[maxn],val[maxn],f[maxn]; int dfn[maxn],low[maxn],sta[maxn],col[maxn]; int head[maxn],to[maxn],nxt[maxn]; bool insta[maxn]; void add(int x,int y) { to[++cnt] = y; nxt[cnt] = head[x]; head[x] = cnt; } void clear() { memset(head,0,sizeof(head)); memset(to,0,sizeof(to)); memset(nxt,0,sizeof(nxt)); cnt = 0; } void tarjan(int u) { dfn[u] = low[u] = ++now; sta[++top] = u; insta[u] = true; for(int i = head[u]; i; i = nxt[i]) { int v = to[i]; if(!dfn[v]) { tarjan(v); low[u] = min(low[u],low[v]); } else if(insta[v]) low[u] = min(low[u],dfn[v]); } if(low[u] == dfn[u]) { col[u] = ++num; insta[u] = false; val[num] += a[u]; while(sta[top] != u) { int v = sta[top--]; col[v] = num; insta[v] = false; val[num] += a[v]; } top--; } } void dfs(int u) { if(f[u]) return; f[u] = val[u]; int sum = 0; for(int i = head[u]; i; i = nxt[i]) { int v = to[i]; dfs(v); sum = max(sum,f[v]); } f[u] += sum; } int main() { scanf("%d%d",&n,&m); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); for(int i = 1; i <= m; i++) { scanf("%d%d",&x[i],&y[i]); add(x[i],y[i]); } for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i); clear(); for(int i = 1; i <= m; i++) if(col[x[i]] != col[y[i]]) add(col[x[i]],col[y[i]]); for(int i = 1; i <= num; i++) { dfs(i); ans = max(ans,f[i]); } printf("%d",ans); return 0; }
#include<cstdio>
#include<iostream>
#define MogeKo qwq
using namespace std;
const int maxn = 100005;
int n,m,a,b,sum,ans;
int now,top,num;
int dfn[maxn],low[maxn],sta[maxn],out[maxn],col[maxn];
int cnt,to[maxn],nxt[maxn],head[maxn];
bool insta[maxn];
void add(int x,int y) {
to[++cnt] = y;
nxt[cnt] = head[x];
head[x] = cnt;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++now;
sta[++top] = u;
insta[u] = true;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(!dfn[v]) {
Tarjan(v);
low[u] = min(low[u],low[v]);
} else if(insta[v])
low[u] = min(low[u],dfn[v]);
}
if(low[u] == dfn[u]) {
col[u] = ++num;
insta[u] = false;
while(sta[top]!=u) {
int v = sta[top];
col[v] = num;
insta[v] = false;
top--;
}
top--;
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1; i <= m; i++) {
scanf("%d%d",&a,&b);
add(a,b);
}
for(int i = 1; i <= n; i++)
if(!dfn[i])
Tarjan(i);
for(int u = 1; u <= n; u++)
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(col[u] != col[v])
out[col[u]]++;
}
for(int i = 1; i <= num; i++) {
if(!out[i]) {
if(ans) {
printf("0");
return 0;
}
for(int u = 1; u <= n; u++)
if(col[u] == i)ans++;
}
}
printf("%d",ans);
return 0;
}
无向图的强连通分量
割点:删去后原图不连通的点。
割边(桥):删去后原图不连通的边。
点双连通分量:无割点的极大强连通子图。
边双连通分量:无割边的极大强连通子图。
- 有割点不一定有桥,有桥一定存在割点。
- 桥一定是割点依附的边。
- 一个点可以在最多2个点双里,但只能在1个边双里(否则可以合并)。
求割点/割边:
根结点u为割顶当且仅当它有两个或者多个子结点;
非根结点u为割顶当且仅当u存在结点v,使得v极其所有后代都没有反向边可以连回u的祖先(不包括u),即low[v]>=dfn[u]。
桥的求法其实也是类似的,当结点u的子结点v的后代通过反向边只能连回v,那么删除这条边(u, v)就可以使得图非连通了。即low[v]>dfn[u]。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#define MogeKo qwq
using namespace std;
const int maxn = 1e6+10;
const int INF = 2147483647;
int n,m,cnt,now,num,top,sum;
int x,y,z;
int to[maxn],head[maxn],nxt[maxn];
int dfn[maxn],low[maxn];
bool iscut[maxn];
void add(int x,int y) {
to[++cnt] = y;
nxt[cnt] = head[x];
head[x] = cnt;
}
void Tarjan(int u,int fa) {
dfn[u] = low[u] = ++now;
int ch = 0;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if(!dfn[v]) {
if(fa == u)ch++;
Tarjan(v,fa);
low[u] = min(low[u],low[v]);
if(fa != u && low[v] >= dfn[u]) iscut[u] = true;
}
low[u] = min(low[u],dfn[v]);
if(fa == u && ch >= 2) iscut[u] = true;
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1; i <= m; i++) {
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for(int i = 1; i <= n; i++)
if(!dfn[i])
Tarjan(i,i);
for(int i = 1; i <= n; i++)
if(iscut[i])sum++;
printf("%d\n",sum);
for(int i = 1; i <= n; i++)
if(iscut[i]) printf("%d ",i);
return 0;
}
割边:
void tarjan(int u,int fa) { dfn[u] = low[u] = ++now; sta[++top] = u; insta[u] = true; for(int i = head[u]; i; i = nxt[i]) { int v = to[i]; if(v == fa) continue; if(!dfn[v]) { tarjan(v,u); low[u] = min(low[u],low[v]); if(low[v] > dfn[u]) cut[i] = true; } else if(insta[v]) low[u] = min(low[u],dfn[v]); } if(low[u] == dfn[u]) { int v; num++; do { v = sta[top--]; insta[v] = false; col[v] = num; } while(v != u); } }
圆方树
将每个点双作为一个方点,这个点双中的点作为圆点,去掉点双中所有内部的边,并将圆点连接到对应的方点。