Tarjan的强连通分量算法

  Tarjan算法用于寻找图G(V,E)中的所有强连通分量,其时间复杂度为O(|V|+|E|)。

  所谓强连通分量就是V的某个极大子集,其中任意两个结点u,v在图中都存在一条从u到v的路径。

  Tarjan的算法的流程是通过深度优先搜索遍历每个顶点,并且维护以下属性dfn,low,instk,p其中dfn表示该顶点第一次被访问时的次序,instk需要与一个栈stk配合使用,stk用于记录从某个顶点出发,尚未被包含进强连通分量的所有顶点,而instk用于记录一个顶点是否还存在于stk中,low表示从该结点出发可以访问到的所有在栈中的顶点中dfn属性最小的顶点的dfn值,p表示顶点所处强连通分量的代表顶点。

  算法的流程如下:

 1 tarjan(u, stk)
 2   if(u.dfn != 0)
 3     return
 4   u.dfn = order()
 5   u.low = u.dfn
 6   u.instk = true  
 7   stk.push(u)
 8   for (u, v) in E
 9     targin(v)
10     if(v.instk)
11       u.low = min(u.low, v.low)
12   if(u.dfn == u.low)
13     while(true)
14       top = stk.pop()
15       top.instk = false
16       top.p = u
17       if(top == u)
18         break

  其中order()表示分配下一个次序号,要求order()方法的返回值随调用次数增加而递增,且不能少于1,可以通过维护一个计数器实现。我们需要对每个V中的顶点调用上述Tarjan流程即可保证强连通分量的正确分离。

  说明时间复杂度,由于每个结点被访问都会设置dfn值,因此一个结点最多只会被访问一次,其4~7行总执行次数不可能超过|V|。而8~11行中每次都会使用一条完全不同的边,其总执行次数不可能超过|E|。12~18行每次循环都会令一个顶点弹出stk,由于只有4~7行会向栈中压入一个顶点,因此总执行次数不会超过|V|。因此总的时间复杂度为O(|V|+|E|)。

  再说明算法正确性。从两个角度说明:1.任意两个连通顶点u,v都会拥有相同的p属性值,即u.p=v.p。2.任意两个不连通顶点都会拥有不同的p属性值。

  命题1:对于栈中的元素x,y,若x.dfn<y.dfn,则x必定在y之后出栈。因为dfn属性与入栈的顺序是一致的。

  命题2:若顶点x被加入栈中,则栈中所有现存顶点到x都有一条路径。假设当栈中所有顶点满足命题时,我们通过栈中的某个顶点y,将其后置顶点x加入到栈中,由于假设可知栈中y及y之下所有的顶点都能访问到x。对于y之上的第一个顶点z,若z不为x,则由于z在回溯到y时,没有从栈中弹出,故z.dfn>z.low,即z能访问到z之下的某个顶点,故z能访问到x。因此由归纳法可知命题成立。

  命题3:当我们确定了栈中某个顶点u的low值时,在栈中u之上所有的顶点和u必定处在同一个强连通分量中。假设当栈中所有顶点满足这一性质时,我们压入顶点u,并利用深度优先搜索算法遍历u的后置顶点。当我们确定了u的low值时,若在栈中u之上还存在顶点v,不妨设v为u之上的第一个顶点,显然v.dfn>u.dfn,即v的回溯应该发生在u回溯之前,而v没有被出栈,意味着v.low<v.dfn,即v能访问到栈中某个v之下的顶点z,v和u是连通的。依旧是使用了归纳法。

  对于1,不妨设u.dfn<v.dfn,由于v能访问到u,故v.low<=u.dfn,而由命题2知道,所有栈中v之下的顶点x都满足x.low<=v.low<=u.dfn,即v出栈时必定会导致u的出栈,故v.p=u.p。

  对于2,当u和v被设置相同的p值时,意味着二者同时出栈。而由命题3可知u和v必定是连通的。

  因此当我们对V中每个顶点调用Tarjan流程时,将会保证强连通分量的正确分离。

  

posted @ 2017-11-28 23:27  cccwiseee  阅读(429)  评论(0编辑  收藏  举报