强连通分量和桥和割点——Tarjanの板子

来源:互联网 发布:ubuntu如何添加中文 编辑:程序博客网 时间:2024/06/10 12:45

强连通分量是啥?

有向图的连通分量是有向图的一个子图,对于子图中任意一个有序点对(u,v),总存在一条从u到v的路径。特殊地,一个点也算一个连通分量。而强连通分量就是一个极大连通分量。

桥和割点好像有点相似之处

无向图的桥边是指一条去掉它之后整个图会变得不连通的边,而无向图的割点就是一个去掉它之后整个图会变得不连通的点。。。好相似啊有没有= =。。有向图里面实际上也有桥和割点,概念和上面是一样的。然而ATP没有好好看有向图跟无向图有什么区别,所以下面提到的桥边和割点仅限于无向图啦。

一些简单的概念

要理解tarjan求这些玩意儿的过程,首先要介绍几个在DFS中的名词儿:

  1. dfs时间戳:也叫dfs序,存储的是这个节点在dfs的过程中被访问的顺序。
  2. 搜索树:在dfs中如果限制每个节点只能被访问一次,那么除了第一个访问的点以外,所有点会有且仅有一个父节点,并且每个点有可能会成为多个点的父节点,这形成了一个树结构,叫做这次dfs形成的搜索树。搜索树上的边,也就是每次递归的时候连接已访问节点和未访问节点的边,叫做树边。
  3. 前向边,后向边和横向边:前向边是指从搜索树中某个祖先连接到它的子孙的一条边,而后向边就是从搜索树中某个节点连接到它的祖先的一条边。横向边就是连接兄弟节点的边。显然这些边在dfs的时候会发现它是连接两个已访问节点的,所以不会被加入搜索树中。可以发现无向图是不会存在前向边的。
  4. low数组:这是tarjan里面很关键的一个数组,它存储的是对于每个点,它在搜索树中的子树里面所有节点能通过后向边访问到的最小的dfs序。它主要是用来判断某个点以及它子树里面的节点是否有唯一的一条路径让它跟前面联通。

Tarjan的三个长得很像的板子

(一)强连通分量:

求强连通分量就是要把图划成一块块的。首先我们可以发现,一个环上的节点是一个强连通分量;那么在dfs的过程中,如果v的子树中存在某个节点有指向v的真祖先的后向边,说明v这棵子树一定是跟它的那个祖先在同一个强连通分量里面的,因为那个祖先可以到v,v可以到它的子树中的节点,而v的子树中又存在节点可以回到那个祖先,这就成了一个环形。

于是我们可以发现如果v的dfs序和它的low数组相等,说明v的子树里不存在返回祖先的后向边,那么v和它的子树就可以被划成一块。但这一块里面可能还存在别的强连通分量,所以我们应该在dfs的时候维护一个栈,当dfs自底向上返回的时候,每次遇到一个dfs序和low相等的节点v就把栈里面在v上面的所有点都弹出来组成一个强连通分量,这样就可以保证每次出来的节点都是一个完整的强联通分量啦。

void dfs(int u){    w[u]=low[u]=++cnt;    ext[u]=true;st[++top]=u;    for (int i=p[u];i!=0;i=next[i])      if (w[a[i]]==0){          int v=a[i];          dfs(v);          low[u]=min(low[u],low[v]);      }else if (ext[a[i]]==true) low[u]=min(low[u],w[a[i]]);    if (w[u]==low[u]){        ++bcnt;        while (st[top+1]!=u){            ext[st[top]]=false;            ++size[bcnt];            belong[st[top--]]=bcnt;        }    }}

注意的问题是更新low数组的时候要分成使用的是树边还是后向边两种情况讨论,如果是树边的话就直接用它儿子的low来更新,否则应该用它祖先的dfs序来更新。并且判断它是当前节点u的祖先的条件就是它还在栈中,因为一个点的祖先肯定不可能比这个点先弹栈。

