Tarjan算法与连通性

Tarjan算法

  • Tarjan与有向图
    • 一、强连通定义
    • 二、Tarjan算法求强连通分量
      • 2.tarjan的构成要素
      • 3.算法的分析
      • 4.算法的实现
        • 11,未被访问:
        • 22,被访问过,已经在栈中:
      • 5.算法的代码实物
    • 三,缩点
    • 四,实际应用
  • Tarjan和无向图
    • 一,定义和性质
    • 二,割边(桥)和 E-DCC
      • 11,模板
      • 22,实际应用
    • 三,割点
      • 11,概况
      • 22,实现
    • 四,V-DCC(点双联通分量)
      • 1,求v-dcc
      • 2,v-dcc特异性缩点
  • 例题


Tarjan与有向图

一、强连通定义

  • 有向图的任意两个节点可以相互到达则称这两个点强连通(你能到我,我也能到你)
  • 如果图G 的任意两个节点强连通,那么G 就是一个强连通图
  • 有向图的极大强连通子图,称为强连通分量
  • PS : 一个点也可以是强连通的图哦

二、Tarjan算法求强连通分量

2.tarjan的构成要素

这是基于DFS 的算法所以这些东西很重要

int dfn[MAXN] (dfs的时间戳,就是dfn[i]即为i点第几次被搜到的)
int low[MAXN](表示 i号点 它能走到的dfn最小的点的编号,注意:不是dfn号)
stack<int>s  一个栈,用来收集和回溯
int cnt 全局计数器,记录dfn的初始变换,可持续保持数值,不受递归影响
bool st[MAXN] (记录是否在栈里)

3.算法的分析

设一个点 U 为联通子图 G 在 dfs 搜索树里的根,那么他就是在 G 中dfn的min对应的点(根都是最小的,因为先找到了它啊)

  • 就从dfn和low的定义上讲,一个ssc的(假定我们不知道)某个节点必然会dfn==low(low是追溯的最早点,点自己和自己没边,自己能找到自己肯定是ssc)
  • 初始化: 对所有的节点 dfn[u]=cnt++; low[u]=dfn[u];

在没有扩展之前,每个点自己都是一个联通子图。

4.算法的实现

在搜索过程中,对于结点 u 和它指向的节点 v 考虑 2 种情况:

11,未被访问:

判断标准:dfn 没有被定义,为0,压入到栈里st变为true
几何意义:传参前找到了前向边或者树枝边,回溯时后向边造成有效 l o w low low 修改
处理方法

  • 继续对 v v v 进行深度优先搜索。在回溯过程中,用 l o w [ v ] low[v] low[v] 更新 l o w [ u ] low[u] low[u]
  • 因为存在从 u u u v v v 的直接路径,所以 v v v 能够回溯到的已经在栈中的结点, u u u 也一定能够回溯到。
22,被访问过,已经在栈中:

判断标准st [v]
几何意义:横叉边
处理方法:v 即已经被访问过,根据 low 值的定义,使用min函数,可以取到与之相连的栈里最早被dfs的点, l o w [ u ] low[ u ] low[u]则用 d f n [ v ] dfn[ v ] dfn[v]尝试更新

5.算法的代码实物

void tarjan(int x)
{
    low[x]=dfn[x]=++cnt;
    st[x]=1;
    s.push(x);

    for(int i=h[x];~i;i=nxt[i])
    {
        int j=e[i];
        if(!dfn[j])tarjan(j),low[x]=min(low[x],low[j]);
        else if(st[j])low[x]=min(low[x],dfn[j]);
    }

    if(dfn[x]==low[x])
    {
        int y;
        ++scc_cnt;
        do
        {
            y=s.top();  s.pop();  st[y]=0;
            id[y]=scc_cnt;
        }while (y!=x);
    }
}

以下是引用别的巨佬的伪代~

