Tarjan与支配树

Tarjan

大概分两类:无向图连通性和有向图连通性。先说有向图,比较简单。(大概)

有向图的连通性大体上就一个:强连通分量。这个我觉得大家都背过了。

首先定义两个数组\(dfn,low\)\(dfn\)是每个节点在搜索树中的dfs序,low的定义是满足下列条件的最小\(dfn[x]\)

  1. 该点在当前搜索栈中。
  2. 存在一条从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所以来修缮博客)

  1. 割点与点双

仍然定义数组\(low\)为满足以下条件的节点dfs序最小值:

  1. x子树上的节点。
  2. 通过一条不在搜索树上的边能到达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的条件 
	} 
}

然后是点双和缩点。为求出点双要维护一个栈:

  1. 第一次访问时入栈。
  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]);
    }
}
  1. 割边(或者说桥)

割边判定法则:(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算法求出支配树。大概分三步:

  1. dfs序。
  2. 半支配点。
  3. 支配点。

半支配点:点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\),有:

  1. \(x=v\),则\(x\)\(y\)的支配点。
  2. \(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;
}
posted @ 2022-09-03 11:38  gtm1514  阅读(23)  评论(0编辑  收藏  举报