先简单叙述一下tarjan算法的执行过程(其他诸如伪代码之类的相关细节可以自己网上搜索,这里就不重复贴出了):
用到两类数组:
dfs[]:DFS过程中给定节点的深度优先数,即该节点在DFS中被访问的次序
low[]:从给定节点回溯时,节点的low值为从节点在DFS树中的子树中的节点可以回溯到的栈中DFS值最小的节点的dfs值
一个数据结构:栈,用于确定强连通分量
执行过程:对有向图进行深度优先搜索,每抵达一个新节点A就把该节点A入栈,并初始化dfs[A],然后将low[A]初始化为dfs[A],随后考察该节点A通过边可达的所有节点。若其中一个节点未访问,则对其递归DFS,遍历结束从该节点退出回溯至A时,用该节点low值(已确定不会再改变)更新A的low值,若其中一个节点已访问但不在栈中,则跳过,若已访问且在栈中,则用该节点dfs值跟新A节点low值。当通过边与A相连的所有节点都考察完毕了,A的low值就最终确定了,不会再变,此时在从A回溯至DFS中A的前驱节点前检查low[A]是否等于DFS[A],若是不断弹栈直到把A弹出为止,弹出的节点组成一个强连通分量,若不是什么都不做,回溯至前驱节点。
要注意的是,算法执行过程中按DFS遍历次序入栈,退栈不影响栈中各节点的DFS访问顺序和它们在栈中位置关系的逻辑关系,所以任何时刻,若栈中B在栈中A之上则dfs[B]>dfs[A],反之也真。
还有就是从tarjan算法伪代码不难看出,当从节点A回溯时必有dfs[A]>=low[A].
并且算法中给定节点仅入栈一次出栈一次访问一次,回溯过给定节点就不会再次回溯,回溯至给定节点时节点的low值就已确定,直至算法结束都不会改变
结合这几件简单事实可以证明下面三个命题,学习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‘的弹栈操作在回溯至A节点之前发生,故回溯至A节点时B节点已不存在于栈中矛盾。这样B‘必然在栈中,显然B‘不为A,B‘也不可能在栈中A的位置之下,若不然由B‘为L根节点和B属于L知,有从B到达B‘的路径,但栈中B位于A之上,这样B位于A的子树中(若不然注意栈中B位于A之下即A比B先访问,故dfs[B]>dfs[A],这样当访问B时A已从栈弹出(这是因为B位于DFS树中A节点右侧的A的祖先节点的子女中)),又如前所述有从B到达B‘的路径且dfs[B‘]<dfs[A](因为栈中B‘在A之下)(同时要注意A在B‘子树中,理由同下,若不然访问A时B‘已经因回溯至B‘时dfs[B‘]==low[B‘]而被弹出栈,这样回溯至A时B‘不在栈中,矛盾’),故low[A]<dfs[A]矛盾。于是我们证明了B‘必然位于栈中A之上,这样当回溯至B‘时由于dfs[B‘]=low[B‘](B‘为L的根节点),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被访问后访问(即dfs[C]>dfs[A]),即必在A被压栈之后压栈,这样当回溯到C时A必在栈中且位于C之下,由于C属于A所在强连通分量,所以存在C到A的路径。当回溯到C时,可以断言必有dfs[C]!=low[C],若不然C成为强连通分量M的根节点,由于A为A所在强连通分量根节点而A不等于C,故M和A所在强连通分量为不同的强连通分量,注意到C属于A所在强连通分量,这样A所在强连通分量可以和M合并组合成一个更大的强连通分支,这和M以及A所在强连通分量为极大强连通子图矛盾,所以就证明必有dfs[C]!=low[C]。于是当回溯至C时,不会对C执行弹出C及栈中其上节点的弹栈操作。然后可以断言,在回溯至C后和回溯至A前不可能有针对栈中C和A之间(不包括A和C)的节点的弹栈操作,若不然取这些弹栈操作中最早发生的一次,此时将会回溯至栈中C和A之间的节点D,此时应有dfs[D]=low[D],应对D执行该弹栈操作。注意到D在栈中位于A之上,dfs[D]>dfs[A],D比A后访问,这样D必位于DFS树中节点A的子树中,否则由于D后访问,D就位于A右边的A的祖先节点的子女中,这样访问D时A就已经因之前回溯到A时判断出dfs[A]=low[A]而弹出了,故不在栈中,这样回溯至D时A仍不在栈中(给定节点只会进栈一次),矛盾(回溯至D在回溯至A之前发生,此时A仍在栈中)于是D位于DFS树中节点A的子树中,此外C位于DFS树中节点D的子树中,理由同上(根据假设回溯至D时dfs[D]==low[D],而D在栈中位于C之下,C后访问,若C不在D的子树中,访问C时D已弹出,回溯至C时栈中无D,矛盾(回溯至C在回溯至D之前发生,此时D仍在栈中))。这样D的子树中的节点C有一条指向D的祖先节点A的路径,在栈中A在D之下,所以回溯至D时low[D]<dfs[D]这和dfs[D]=low[D]矛盾,这样就证明了在回溯至C后和回溯至A前不可能有针对栈中C和A之间(不包括A和C)的节点的弹栈操作。这样回溯至C之后,回溯至A之前,C不可能被弹出,故回溯至A时C仍在栈中,这和回溯至A时C已被弹出的假设矛盾,这就证明了回溯到A时,之前入栈并已被弹出的任一节点C不可能属于A所在强连通分量。另外,回溯到A时从未被访问的节点也不可能属于A所在强连通分量,若不然就存在一条A到未被访问节点的路径,根据DFS搜索顺序,回溯到A时未被访问的节点实际上已经访问过了,矛盾。这样就证明了A所在的强连通分量中所有节点都位于栈内,下面可以断言栈中位于A之下的所有节点不可能属于A所在的强连通分量,证明很简单,若不然存在A之下的某节点E属于A所在的强连通分量,注意dfs[E]<dfs[A],这样E比A先访问,而E属于A所在的强连通分量,故A不可能是DFS中A所在的强连通分量被访问的第一个节点,矛盾。这样就证明了栈中A及A之上的所有节点构成了A所在的强连通分量的全部节点,证明完毕。
命题:2 在tarjan算法中回溯到一给定节点A时,若dfs[A](A在DFS中被访问的次序)==low[A](A的子树中的节点所能回溯到的栈中DFS值最小的节点的DFS值)则该节点必为通过DFS所到达的深度优先树中该节点所在的强连通分量中的第一个被访问的节点(根节点)
证明:使用反证法,若该节点不为深度优先树中该节点所在的强连通分量中被访问的第一个节点,则记深度优先树中该节点强连通分量中被访问的第一个节点为F,F不等于A。由命题3证明知F所在的强连通的分量(即A所在强连通分量)除F以外的所有节点一定都位于F的子树中,而A在A所在强连通分量中,A不等于F,所以A在F的子树中。在DFS过程中回溯到A时,由于F比A先访问,所以在栈中F位于A之下,dfs[F]<dfs[A],由于dfs[A]==low[A],所以回溯到A时会对A执行弹栈操作,此后A再也不会出现在栈中。当回溯到F时,由于F为深度优先树中节点F所在强连通分量((即A所在强连通分量))中被访问的第一个节点,所以由命题1,此时栈中F节点之上的所有节点(包括F节点)必为A节点所在强连通分量(即F所在强连通分量)的所有节点,但是位于A节点所在强连通分量中的节点A不在栈中,矛盾,这就证明了命题2
命题3:在tarjan算法中回溯到一给定节点A时,若A是它所在的强连通分量在深度优先树中被第一个访问的节点,则必有dfs[A]==low[A]
证明:首先,回溯至节点A时,由于A为它所在强连通分量的根节点,而A所在的强连通的分量除A以外的所有节点一定都位于A的子树中(假若有节点B(A不等于B)位于A所在的强连通分量,而节点B不在A的子树中,由于存在从A到B的路径,所以当访问A所在的强连通分量在DFS树中被访问的第一个节点A后,根据DFS的搜索规则,在回溯至A之前一定会访问B,而访问A后回溯至A前一直在访问A的子树,在DFS中访问A的子树时不可能跨越子树去访问不在子树中的节点B),所以A的子树中有一个属于A所在强连通分量的节点,存在该节点到A的一条路径,由于回溯至A时A在栈中,所以low[A]<=dfs[A].我们来证明回溯至A时必有dfs[A]==low[A],如若不然,回溯至A时有low[A]<dfs[A],此时栈中A之上的所有节点C均满足low[C]<dfs[C](否则low[C]==dfs[C],这样回溯至A前回溯至C时,C被出栈,回溯至A时节点C不在栈中,矛盾。还要注意的是回溯至一给定节点时该节点的low值已确定,以后不会再改变),且根据DFS访问顺序栈中A之上的所有节点C都已经被回溯过了(而且已经回溯过的节点不会再次被访问和回溯),回溯至A时low[A]<dfs[A],这样不会针对A执行弹栈操作,于是对A的回溯结束后,栈中A及A之上的所有节点都会保留。另外,栈中A之下必然存在节点D,使得当回溯至D时有low[D]==dfs[D],如若不然当回溯至栈底节点F(栈中DFS值最小节点)时,仍有low[F]<dfs[F],从而存在从F的子树中某节点到栈中dfs值比dfs[F]还小的节点的路径,而栈中dfs值比dfs[F]还小的节点根本不存在,矛盾。于是我们知道,对A的回溯结束后,必然会回溯到栈中A之下的某节点,该节点low值==dfs值,我们取最早回溯到的这样的节点E,当回溯到E时,回溯到A时栈中A及A之上的节点仍然位于栈中(这是因为对A的回溯结束后,栈中A及A之上节点仍在栈中,此后由于这些节点不会被再次回溯到,所以只有第一次对栈中A之下的节点执行弹栈操作时这些节点才会被弹出,在此之前,结束对A的回溯之后,这些节点都会被保留),而回溯到E时有low[E]==dfs[E],根据之前证明的命题:2,E为通过DFS所到达的深度优先树中E所在的强连通分量中的第一个被访问的节点(根节点),然后由命题1我们得出栈中E节点之上的所有节点(包括E节点)必为E节点所在强连通分量的所有节点,回溯到E时栈中A及A之上的节点仍然位于栈中,在栈中E位于A之下,这样栈中E节点之上所有节点包括了栈中A及A节点之上所有节点,于是A在E所在的强连通分量中,同时A又是A所在的强连通分量在DFS树中的根节点,于是我们把E所在强连通分量和A所在强连通分量合并得到强连通分支G,E所在强连通分量和A所在强连通分量都被包含于G中,这和它们是极大强连通子图即强连通分量矛盾,这样命题3证毕
PS:博主水平有限,如果以上证明存在疏漏或错误恳请指正,谢谢.Tarjan算法简单易实现,但是原理还是比较复杂的,不容易理解,在这里向图灵奖获得者Tarjan致敬
原文地址:https://www.cnblogs.com/WSKIT/p/9655951.html