概念
- 
强连通:有向图 G 中,任意两个节点互相可达。
 
- 
强连通分量 (Strongly Connect Componet,SCC):原图的一个极大强连通子图。也就是说,一个强连通分量是
- 强连通的:强连通分量内任意两个节点互相可达
 
- 极大的:不存在节点 u′,使得强连通分量加入 u′ 后仍然是一个强连通子图。
 
 
- 
缩点:如果我们把每个强连通分量中的所有点看成一个点,就构成了一个 SCC 图。这个图一定不会存在有向环,因此是一个 DAG。
 

如上图左,节点 1、4 单独构成一个 SCC,节点2、5,节点3、6,节点7、8、9、10、11、12 构成了三个 SCC。缩点后得到新图如上图右所示。可以观察到,确实是一个有向无环图。
Tarjan 算法
Tarjan 算法可以在 O(V+E) 的复杂度下,通过一次 DFS 找出一个图的所有强连通分量。另外,Tarjan 算法还可稍加修改,用于求解割点、桥、双连通分量问题。
在讨论 Tarjan 算法之前,我们先了解搜索树。在对有向图进行 DFS 时,会形成一颗搜索树。每次搜索找到一个还没有访问过的节点的时候经过的边,被称为树枝边。如果在搜索的过程中,某条边的终点是一个已经被搜索过的节点,则这条边不是树枝边。
为了求解强连通分量,Tarjan 算法维护了两个值:dfn 和 low。
- dfnu:在 DFS 过程中,第一次访问节点 u 的次序。
 
- lowu:在 u 的子树中,通过一条不在搜索树的边上能达到的节点的 dfn 最小值。
 
若在节点 u 搜索完成后有 dfnu=lowu,那么 u 的子树中所有没有被划分的节点都和 u 在同一个连通分量中。(因为若节点 v 在 u 的子树中,且没有被先划分走,那么这个节点一定可以回到 u)
按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfn 与 low 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 u 和与其相邻的结点 v 考虑 3 种情况:
- v 未被访问:继续对 v 进行深度搜索。在回溯过程中,用 lowv 更新 lowu。因为存在从 u 到 v 的直接路径,所以 v 能够回溯到的已经在栈中的结点,u 也一定能够回溯到。
 
- v 被访问过,已经在栈中:根据 low 值的定义,用 dfnv 更新 lowu。
 
- v 被访问过,已不在栈中:说明 v 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
 
