OSDN Git Service

完善Tarjan算法内容,添加图片
authorsshwy <jy.cat@qq.com>
Sun, 14 Jul 2019 09:16:18 +0000 (17:16 +0800)
committersshwy <jy.cat@qq.com>
Sun, 14 Jul 2019 09:31:23 +0000 (17:31 +0800)
docs/graph/images/scc1.png [new file with mode: 0644]
docs/graph/scc.md

diff --git a/docs/graph/images/scc1.png b/docs/graph/images/scc1.png
new file mode 100644 (file)
index 0000000..c472ce0
Binary files /dev/null and b/docs/graph/images/scc1.png differ
index 8b35b25..6184e6c 100644 (file)
 
 Robert E. Tarjan (1948~) 美国人。
 
-Tarjan 发明了很多很有用的东西,下到 NOIP 上到 CTSC 难度的都有。
+Tarjan 发明了很多很有用的东西,下到 NOIP 上到 CTSC 难度的都有。光Tarjan算法就有很多,比如求各种联通分量的Tarjan算法,求LCA(Lowest Common Ancestor,最近公共祖先)的Tarjan算法。并查集、Splay也是Tarjan发明的。
 
-【举例子:Tarjan 算法,并查集,Splay 树,Tarjan 离线求 lca(Lowest Common Ancestor,最近公共祖先)等等】
+我们这里要介绍的是在有向图中求强连通分量的 Tarjan 算法。
 
-我们这里要介绍的是图论中的 Tarjan 算法,用来处理各种连通性相关的问题
+另外,Tarjan的名字`j`不发音,中文译为塔扬
 
-### 定义
+### DFS 生成树
 
-方便起见,我们先定义一些东西。
+在介绍该算法之前,先来了解**DFS 生成树**,我们以下面的有向图为例:
 
- `dfn[x]` :结点 x 第一次被访问的时间戳 (dfs number)
+![scc1.png](./images/scc1.png)
 
- `low[x]` :结点 x 所能访问到的点的 dfn 值的最小值
+有向图的DFS生成树主要有 4 种边(不一定全部出现):
+1. 树边(tree edge):绿色边,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
+2. 反祖边(back edge):黄色边,也被叫做回边,即指向祖先结点的边。
+3. 横叉边(cross edge):红色边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点**并不是**当前结点的祖先时形成的。
+4. 前向边(forward edge):蓝色边,它是在搜索的时候遇到子树中的结点的时候形成的。
 
-这里的树指的是 DFS 树
+一个结点的子树内结点的 DFN 都大于该结点的 DFN。
 
-所有结点按 dfn 排序即可得 dfs 序列
+从根开始的一条路径上的 DFN 严格递增。
 
-### DFS 树的性质
+我们考虑 DFS 生成树与强连通分量之间的关系。
 
-一个结点的子树内结点的 dfn 都大于该结点的 dfn
+如果结点 u 是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以 u 为根的子树中。u 被称为这个强连通分量的根
 
-从根开始的一条路径上的 dfn 严格递增
+反证法:假设有个结点 v 在该强连通分量中但是不在以 u 为根的子树中,那么 u 到 v 的路径中肯定有一条离开子树的边。但是这样的边只可能是横叉边或者反祖边,然而这两条边都要求指向的结点已经被访问过了,这就和 u 是第一个访问的结点矛盾了。得证
 
-一棵 DFS 树被构造出来后,考虑图中的非树边。
+### Tarjan 算法求强连通分量
 
-前向边 (forward edge):祖先→儿子
+在 Tarjan 算法中为每个结点 u 维护了以下几个变量:
+1. $DFN[u]$:深度优先搜索遍历时结点 u 被搜索的次序。
+2. $low[u]$:设以 u 为根的子树为 $Subtree(u)$。$low[u]$ 定义为以下结点的 $DFN$ 的最小值:$Subtree(u)$ 中的结点;从 $Subtree(u)$ 通过一条不在搜索树上的边能到达的结点。
 
-后向边 (backward edge):儿子→祖先
+显然,按照 DFS 搜索树的递归顺序,$low[u]$ 是单调递增的。
 
