图论连通性学习指南

发布时间 2023-10-19 21:35:30作者: White_Sheep

前置芝士

DFS生成树

img

有向图的 DFS 生成树主要有 4 种边(不一定全部出现):

  1. 树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
  2. 反祖边(back edge):示意图中以红色边表示(即7->1),也被叫做回边,即指向祖先结点的边。
  3. 横叉边(cross edge):示意图中以蓝色边表示(即9->7),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
  4. 前向边(forward edge):示意图中以绿色边表示(即3->6),它是在搜索的时候遇到子树中的结点的时候形成的。

我们考虑 DFS 生成树与强连通分量之间的关系。

如果结点 u是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以u为根的子树中。结点u被称为这个强连通分量的根。

反证法:假设有个结点 v在该强连通分量中但是不在以u为根的子树中,那么u到v的路径中肯定有一条离开子树的边。但是这样的边只可能是横叉边或者反祖边,然而这两条边都要求指向的结点已经被访问过了,这就和u是第一个访问的结点矛盾了。得证。

连通分量

在图论中,连通分量是指无向图中的一组顶点,其中任意两个顶点之间都存在路径。换句话说,连通分量是指图中的一个极大连通子图,即不能再添加任何其他顶点使得该子图仍然是连通的。

强连通分量

在有向图G中,如果两个顶点u,v间有一条从u到v的有向路径,同时还有一条从v到u的有向路径,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量。

极大的强连通子图

双连通分量

边双连通分量(eDcc)

边双连通分量即一个无向图中,去掉一条边后仍互相连通的极大子图。(单独的一个点也可能是一个边双连通分量)换言之,一个边双连通分量中不包含桥。

[性质]

在一个边双连通分量中,任意两点都存在至少两条互相分离的路径。

点双连通分量(vDcc)

极大的不包含割点的连通块。

求点数大于1的强连通分量

[problem description]

有一个 \(n\) 个点,\(m\) 条边的有向图,请求出这个图点数大于 \(1\) 的强连通分量个数。

[input]

第一行为两个整数 \(n\)\(m\)

第二行至 \(m+1\) 行,每一行有两个整数 \(a\)\(b\),表示有一条从 \(a\)\(b\) 的有向边。

[output]

仅一行,表示点数大于 \(1\) 的强连通分量个数。

\(2\le m\le 5\times 10^4\)\(1 \leq a, b \leq n\)

[solved]

Tarjan 算法求强连通分量

在 Tarjan 算法中为每个结点 维护了以下几个变量:

1.\(dfn_u\)深度优先搜索遍历时结点u被搜索的次序。

2.在\(low_u\)的子树中能够回溯到的最早的已经在栈中的结点。设以u为根的子树为SubTree。\(low_u\)定义为以下结点的dfn的最小值:SubTree中的结点;从\(SubTree_u\)通过一条不在搜索树上的边能到达的结点。

一个结点的子树内结点的 dfn 都大于该结点的 dfn。

从根开始的一条路径上的 dfn 严格递增,low 严格非降。

按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfnlow 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点u和与其相邻的结点v(v不是u的父节点)考虑 3 种情况:

  1. v未被访问:继续对v进行深度搜索。在回溯过程中,用\(low_v\)更新\(low_u\)。因为存在从u到v的直接路径,所以v能够回溯到的已经在栈中的结点,u也一定能够回溯到。
  2. v被访问过,已经在栈中:根据 low 值的定义,用\(dfn_v\)更新\(low_u\)
  3. v被访问过,已不在栈中:说明v已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
/*
代码模板
*/
const int N=10010;
int n,m,a,b;
vector<int> e[N]; 
int dfn[N],low[N],idx;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;

void tarjan(int x){
  //入x时,盖戳、入栈
  dfn[x]=low[x]=++idx;
  stk[++top]=x,instk[x]=1;
  for(int y : e[x]){
    if(!dfn[y]){//若y尚未访问
      tarjan(y);
      low[x]=min(low[x],low[y]);//回x时更新low
    }
    //如果已经访问过,不在栈中,说明已经组成了一个强连通分量,我们需要就将它剔除,不用更新,如果更新会导致一些节点不法出栈
    else if(instk[y])//若y已访问且在栈中
      low[x]=min(low[x],dfn[y]);//在x时更新low
  }
  //离x时,收集SCC
  if(dfn[x]==low[x]){//若x是SCC的根
    int y; ++cnt;
    do{
      y=stk[top--];
      instk[y]=0;
      scc[y]=cnt;//SCC编号
      ++siz[cnt];//SCC大小
    }while(y!=x);
  }
}
void solve(){
	 cin>>n>>m;
  while(m--)
    cin>>a>>b, e[a].push_back(b);
  for(int i=1; i<=n; i++)//可能不连通
    if(!dfn[i]) tarjan(i);
   int res=0;
   for(int i=1;i<=cnt;i++)
     if(siz[i]>1) res++;
   cout<<res<<endl;
}

边双连通分量

[problem description]

对于一个 \(n\) 个节点 \(m\) 条无向边的图,请输出其边双连通分量的个数,并且输出每个边双连通分量。

[input]

第一行,两个整数 \(n\)\(m\)

接下来 \(m\) 行,每行两个整数 \(u, v\),表示一条无向边。

[output]

第一行一个整数 \(x\) 表示边双连通分量的个数。

接下来的 \(x\) 行,每行第一个数 \(a\) 表示该分量结点个数,然后 \(a\) 个数,描述一个边双连通分量。

你可以以任意顺序输出边双连通分量与边双连通分量内的结点。

[样例输入]

5 3
1 2
2 3
1 3

[样例输出]

3
3 1 3 2
1 4
1 5

\(1 \le n \le 5 \times10 ^5\)\(1 \le m \le 2 \times 10^6\)

[solved]