22 Sep 2016

有向图连通性相关

以为无向图割点已经烦爆了,没想到有向图更烦

这是有关有向图连通性的妇孺皆知我不知的一些东西

• Flow Graph: 若有向图$G$中存在一点$r$,从$r$出发可以到达$G$中所有的点,则称$G$是FlowGraph,记为$(G,r)$

• 搜索树(Dfs Tree): 从$r$出发对$G$进行深度优先遍历,递归时经过的有向边构成一棵树,称为$G$以$r$为根的搜索树

• 时间戳(Dfn): dfn表示时间戳,id表示时间戳对应的节点

Flow Graph$(G,r)$里面边的类型:

• 树枝边(tree arcs):边$(x,y)$在搜索树$T$中;

• 前向边(forward arcs):在$T$中存在一条从$x$到$y$的路径;

• 后向边(cycle arcs):在$T$中存在一条从$y$到$x$的路径;

• 横叉边(cross arcs):在$T$中既没有$x$到$y$的路径,也没有$y$到$x$的路径,并且dfn[y]<dfn[x]。

$(G,r)$也就只有这点边了


然后是关于割点的定义:

• 必经点(即有向图割点): 若在$(G,r)$中从$r$到$y$的路径一定经过点$x$,则称$x$是从$r$到达$y$的必经点,记为$x\ dom\ y$

• 必经点集合: 从$r$出发到达$y$的所有必经点构成的集合记为$dom(y)$,即$dom(y)=\lbrace x \mid x ~dom ~y \rbrace$

• 最近必经点(idom): 节点$y$的必经点集合$dom(y)$中dfn值最大的点$x$是距离$y$最近的必经点,称为$y$的最近必经点。最近必经点是唯一的,因此可以记$x=idom(y)$


然后是奇怪的半必经点:

• 半必经点(semi): 在搜索树$T$上点$y$的祖先中,通过非树枝边可以到达$y$的深度最小的祖先$x$,称为$y$的半必经点。半必经点也是唯一的,因此可以记$x=semi(y)$

* 这里有个坑就是一个点的父亲也可以是它的$semi$

• 半必经点定理:

对于$G$中的一点$y$,考虑所有$x \in pre(y)$[即$\exists(x,y)$],定义一个临时变量$temp=+\infty$

若$dfn[x]<dfn[y]$,则$(x,y)$为树枝边或前向边,此时$temp=\min⁡(temp,dfn[x])$

若$dfn[x]>dfn[y]$,则$(x,y)$为横叉边或后向边,此时$\forall x$在$T$中的祖先$z$,满足$dfn[z]>dfn[y]$时,有$temp=min⁡(temp,dfn[semi[z]])$

$semi[y]=id[temp]$。即在上述所有可能情况中取$dfn$值最小的一种,就是$y$的半必经点。

注意半必经点不一定是必经点

由于论文没有证明,我来简要证明一下

* if $dfn[x]<dfn[y]$ 如果这条边是横叉边,那么由深搜的性质,$y$点会被先搜到,而不是分在两个子树里,所以这条边只可以是前向边或树枝边,$semi[y]=\min(semi[y],dfn[x])$显而易见

* if $dfn[x]>dfn[y]$ 上定理成立的原因:$semi[y]=dfn[semi[z]]$是显然的,相当于从自己的一个祖先跳到别的树枝上再跳回来。然后又要满足$semi[y]$的最小性,所以取$\min$。

* Q&A:

Q:如果没有自己祖先跳到别的树枝上再跳回来的边呢?[即$\min(dfn[semi[z]])>dfn[y]$]

A:那么这个还有$dfn[x]<dfn[y]$的情况,至少父亲会用一条树枝边指向它,由定理中的*那条,父亲可以是他的$semi$,所以不影响

那么这个定理就证明完了


还有一个必经点定理:

• 对于$G$中的一点$x$,考虑搜索树$T$中semi(x)到$x$的路径上除端点之外的点构成的集合$path$

• 设$y=id[\min⁡\lbrace dfn[semi(z)] \mid  z\in path\rbrace  ]$,即path中半必经节点的时间戳最小的节点。

• $idom(x)=\begin{cases}semi(x), &\text{semi(x) = semi(y)} \\ idom(y), & \text{semi(x) $\neq$ semi(y)} \end{cases}$

证明:

* Q&A:

Q: 为什么不包含端点

A: 首先显然是不包含$x$自己的,然后如果包含了semi(x),那么它的idom岂不会沿着semi(x)一直跳,那还搞…*&

Q: 为什么要找$y=id[\min⁡\lbrace dfn[semi(z)] \mid  z\in path\rbrace  ]$?

A: 因为首先$path$是$(semi[x] \to x)$的路径,$semi[x]$有到$x$的边。也就是说,$idom[x] \leq semi[x]$ ,最小的$semi[y]$,有一条跳向$y$的边,$y$有一条通往$x$的树枝边,又$y$到$x$之间不存在$idom[x]$$(idom[x]\leq semi[x])$,说明必经点必定在$semi[y]$之前,或者说,必经点就是$idom[y]$

Q: 那不是有可能点$z$的$idom[z]<idom[y]$呀,答案不应该是$\min\lbrace idom[z] \mid z \in path\rbrace $么($idom[z]$指其dfs序)

A: 这个很有道理,但是这样的话$idom[y]$也就$~=idom[z]$了

那么证明算是结束了


有了这些乱七八糟的玩意我们就可以构建一棵Dominator Tree

• 设有向图$G=(V,E)$,$(G,r)$是一个Flow Graph,则称$(G,r)$的子图$D=(V,\lbrace (idom(i),i) \mid i \in V,i \neq r \rbrace,r)$为$(G,r)$的一棵Dominator Tree。

• $(G,r)$的DominatorTree是一棵有向有根树,从$r$出发可以到达$G$中的所有点,并且树上的每条边$(u,v)$都满足:$u=idom(v)$,即父节点是子节点的最近必经点。

• $x=idom(y)$,当且仅当有向边$(x,y)$是DominatorTree中的一条树枝边。

• $x\ dom\ y$,当且仅当在DominatorTree中存在一条从$x$到$y$的路径。

• $x$的必经点集合$dom(x)$就是DominatorTree上x的所有祖先以及$x$自身。

看看看,性质多么优良的一棵树!自适应有向图的连通性问题呢!

接下来讲构建,用到Lengauer-Tarjan算法:


Lengaur-Tarjan

• Lengauer和Tarjan于1979年提出,经典Tarjan算法的进一步应用。

• 时间复杂度$O(NlogN)$或$O(Nα(N))$,几乎接近于线性。

• 基本思想:

• 通过DFS构建搜索树,并计算出每个节点的时间戳。

• 根据半必经点定理,按照时间戳从大到小的顺序计算每个节点的半必经节点。

• 根据必经点定理,按照时间戳从小到大的顺序,把半必经节点≠必经节点的节点进行修正。

这个最好自己理解下,用到了之前的定理以及dfs序的性质,附代码:

Code

int dfn[M],tid[M],anc[M],dfs_clock;
int idom[M],semi[M],best[M],f[M];
//anc:并查集的集合标号  best:就是那个路径压缩维护的东西
typedef vector<int> vec;
vec pre[M],G[M],dom[M];
//pre[x]:x的前驱节点 G[x]:x的后向节点 dom:新图
inline void predfs(int v){
	dfn[v]=++dfs_clock,tid[dfs_clock]=v;
	semi[dfs_clock]=anc[dfs_clock]=best[dfs_clock]=dfs_clock;
	for(int i=0;i<G[v].size();++i){
		int to=G[v][i];
		if(!dfn[to])predfs(to),f[dfn[to]]=dfn[v];
	}
}
inline int get(int x){
	if(anc[x]==x)return x;
	int y=get(anc[x]);
	if(semi[best[x]]>semi[best[anc[x]]])best[x]=best[anc[x]];
		//maintain
	anc[x]=y;//path compression
	return y;
}
inline void Min(int &a,int b){
	if(a>b)a=b;
}
inline void Tarjan(){
  	//注意新图完全构建在dfs序上
  	#define pb push_back
  	for(int i=1,a,b;i<=m;++i){
		scanf("%d %d",&a,&b);
		G[a].pb(b),pre[b].pb(a);
	}
  	predfs(1);
	for(int y=dfs_clock;y>1;--y){
		int x=f[y],w=tid[y];
		for(int i=0;i<pre[w].size();++i){
			int z=dfn[pre[w][i]];
			if(!z)continue;
			get(z);
			Min(semi[y],semi[best[z]]);
		}
		dom[semi[y]].pb(y);
		anc[y]=x;
		for(int i=0,z;i<dom[x].size();++i){
			z=dom[x][i];
			get(z);
			if(semi[best[z]]<x)idom[z]=best[z];
			else idom[z]=x;
		}
		dom[x].clear();
	}
	for(int i=2;i<=dfs_clock;++i){
		if(idom[i]!=semi[i])idom[i]=idom[idom[i]];
		dom[idom[i]].pb(i);
	}
}

