对求有向图强连通分量的tarjan算法原理的一点理解

先简单叙述一下tarjan算法的执行过程(其他诸如伪代码之类的相关细节可以自己网上搜索,这里就不重复贴出了):

用到两类数组:

dfs[]:DFS过程中给定节点的深度优先数,即该节点在DFS中被访问的次序

low[]:从给定节点回溯时,节点的low值为从节点在DFS树中的子树中的节点以及该节点通过后退边或横叉边可以回溯到的栈中DFS值最小的节点的dfs值

一个数据结构:栈,用于确定强连通分量

定义:横叉边:一条有向边(u,v)为横叉边,当且仅当(1)u,v之间没有祖先后代关系(2)(1)满足的条件下,设u,v的最近公共祖先为L,u在以L子女节点k1为根的子树T1中,v在以L子女节点k2为根的子树T2中,则T1在T2右侧

前进边:弧尾为祖先节点,弧头为子孙节点的有向边

后退边:弧尾为子孙节点,弧头为祖先节点的有向边

树边:对有向图进行深度优先搜索形成的DFS生成树上的有向边,该有向边始终由父节点指向子女节点

**执行过程:对有向图进行深度优先搜索,每抵达一个新节点A就把该节点A入栈,并初始化dfs[A],然后将low[A]初始化为dfs[A],随后考察该节点A通过边可达的所有节点。若其中一个节点未访问,则对其递归DFS,遍历结束从该节点退出回溯至A时,用该节点low值(已确定不会再改变)更新A的low值(low[A]=min(low[A],low[该节点]))**,若其中一个节点已访问,当该节点的dfs值小于dfs[A]且该节点在栈中时,用该节点dfs值更新A节点low值(low[A]=min(low[A],dfs[该节点])),否则跳过什么也不做。当通过边与A相连的所有节点都考察完毕了,A的low值就最终确定了,不会再变,此时在从A回溯至DFS中A的前驱节点前检查low[A]是否等于dfs[A],若是不断弹栈直到把A弹出为止,弹出的节点组成一个强连通分量,若不是什么都不做,回溯至前驱节点。

伪代码:

Tarjan(v)

{

  stack.push(v);

  dfs[v]=new_dfs();

  low[v]=dfs[v];

  visited[v] = true;

  for(v通过有向边弧头指向的每一个顶点n)

 {

     if(!visited[n])

    {

        Tarjan(n);

        low[v]=min{low[v],low[n]};

    }

    else

   {

       if (dfs[n]<dfs[v] && stack.contain(n))

      {

          low[v]=min{low[v],dfs[n]};

      }

   }

 }

 if(low[v]==dfs[v])

 {

    连续从栈中弹出节点直到v被弹出为止,被弹出的节点组成一个强连通分量

 }

}

要注意的是,算法执行过程中按DFS遍历次序入栈,退栈不影响栈中各节点的DFS访问顺序和它们在栈中位置关系的逻辑关系,所以任何时刻,若栈中B在栈中A之上则dfs[B]>dfs[A],反之也真。

还有就是从tarjan算法伪代码不难看出,当从节点A回溯时必有dfs[A]>=low[A].

并且算法中给定节点仅入栈一次出栈一次访问一次,回溯过给定节点就不会再次回溯,回溯至给定节点时节点的low值就已确定,直至算法结束都不会改变

算法中若一个节点已经入栈,则只有当回溯到该节点或该节点的祖先节点时该节点才可能出栈,而且在回溯到DFS树根节点时该节点之前未出栈而此时必然会出栈,即对于DFS树根节点root有low[root]=dfs[root]

结合这几件简单事实可以证明下面七个命题,学习tarjan算法的关键就是理解回溯到给定节点A时对条件dfs[A]==low[A]的测试的含义以及测试成功后所执行的一系列弹栈操作,以下七个命题有助于理解

定理1:在tarjan算法中回溯到一给定节点A时,若A节点为通过DFS所到达的深度优先树中A节点所在的强连通分量中的第一个被访问的节点(根节点),则栈中A节点之上的所有节点(包括A节点)必为A节点所在强连通分量的所有节点