tarjan(u)
{
  DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
  Stack.push(u)   // 将节点u压入栈中
  for each (u, v) in E // 枚举每一条边
    if (v is not visted) // 如果节点v未被访问过
        tarjan(v) // 继续向下找
        Low[u] = min(Low[u], Low[v])
    else if (v in S) // 如果节点u还在栈内
       Low[u] = min(Low[u], DFN[v])
  if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
  repeat v = S.pop  // 将v退栈,为该强连通分量中一个顶点
  print v
  until (u== v)
}

三,缩点

  • 思想:对同一联通分量的点进行染色,同一色为一个大点,大点内部的边吞掉,指向外部的边转换成一个大点指向另一个的点的边
  • 手段
int id[MAXN]  (一个小点所属的大点序号)
vector<int>new_g[MAXN]  (建立一个缩点之后的新图)
核心:
for(each e in color(x)){
	if(e.to is not in subgraph(x))   new_g[x].push_back(color[e.to]);   }

for(int k=1;k<=n;k++)
    {
        for(int i=h[k];~i;i=nxt[i])
        {
            int j=e[i];
            if(id[k]!=id[j])addx(id[k],id[j]),cout<<id[k]<<" --> "<<id[j]<<endl;
        }
    }

四,实际应用

  • 一个拓补图最少加入多少条边构造一个新的scc?
  • ans = max(出度为0的点,入度为0的点)
    证明,略…(超复杂的)

Tarjan和无向图

一,定义和性质

  • 无向图的dfs搜索树中没有横叉边

割边:无向图中删去此边,全图不再联通(图的极大连通分量数增加了)
割点:无向图中删去此点及其相连的边,全图不再联通(图的极大连通分量数增加了)

点双联通分量(V - DCC):不含割点的极大联通子图
边双联通分量(E - DCC):不含割边的极大联通子图

e-dcc性质:

  • 1,任意两点之间存在至少两条不相交(边不重复,点可重)路径
  • 2,删去e-dcc内任意一个单点,e-dcc内部依然联通

v-dcc性质:

  • 1,任意两点之间存在至少两条不相交(点不重复,边可重)路径
  • 2,任意v-dcc内部包含一个割点
  • 3,每一个割点至少属于两个v-dcc
    (这种缩点很不一般)
    e-dcc不一定是v-dcc,v-dcc不一定是e-dcc

割边相连的两点不一定是割点
两个割点的连边不一定是割边

二,割边(桥)和 E-DCC

11,模板

延续tarjan(有向版的框架)

  • 思考:只讨论边(x ,j),如果没有这条边,子节点回不去他的父亲或者祖先,那么联通 x,j 的唯一方法就是这条边,删去立刻不联通
  • 实现:对于搜索完回溯的时候,判断一条无向边(x ,j)若有 d f n [ x ] < l o w [ j ] dfn[x] dfn[x]<low[j](严格大于),判定为割边
  • edcc 的记录类似于scc

小细节:无向图的边是双向的,记录下来边可以防止远地不动

void tarjan_edcc(int x,int from)
{
    low[x]=dfn[x]=++cnt;
    s.push(x);

    for(int i=h[x];~i;i=nxt[i])
    {
        int j=e[i];
        if(!dfn[j])
        {
            tarjan_edcc(j,i);
            low[x]=min(low[x],low[j]);
            if(low[j]>dfn[x])cut_edge[i]=cut_edge[i^1]=1;
        }
        else if(i!=(from^1) )low[x]=min(low[x],dfn[j]);
    }

    if(low[x]==dfn[x])
    {
        int y;
        ++edcc_cnt;
        do{
            y=s.top(); s.pop(); 
            id[y]=edcc_cnt;
        }while (y!=x);
    } 
}

22,实际应用

  • 问题描述:将求完edcc的全图构造为一个大的edcc

  • 做法:统计所有度为1的节点数 c n t cnt cnt ,易证明: a n s ≥ ( c n t + 1 ) / 2 ans\geq{} (cnt+1)/2 ans(cnt+1)/2

  • 几何状态:edcc之后是只有桥的树,度为1的点均为叶子节点,考虑这些叶子节点互联,从底层构造环,抵消桥特性

  • 具体方法:首先把两个lca最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个lca最远的两个叶节点,这样一对一对找完,恰好是 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2次,把所有点收缩到了一起。