于是决定放几道裸题:


CodeChef May15 Task10 GRAPHCNT

直接就有题解了自己点上面的标题


HDU4694 Important Sisters

题目大意:

有一棵$n$个节点以$n$为源的树。问每个节点到$n$节点的必经点的编号和。


题解:

目测不需要

Code

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define bug(x) cout<<#x<<"="<<x<<" "
#define debug(x) cout<<#x<<"="<<x<<endl
const int M=5e4+5;
typedef long long ll;
int n,m;
inline void rd(int &a){
	a=0;char c;
	while(c=getchar(),!isdigit(c));
	do a=a*10+(c^48);
		while(c=getchar(),isdigit(c));
}
struct Edge{
	int to,nxt;
}G[M<<1][3];
int head[M][3],tot_edge[3];
inline void add_edge(int from,int to,int f){
	G[tot_edge[f]][f]=(Edge){to,head[from][f]};
	head[from][f]=tot_edge[f]++;
}
inline void Min(int &a,int b){
	if(a>b)a=b;
}
int best[M],anc[M],semi[M],idom[M];
int dfn[M],f[M],tid[M],dfs_clock;
inline void predfs(int v){
	dfn[v]=++dfs_clock;
	tid[dfs_clock]=v;
	for(int i=head[v][0];~i;i=G[i][0].nxt){
		int to=G[i][0].to;
		if(dfn[to])continue;
		predfs(to),f[dfn[to]]=dfn[v];
	}
}
inline int get(int x){
	if(x==anc[x])return x;
	int y=get(anc[x]);
	if(semi[best[x]]>semi[best[anc[x]]])best[x]=best[anc[x]];
	return anc[x]=y;
}
ll res[M];
template<class T>void nt(T x){
	if(!x)return;
	nt(x/10);
	putchar(48+x%10);
}
template<class T>void pt(T x){
	if(!x)putchar('0');
	else nt(x);
}
inline void dfs(int v){
	res[tid[v]]+=tid[v];
	for(int i=head[v][2];~i;i=G[i][2].nxt){
		int to=G[i][2].to;
		res[tid[to]]+=res[tid[v]];
		dfs(to);
	}
}
inline void Init(){
	for(int i=1;i<=n;++i){
		best[i]=anc[i]=semi[i]=i;
		idom[i]=res[i]=dfn[i]=0;
		for(int j=0;j<3;++j)
			head[i][j]=-1;
	}
	dfs_clock=f[1]=0;
	memset(tot_edge,0,sizeof tot_edge);
}
inline void gao(){
	Init();
	for(int i=1,a,b;i<=m;++i){
		rd(a),rd(b);
		add_edge(a,b,0),add_edge(b,a,1);
	}
	predfs(n);
	for(int y=dfs_clock;y>1;--y){
		int x=f[y];
		for(int i=head[tid[y]][1];~i;i=G[i][1].nxt){
			int z=dfn[G[i][1].to];
			if(!z)continue;
			get(z);
			Min(semi[y],semi[best[z]]);
		}
		anc[y]=x;
		add_edge(semi[y],y,2);
		for(int i=head[x][2];~i;i=G[i][2].nxt){
			int z=G[i][2].to;
			get(z);
			if(semi[best[z]]<x)idom[z]=best[z];
			else idom[z]=x;
		}
		head[x][2]=-1;
	}
	for(int x=2;x<=dfs_clock;++x){
		if(idom[x]!=semi[x])idom[x]=idom[idom[x]];
		add_edge(idom[x],x,2);
	}
	dfs(1);
	for(int i=1;i<n;++i)
		pt(res[i]),putchar(' ');
	pt(res[n]),putchar('\n');
}
int main(){
	for(;cin>>n>>m;)gao();
	return 0;
}

Tags:
Stats:
comments


Share:


Comments: