暑假集训随笔4 强连通分量与点双、边双连通分量

强连通分量

一个在有向图中的概念
\(强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。\)
\(强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图\)

tarjan算法的一些理解

注意到如果一些点属于一个强连通分量,那么从其中一个点一定可以“走到”所有的点,而对于dfs而言,“走到”的路径构成了dfs树,而那个depth最低的点就是dfs时这条路的起始点。但是如果想要形成一个闭环的话,光是走到是不够的,还需要一次重复的访问已经走到的点来构成这个闭环,而这次重复的访问对应的就是非树边。
然而有些点可能已经在之前的过程中参与构成了其他的强连通分量,这些点事实上应当被认为已经被删除,故可以用是否在栈中来判断它们是否被删除,因为作为一个之前访问过的点,除非被弹出否则它们必定在栈中。

int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc;  // 结点 i 所在 SCC 的编号
int sz[N];       // 强连通 i 的大小

void tarjan(int u) {
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
  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 (in_stack[v]) {
      low[u] = min(low[u], dfn[v]);
    }
  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) {
      scc[s[tp]] = sc;
      sz[sc]++;
      in_stack[s[tp]] = 0;
      --tp;
    }
    scc[s[tp]] = sc;
    sz[sc]++;
    in_stack[s[tp]] = 0;
    --tp;
  }
}

常见的用途

对于一些可能非DAG的图,可以考虑先用tarjan进行缩点再套\(topo\ sort\)进行求解。不过要注意强联通分量之间的重边需要被删除,可以考虑用map来处理

割点

在整颗搜索树上,如果u是根节点或者low[v]>=dfn[u]时这个点是割点。因此代码写起了与tarjan类似。但是有一个很关键的地方在于同一个u可能会多次被它的v判定为割点,因此要用一个数组去记录u有没有被记录为割点过,防止重复统计

点双连通分量

当一个图中没有割点时这个图就是点双

求解

对于割点u,将在u的子树中且在栈中的点认为是点双,逐次弹出,但是不弹出u,因为u可能属于多个点双

边双连通分量

注意到一个有趣(而且很显然)的事实:当指定了一个点不能访问其父亲(就是在dfs树上的father)时,我们事实上已经对这个无向图完成了定向,即转换成按照dfs树上的父子顺序由父亲指向儿子的有向图,而那些不在dfs树上的边则一定对应一条指向自己祖先的边,而非如同有向图一样还可能是指向一个访问过但是与当前节点没有祖先关系的点(即该边不可能是横向边)。
证明这一点很容易:由于我们的dfs树是伴随着dfs过程才被建立起来的,它事实上来自于我们原始的无向图,因此假设有一个点u有一条横向边指向v,那在dfs时v一定早就通过这条边的反向边或者间接访问的方式将u转化为了自己的子树中的某个节点,因此该边不不可能是"横向边"。
这个性质告诉我们一件很好的事情:求边联通分量的过程与有向图的强连通分量几乎相同,除了需要记录自己的上一个点/边,以及不必维护每个点是否在栈中的信息,因为每个被访问的点只有没有做tarjan或者在栈中两个可能这两件事。
另一个值得注意的事情是如果无向图有重边的话那么自己的father也是可能能被访问的,因此不能记录自己是从哪个点来的,而应该记录从哪条边来。
最后是洛谷模板题的代码

#include<bits/stdc++.h>
using namespace std;

const int maxn=600000+15;
int n,m,sum=1,tim,top;
int head[maxn],sd[maxn],dfn[maxn],low[maxn];
struct EDGE
{
    int to;int next;int from;
}edge[maxn*10];
void add(int x,int y)
{
    edge[++sum].next=head[x];
    edge[sum].from=x;
    edge[sum].to=y;
    head[x]=sum;
}
vector<int>ecc[maxn];
int cont_ecc;
void tarjan(int x,int pre)
{
	//cout<<x<<" "<<pre<<endl;
    low[x]=dfn[x]=++tim;
    stac[++top]=x;
    for (int i=head[x];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(i==(pre^1)){ //注意这里 异或运算的优先级低于比较运算
        //	cout<<i<<" "<<(pre^1);
        	continue;
		}
        
        if (!dfn[v]) {
        tarjan(v,i);
        low[x]=min(low[x],low[v]);
    }
        else 
        {
            low[x]=min(low[x],dfn[v]);
        }
    }
    if (dfn[x]==low[x])
    {
    	++cont_ecc;
    	
        int y;
        while (y=stac[top--])
        {
			ecc[cont_ecc].push_back(y);
            in[y]=0;
            if (x==y) break;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    sum=1;
    for (int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        add(u,v);
        add(v,u);
    }
    for (int i=1;i<=n;i++)
    if (!dfn[i]) tarjan(i,0);
	cout<<cont_ecc<<endl;
	for(int i=1;i<=cont_ecc;i++){
		cout<<ecc[i].size()<<" ";
		for(const auto &u:ecc[i]){
			cout<<u<<" ";
		}
		cout<<'\n';
	}

    return 0;
}
posted @ 2023-08-17 16:15  wxk123  阅读(12)  评论(0编辑  收藏  举报