三,割点

11,概况

1 ,对于搜索树根结点(找到dfn=0 的递归入口),只有根有大于等于2个的子节点时,他才可以是割点(详见图一和二的比较)
2,对于子节点,若在无向图G中,当且仅当点u存在一个可遍历到的后代v,且点v无法走回点u的前辈时,点u就为割点。
(详见图三的v1和v2对比)

Tarjan算法与连通性_第1张图片

22,实现

1,根节点设为全局变量,对于根结点,统计可以搜索到的子节点(son_num为子树个数)由于是根节点,所以一定存在low==dfn,若是 son_num>1很好,是个割点了

2,子节点判断方法

  • 对于子节点,找到某点如果不过他的父亲,就去不了他的祖先那里,那么这就是一个割点。
  • tarjan裸法里加一个小手段,tarjan裸算法中,搜索回溯tarjan(v); 这一步后,此时v的low一定已经确定,在循环里,介入与dfn的比较,如果v最早只能到x或者更晚的点,那就是不靠x回不到祖先了,那么就是割点了

3,注意,对于割点的判断可能会有很多次,如果把割点计数嵌入tarjan里,就会很多次重复(经验细节谈)

下面的代码嵌入了删除该点之后可在该点所在联通块内可以形成新的联通块的个数

bool cut[N];
void tarjan_cutdot(int x)
{
	low[x]=dfn[x]=++cnt;
	int s=0;
	
	for(int i=h[x];~i;i=nxt[i])
	{
		int j=e[i];
		if(!dfn[j])
		{
			tarjan_cutdot(j);
			low[x]=min(low[x],low[j]);
			if(low[j]>=dfn[x])s++;
		}
		else low[x]=min(low[x],dfn[j]);
	}
	
	if(s>0&&x!=root)cut[x]=1,s++,num=max(num,s);   不是根节点且删除可以产生至少一个子树
	else if(x==root&&s>1)cut[x]=1,num=max(num,s);   是根节点且删除产生大于1的子树
}

例题:ac wing 1183

四,V-DCC(点双联通分量)

1,求v-dcc

  • 判断出现割点的时候,把割点以下的栈内子树全弹出
  • 注意弹出栈不是弹到x(tarjan传参节点)为止 而是弹到 j为止(x仍保留在栈中)

u一定和新分支 j 组成一个dcc 也和旧连通块组成dcc
那么当前最高点u还要被用在更高的包含u的旧连通块
所以如果这个时候出栈了 回溯到比u高的点的时候 u就加不进旧连通块里

void tarjan_vdcc(int x)
{
	low[x]=dfn[x]=++cnt;
	int num=0;
	s.push(x);
	
	if(x==root&&h[x]=-1)
	{
		vdcc_cnt++;
		dcc[vdcc_cnt].push_back(x);
		return ;
	}
	
	for(int i=h[x];~i;i=nxt[i])
	{
		int j=e[i];
		if(!dfn[j])
		{
			trajan_vdcc(j);
			low[x]=min(low[x],low[j]);
			if(low[j]>=dfn[x])
			{
				num++;
				if(num>1||x!=root) cut[x]=1;
				++vdcc_cnt;
				int y;
				do{
					y=s.top(); s.pop();
					dcc[vdcc_cnt].push_back(y);
				}while (y!=j);
					
				dcc[vdcc+cnt].push_back(x);
			}
		}
		else low[x]=min(low[x],dfn[j]); 
	}
}

2,v-dcc特异性缩点

  • 1,每个割点自成一点
  • 2,从每个 vdcc 缩成点向该 vdcc 内的割点连一条边

例题

星 梦 的 b l o g
OI Wiki 的 解 释 这个很棒棒哎,宝藏推荐(原理讲解很好)
算 法 的 手 动 模 拟 走 这 边 !
割 点 的 素 材 来 源 图一上,就很清晰唉

你可能感兴趣的:(图论算法,算法,dfs,强联通,图论)