很多题目中都会用到“把强连通分量缩成一个点”这个东西,可以发现把强连通分量缩成一个点以后图变成了一个没有环的图。有向无环图?可以上拓扑序之类的乱搞啦。

(二)桥边

求桥边也是利用了low的记录回溯的信息。在无向图中,对于一个点v来说,考虑它的父亲边是不是桥边,就是要判断除了这条父亲边以外,v以及它在搜索树中的子树还有没有其它回到前面的路径。如果没有的话说明要从上面到v只有父亲边那一条路可以走,那么父亲边就是桥边。

于是在更新low的时候判断一下不要用父亲边更新low就可以啦。如果在这样的情况下节点v的low仍然小于它的dfs序说明它还是可以回去的,否则那条父亲边就是桥边啦。这样子有重边的时候也不需要特判十分方便呀。

bool same(int i,int j){    if ((i%2==0&&i-1==j)||(i%2==1&&i+1==j)) return true;    return false;}void Tarjan(int u){    w[u]=low[u]=++cnt;    st[++tp]=u;    for (int i=p[u];i!=0;i=nxt[i])      if (w[a[i]]==0){          fa[a[i]]=i;Tarjan(a[i]);          low[u]=min(low[u],low[a[i]]);      }else if (!same(fa[u],i)) low[u]=min(low[u],w[a[i]]);    if (low[u]==w[u]){        int v;++bcnt;        do{            v=st[tp--];pos[v]=bcnt;        }while (v!=u);    }}

ATP在这里判是否是父边的方法有点愚蠢。。主要是强迫症的ATP不想把边从0开始编号更不想把p数组的初值全搞成-1。。。(莫名其妙啊你)

如果把无向图中所有的桥边提出来。桥边连接的每一块节点两两之间至少有两条不相同的路径,这种块块叫做双连通分量。可以发现它们形成的图也是没有环的。无向无环图?这就是棵树啦。

(三)割点

割点和桥太像了以至于它们的板子看起来就像是一个东西。。原理也差不多啊就是更新low的时候不要用它连到父节点的边来更新就可以了。。这也不存在什么重边不重边的问题对吧。

void Tarjan(int u,int fa,int rt){    w[u]=low[u]=++cnt;    for (int i=p[u];i!=0;i=nxt[i])      if (w[a[i]]==0){          if (u==rt) ++ss;          Tarjan(a[i],u,rt);          low[u]=min(low[u],low[a[i]]);      }else low[u]=min(low[u],w[a[i]]);//直接更新low即可}

贴一坨相关题目吧

  1. BZOJ1051 受欢迎的牛
    强连通分量的板子题啦。ATP写的第一个强连通分量的题就是这个题啦!
  2. POJ1236 Network of Schools
    第一问是板子,第二问是说最少加入多少条边能让图强连通,好像要稍微想想啦。
  3. HDU1269 迷宫城堡
    还是个板子题。
  4. HDU3861 The King’s Problem
    除了强连通分量以外好像还加了点儿别的东西。
  5. BZOJ1797 Mincut最小割
    这题好像是什么“最小割唯一性判定”?反正也跟tarjan有关就是了。。虽然好像比上面的题难。。。。很多。。。。
  6. BZOJ2427 软件安装
    DP相关的啦。
  7. BZOJ3887 Grass Cownoisseur
    还要统计一下强连通分量的size
  8. BZOJ1179 Atm
    也是比较简单的tarjan题目啦。
  9. POJ3177 Redundant Paths
    tarjan求桥边的板子题啦
  10. POJ3694 Network
    好像除了桥边以外还加了某种鬼畜的东西进去
  11. POJ1144 Network
    和上面那个题是同名的诶。不过这个题是求割点。
  12. POJ1523 SPF
    要求每个割点把图分成了几个不同的联通块。。这个好像有点难搞

woc我咋搞出来这么一大坨题目= =

0 0