¨ªå\8f\89è¾¹ (cross edge)ï¼\9a没æ\9c\89ç¥\96å\85\88â\80\94å\84¿å­\90å\85³ç³»ç\9a\84
\8c\89ç\85§æ·±åº¦ä¼\98å\85\88æ\90\9cç´¢ç®\97æ³\95æ\90\9cç´¢ç\9a\84次åº\8f对å\9b¾ä¸­æ\89\80æ\9c\89ç\9a\84ç»\93ç\82¹è¿\9bè¡\8cæ\90\9cç´¢ã\80\82å\9c¨æ\90\9cç´¢è¿\87ç¨\8b中ï¼\8c对äº\8eç»\93ç\82¹ $u$ å\92\8cä¸\8eå\85¶ç\9b¸é\82»ç\9a\84ç»\93ç\82¹ $v$ è\80\83è\99\91 3 ç§\8dæ\83\85å\86µï¼\9a
 
-注意:横叉边只会往 dfn 减小的方向连接
+1.  $v$ 未被访问:继续对 $v$ 进行深度搜索。在回溯过程中,用 $low[v]$ 更新 $low[u]$。因为存在从 $u$ 到 $v$ 的直接路径,所以 $v$ 能够回溯到的已经在栈中的结点,$u$ 也一定能够回溯到。
+2.  $v$ 被访问过,已经在栈中:即已经被访问过,根据 $low$ 值的定义(能够回溯到的最早的已经在栈中的结点),则用 $DFN[v]$ 更新 $low[u]$.
+3.  $v$ 被访问过,已不在在栈中:说明 $v$ 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
 
-注意:在无向图中,没有横叉边(为什么?)
+将上述算法写成伪代码:
+
+```
+TARJAN_SEARCH(int u)
+    vis[u]=true
+    low[u]=dfn[u]=++dfncnt
+    push u to the stack
+    for each (u,v) then do
+        if v hasn't been search then
+            TARJAN_SEARCH(v) // 搜索
+            low[u]=min(low[u],low[v])// 回溯
+        else if v has been in the stack then
+            low[u]=min(low[u],dfn[v])
+```
+
+对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 $DFN[u]=low[u]$。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 DFN 值和 $low$ 值最小,不会被该连通分量中的其他结点所影响。
+
+因此,在回溯的过程中,判定 $DFN[u]=low[u]$ 的条件是否成立,如果成立,则栈中从 $u$ 后面的结点构成一个 SCC。
 
 ### 实现
 
 ```cpp
-dfs(x) {
-  dfn[x] = low[x] = ++index;
-  S.push(x);
-  instack[x] = true;
-    for
-      each edge(x, y) {
-        if (!dfn[y]) {
-          dfs(y);
-          low[x] = min(low[x], low[y]);
-        } else if (instack[y]) {
-          low[x] = min(low[x], dfn[y]);
-        }
-      }
-    if (dfn[x] == low[x]) {
-      while (1) {
-        t = S.pop();
-        instack[t] = false;
-        if (t == x) break;
-      }
-    }
+int dfn[N],low[N],dfncnt,s[N],tp;
+int scc[N],sc;// 结点 i 所在 scc 的编号
+int sz[N];// 强连通 i 的大小
+void tarjan(int u){
+    low[u]=dfn[u]=++dfncnt, s[++tp]=u;
+       for(int i=h[u];i;i=e[i].nex){
+        const int &v=e[i].t;
+               if(!dfn[v])tarjan(v), low[u]=min(low[u],low[v]);
+               else if(!scc[v]) low[u]=min(low[u],dfn[v]);
+       }
+       if(dfn[u]==low[u]){
+               ++sc;
+               while(s[tp]!=u)scc[s[tp]]=sc,sz[sc]++,--tp;
+               scc[s[tp]]=sc,sz[sc]++,--tp;
+       }
 }
 ```
 
-(转自维基:<https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm>)
-
-时间复杂度 $O(n + m)$ 
+时间复杂度 $O(n + m)$ 。
 
 ## Kosaraju 算法