写成伪代码形式:
\begin{algorithm}
\caption{Tarjan's Algorithm for SCC}
\begin{algorithmic}
\STATE $s$ $\leftarrow$ an empty stack
\STATE $\text{dfn\_clock}$ $\leftarrow$ 1
\FUNCTION{dfs}{node $n$}
    \STATE $\text{dfn}_n:=\text{low}_n:=\text{dfn\_clock}$
    \STATE $\text{dfn\_clock}:=\text{dfn\_clock}+1$
    \STATE push $n$ in stack $s$
    \FOR{each node $m$ with an edge from $n$ to $m$}
        \IF{m hasn't been visited}
            \STATE \CALL{dfs}{m}
            \STATE $\text{low}_n$ $:=$ $\min(\text{low}_n,\text{low}_m)$
            
        \ELSEIF{m in stack $s$}
            \STATE $\text{low}_n$ $:=$ $\min(\text{low}_n,\text{dfn}_m)$
        \ENDIF
    \ENDFOR
    \IF{$\text{low}_n$=$\text{dfn}_n$}
        \WHILE{u in stack $s$}
            \STATE pop $v$ from $s$
            \STATE add $v$ to SCC
        \ENDWHILE
    \ENDIF
\ENDFUNCTION
\end{algorithmic}
\end{algorithm}
应用
受欢迎的牛
洛谷P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
题意简述:给定一个有向图,求有几个节点,从其余节点均可到达。
求连通分量后,符合条件的节点一定在同一个连通分量中。我们把图进行强连通分量分解后,至多有一个强连通分量满足题目的条件。只需判断缩点后是否只有一个出度为 0 的节点,若只有一个,则答案即为该节点所代表强连通分量内节点个数,否则答案为 0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
   | #include<bits/stdc++.h> using namespace std;
  const int N = 1e4+5;
  stack<int> s; bool inst[N]; vector<int> G[N]; int dfn[N],low[N],dc=0;
  int scc_cnt = 0; int scc[N]; int scc_size[N];
  vector<int> G2[N]; 
  bool vis[N];
  void tarjan(int u){     dfn[u]=low[u]=++dc;     s.push(u);inst[u]=1;     for(int v:G[u]){         if(!dfn[v]){             tarjan(v);             low[u]=min(low[u],low[v]);         }         else if(inst[v]){             low[u]=min(low[u],dfn[v]);         }     }          if(low[u]==dfn[u]){         scc_cnt++;         while(s.size()){             int x= s.top();              scc[x]=scc_cnt;             scc_size[scc_cnt]++;             inst[x]=0;s.pop();             if(x==u)break;         }     }  }
 
 
  int main(){     int n,m;scanf("%d%d",&n,&m);           for(int i=0;i<m;i++){         int u,v;scanf("%d%d",&u,&v);         G[u].push_back(v);      }           for(int i=1;i<=n;i++){         if(!dfn[i])tarjan(i);     }               for(int u=1;u<=n;u++){         for(int v:G[u]){             int uu=scc[u],vv=scc[v];             if(uu==vv)continue;             G2[uu].push_back(vv);          }      }               int ans = -1;     for(int u=1;u<=scc_cnt;u++){         if(G2[u].size()==0){             if(ans != -1){                 puts("0");return 0;             }             ans = scc_size[u];         }     }          printf("%d\n",ans);     return 0; } 
 
 
  | 
 
校园网 Network of Schools
洛谷P2746 [USACO5.3]校园网Network of Schools
对于任务 1,只需统计缩点后入度为 0 的点的个数。(因为这些节点一定要有,其他都可以从这些节点走到)
对于任务 2,其实是问一张有向图最少添加多少条边成为强连通图。这个问题是这么处理的。缩点后,答案即为 max{入度为0的点的个数,出度为0的点的个数}。特殊的,若原图即为强连通图,答案为 0 而不为 1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
   | #include<bits/stdc++.h> using namespace std;
  const int N = 105;
  int n; vector<int> G[N];
  stack<int> s; int dfs_clock,dfn[N],low[N]; bool in_stack[N];
  int scc_cnt,scc_id[N];
  void tarjan(int u){     s.push(u);in_stack[u]=1;     dfn[u]=low[u]=++dfs_clock;     for(int v:G[u]){         if(!dfn[v]){             tarjan(v);             low[u]=min(low[u],low[v]);         }         else if(in_stack[v]){             low[u]=min(low[u],dfn[v]);         }     }
      if(low[u]==dfn[u]){         ++scc_cnt;         while(1){             int x=s.top();s.pop();             in_stack[x]=0;             scc_id[x]=scc_cnt;             if(x==u)break;         }     } }
  int in[N],out[N]; 
  int main(){     scanf("%d",&n);     for(int i=1;i<=n;i++){         int x;         while(1){             scanf("%d",&x);             if(!x)break;             G[i].push_back(x);         }     }
      for(int i=1;i<=n;i++){         if(!dfn[i])tarjan(i);     }
      for(int u=1;u<=n;u++){         for(int v:G[u]){             if(scc_id[u]!=scc_id[v]){                 out[scc_id[u]]++;                 in[scc_id[v]]++;             }         }     }          int in0=0,out0=0;     for(int i=1;i<=scc_cnt;i++){         if(in[i]==0)in0++;         if(out[i]==0)out0++;     }
      printf("%d\n",in0);
      if(scc_cnt==1)puts("0");     else printf("%d\n",max(in0,out0));
      return 0; }
 
  |