证明:事实上,取栈中A之上的某节点B,若节点B不属于A所在强连通分量,则B必然属于另外一个不同的强连通分量L,该强连通分量有根节点B’,B’不可能是从未被访问的节点,否则由B’为不同的强连通分量的根节点知B尚未被访问故而不会被压入栈中,矛盾。B’也不可能是已被访问压入栈中但随后又被弹出的节点,若不然当压入B’后必然会回溯至B’,此时检测到dfs[B’]==low[B’],于是从栈中弹出B’和B’以上全部节点,由B’为不同强连通分量根节点知B要么等于B’要么在B’后压栈,故前述弹栈操作结束后无论如何B不会在栈中,而针对B’的弹栈操作在回溯至A节点之前发生(证明:由于B在A之后压栈,故dfs[B]>dfs[A].B不可能尚未回溯完毕,否则B正在被访问,这样B只能是A的子孙而不可能在A的右侧(否则回溯到A时B尚未访问矛盾),故此时还没有回溯到A,矛盾。从而回溯到A时B已经回溯完成,故B是A的子孙。显然B’不为B(否则回溯到A时B’=B已经回溯完成,B已经被弹出栈,矛盾),故B’只能为B的祖先,B’当然不能为A(因为B’所在强连通分量和A所在强连通分量不同),也不能为A的祖先,否则存在路径B’->A->B->B’,故A,B’相互可达,B’属于A所在强连通分量,矛盾,从而B’为A的子孙,B的祖先证毕),故回溯至A节点时B节点已不存在于栈中矛盾。这样B’必然已被访问且在栈中,由前述证明B’不为A,**B’也不可能在栈中A的位置之下,这是因为前述证明指出B’为A的子孙,故B’必在A压栈后压栈,于是我们证明了B’必然位于栈中A之上,这样当回溯至B’时由于dfs[B’]=lowB’,B’及B’之上的所有节点都会被弹出,而B必然在B’后压栈,这样前述弹栈操作结束后B已不在栈中,此后我们才回溯至A,此时B已不在栈中,矛盾。这样就证明了回溯到A时栈中A之上的任一节点必属于A所在强连通分量。此外,当回溯到A时,之前入栈并已被弹出的任一节点C不可能属于A所在强连通分量,若不然,考虑C入栈后回溯到C的时刻,此时由于C属于A所在强连通分量,所以存在A到C的路径,又由于A为A所在强连通分量的根节点,且A和C并非同一节点(注意算法中tarjan算法中一个给定节点仅入栈一次,出栈一次,而回溯到A时C已弹出,此时如A==C,则C在弹出后又入栈回到栈中,矛盾),所以C必为A的子孙,即C必在A被访问后访问(即dfs[C]>dfs[A]),即必在A被压栈之后压栈,这样当回溯到C时A必在栈中且位于C之下,且由于C属于A所在强连通分量,所以存在C到A的路径。当回溯到C时,可以断言必有dfs[C]!=low[C],若不然C成为强连通分量M的根节点,我们有A为A所在强连通分量根节点而A不等于C,C是A的子孙,M中所有节点均为C或C的子孙,从而A不等于M中任意节点,注意到C属于A所在强连通分量,A和C相互可达,A可以和M合并组合成一个更大的强连通分支,这和M为极大强连通子图矛盾,所以就证明了必有dfs[C]!=low[C]。于是当回溯至C时,不会对C执行弹出C及栈中其上节点的弹栈操作。然后可以断言,在回溯至C后和回溯至A前不可能有针对栈中C和A之间(不包括A和C)的节点的弹栈操作,若不然取这些弹栈操作中最早发生的一次,此时将会回溯至栈中C和A之间的节点D,且此时C及其上节点都在栈中且应有dfs[D]=low[D],故应对D执行该弹栈操作。注意到D在栈中位于A之上,dfs[D]>dfs[A],D比A后访问,这样D必位于DFS树中节点A的子树中(D比A后访问,D不可能在A的右侧,否则回溯至C后和回溯至A前D根本没有被访问故不在栈中,矛盾),于是D位于DFS树中节点A的子树中,此外C位于DFS树中节点D的子树中**,(证明:D在栈中位于C之下,C后访问,dfs[D]<dfs[C]若C不在D的子树中,则C在D的右侧,这样回溯至D时C尚未访问而不在栈中矛盾)。这样D的子树中的节点C有一条指向D的祖先节点A的路径,且存在A到C的路径, 故A和D相互可达,从而D属于A节点所在的强

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值