Tarjan与支配树
Tarjan
大概分两类:无向图连通性和有向图连通性。先说有向图,比较简单。(大概)
有向图的连通性大体上就一个:强连通分量。这个我觉得大家都背过了。
首先定义两个数组\(dfn,low\)。\(dfn\)是每个节点在搜索树中的dfs序,low的定义是满足下列条件的最小\(dfn[x]\):
- 该点在当前搜索栈中。
- 存在一条从x子树出发的有向边到该点。
那么,我们有强连通分量判定法则:若搜完x时\(dfn[x]=low[x]\),则该点到栈顶的所有节点构成强连通分量。代码:
void tarjan(int x){
dfn[x]=low[x]=++num;s.push(x);v[x]=true;//v标记是否在栈中
for(int i=head[x];i;i=edge[i].next){
if(!dfn[edge[i].v]){//如果当前没搜过则继续加入搜索树
tarjan(edge[i].v);
low[x]=min(low[x],low[edge[i].v]);//树边 可以更新父亲的low
}
else if(v[edge[i].v])low[x]=min(low[x],dfn[edge[i].v]);//搜过而且在栈中 按定义更新
}
if(low[x]==dfn[x]){
int y;cnt++;//按照判定法则更新答案
do{
y=s.top();s.pop();
belong[y]=cnt;size[cnt]++;
v[y]=false;//记得取消在栈中标记
}while(x!=y);
}
}
然后缩点的话很简单,暴力扫描每个点就行。
void make(){
for(int x=1;x<=n;x++){
for(int i=head[x];i;i=edge[i].next){
if(belong[x]!=belong[edge[i].v])add1(belong[x],belong[edge[i].v]);
}
}
}
然后是博大精深的无向图方面。(我考试就是因为忘了tarjan怎么打然后爆0所以来修缮博客)
- 割点与点双
仍然定义数组\(low\)为满足以下条件的节点dfs序最小值:
- x子树上的节点。
- 通过一条不在搜索树上的边能到达x子树的节点。
然后是割点判定法则:若x不是根则当且仅当存在一个子节点y使得\(dfn[x]\le low[y]\)。特别的,若x是根则需要有两个。代码:
void tarjan(int x){
dfn[x]=low[x]=++num;
int son=0;
for(int i=head[x];i;i=edge[i].next){
if(!dfn[edge[i].v]){
son++;
tarjan(edge[i].v);
low[x]=min(low[x],low[edge[i].v]);//按定义1求解
if(dfn[x]<=low[edge[i].v]){
if(x!=rt||son>1)cut[x]=true;//更新答案
}
}
else low[x]=min(low[x],dfn[edge[i].v]);//定义2的条件
}
}
然后是点双和缩点。为求出点双要维护一个栈:
- 第一次访问时入栈。
- 割点判定法则成立时:(设儿子为y)从栈顶不断弹出节点直到y,弹出的所有节点与x组成一个点双。(x仍然在栈里)
于是代码也是类似的(这是我建圆方树的代码):
void tarjan(int x,int f){
dfn[x]=low[x]=++num;s[++top]=x;
bool first=false;
for(int i=head[x][0];i;i=edge[i].next){
if(first&&edge[i].v==f){
first=false;continue;
}
if(!dfn[edge[i].v]){
tarjan(edge[i].v,x);
low[x]=min(low[x],low[edge[i].v]);
if(dfn[x]<=low[edge[i].v]){
cnt++;
add(x,cnt,1);add(cnt,x,1);
int y;
do{
y=s[top--];
add(cnt,y,1);add(y,cnt,1);
}while(y!=edge[i].v);
}
}
else low[x]=min(low[x],dfn[edge[i].v]);
}
}
- 割边(或者说桥)
割边判定法则:(x,y)是割边当且仅当x存在一个子节点y,使\(dfn[x]<low[y]\)。
但是为了处理重边,我们不能简单地只把父节点跳过。于是我们可以采用成对变换来标记边。
void tarjan(int x){
dfn[x]=low[x]=++num;
for(int i=head[x];~i;i=edge[i].next){
if(i==(p[x]^1))continue;//p是编号
if(!dfn[edge[i].v]){
p[edge[i].v]=i;//记录哪条边通到儿子(处理重边)
tarjan(edge[i].v);
low[x]=min(low[x],low[edge[i].v]);
if(dfn[x]>low[edge[i].v]){
cnt++;cut[i]=cnt[i^1]=true;
}
}
else low[x]=min(low[x],dfn[edge[i].v]);
}
}
而边双就比点双简单很多,直接把割边去掉剩下的连通块就是边双。dfs实现即可。缩点也同最开始的强连通分量,爆算就行。代码在此不表。
支配树
支配树,即给你一个有向图,让你求每个点的支配点。
支配点:若从起点删去点x后无法到达该点则x为支配点。显然一个点的支配点不止一个。我们开一个数组\(idom\)表示。
我们使用Lengauer-Tarjan算法求出支配树。大概分三步:
- dfs序。
- 半支配点。
- 支配点。
半支配点:点x的半支配点y为:存在一条路径使得y能到达x,且路径上除y以外的所有点的dfs序都比x点大。这些点中dfs序最小的为x的半支配点。显然一个点的半支配点只有一个,我们用数组\(sdom\)存储。
第一步dfs序不用说。直接开始求半支配点。先贴个dfs代码。
void dfs(int x){
dfn[x]=++num;rnk[num]=x;//rnk是个双射
for(int i=head[0][x];i;i=edge[i].next){
if(!dfn[edge[i].v]){
fa[edge[i].v]=x;//搜索树上的父亲
dfs(edge[i].v);
}
}
}
根据定义,我们可以按dfs序倒序暴力枚举所有的点和能直接到达这个点的所有点。也就是说,我们建一个反图然后枚举每个点。对于能到达x的所有点y,对其dfs序进行比较。若\(dfn[y]<dfn[x]\),则其可能是半支配点(因为祖先可能有更小的)。反之,继续跳所有dfs序大于y的祖先,祖先的半支配点可能成为x的半支配点。
但是这个显然是\(O(n^2)\)的,我们尝试优化。我们发现,许多点的半支配点都是一个点,但是我们每个点都要\(O(n)\)遍历整张反图上的所有祖先,进行了许多冗余操作。我们可以尝试在这个方面下功夫。
采用并查集。我们每次处理一个点,就把它和它在搜索树上的父亲合并,同时更新半支配点。这样就可以直接找并查集上半支配点dfs序最小的一个。据说这个是\(O(nlogn)\)的。反正2e5飞快。
求解半支配点之后,我们尝试找到一个点的支配点。(好了又到了一群我看不懂的结论所以直接上结论得了)
Tarjan认为,对每个点\(y\)到它的半支配点\(x\)的链(不算\(x\))上面半支配点dfs序最小的点\(u\)与\(u\)的半支配点\(v\),有:
- 若\(x=v\),则\(x\)为\(y\)的支配点。
- 若\(x\)的dfs序大于\(v\)的dfs序,则\(y\)的支配点是\(u\)的支配点。
所以我们仍然倒序枚举所有点的dfs序。
仍然对于每个点考虑其半支配点。我们要扫描这条链上半支配点dfs序最小的点\(u\)。这个东西……似乎可以跟着上边的带权并查集一起维护。于是我们就可以只扫一次解决。
求出\(y\)的半支配点\(x\)之后从\(x\)到\(y\)连一条有向边,建立支配树。同时枚举\(y\)在搜索树上的父亲在支配树上的所有儿子,于是我们就得到了我们需要的点\(u\)。
扫描完毕后,我们还需要处理一下上面第二种情况中确定的所有点。直接按照dfs序正序扫描就好。
注意每次枚举之后由于我们统计过了儿子,不用再次统计所以直接清空就行。
最后放个洛谷板子的代码。统计答案直接dfs序倒着扫然后后面的加了前面的也加就行。
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
int n,m,t,head[3][200010],ans[200010];
struct node{
int v,next;
}edge[1000010];
void add(int u,int v,int id){//0原图 1反图 2支配树
edge[++t].v=v;edge[t].next=head[id][u];
head[id][u]=t;
}
int num,dfn[200010],rnk[200010],fa[200010];
void dfs(int x){
dfn[x]=++num;rnk[num]=x;//rnk是个双射
for(int i=head[0][x];i;i=edge[i].next){
if(!dfn[edge[i].v]){
fa[edge[i].v]=x;//搜索树上的父亲
dfs(edge[i].v);
}
}
}
int idom[200010],sdom[200010],f[200010],minn[200010];
int find(int x){
if(x==f[x])return x;
int rt=find(f[x]);
if(dfn[sdom[minn[f[x]]]]<dfn[sdom[minn[x]]]){
minn[x]=minn[f[x]];//查找链上支配点dfs序最小的点可以放到并查集里
}
return f[x]=rt;
}
void Lengauer_Tarjan(int st){
dfs(st);//求解dfs序
for(int i=1;i<=n;i++)sdom[i]=f[i]=minn[i]=i;
for(int i=num;i>=2;i--){//显然dfs序为1的点没有半支配点
int x=rnk[i];
for(int i=head[1][x];i;i=edge[i].next){
if(dfn[edge[i].v]){
find(edge[i].v);
if(dfn[sdom[minn[edge[i].v]]]<dfn[sdom[x]]){
sdom[x]=sdom[minn[edge[i].v]];//用祖先的半支配点更新x的半支配点
}
}
}
f[x]=fa[x];
add(sdom[x],x,2);x=fa[x];//连边 同时合并x与它的父亲
for(int i=head[2][x];i;i=edge[i].next){
find(edge[i].v);
if(x==sdom[minn[edge[i].v]]){
idom[edge[i].v]=x;//找到其父亲在支配树上的所有儿子 按照结论更新答案
//显然我们此时minn数组存的就是我们需要的u
}
else idom[edge[i].v]=minn[edge[i].v];
}
head[2][x]=0;
}
for(int i=2;i<=num;i++){
int x=rnk[i];
if(idom[x]!=sdom[x])idom[x]=idom[idom[x]];
}
for(int i=num;i>=2;i--){
ans[rnk[i]]++;
ans[idom[rnk[i]]]+=ans[rnk[i]];
}
ans[1]++;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
add(u,v,0);add(v,u,1);
}
Lengauer_Tarjan(1);
for(int i=1;i<=n;i++)printf("%d ",ans[i]);
return 0;
}