割点&割边 点双&边双

· · 个人记录

基础概念

定义

割点:在无向连通图中,若去掉某点(即去掉所有与之相连的边)后,该图不连通,则称这个点是割点。

割边:在无向连通图中,若去掉某边后,该图不连通,则称这条边是割边,也称桥。

点双连通图:不存在割点的图,两点一线除外。

边双连通图:不存在割边的图,两点一线除外。

点双:极大的点双连通图。

边双:极大的边双连通图。

性质

1.点双(除两点一线型)中任意两点间至少存在两条点不同的路径,即任意两点被包含在至少 1 个简单环中。

证明:若两点只存在一条点不同的路径,则路径上度大于 1 的点就是割点,与定义相违背。

2.割点连接着至少 2 个点双,非割点的点至多存在于 1 个点双中。这是显而易见的。

3.割边(除两点一线型)中任意两点间至少存在两条边不相同的路径,即任意一边都被包含于至少 1 个简单环中。

证明:同 1 的证法。否则点双会出现割边。

4.点双连通不具传递性(因为点会重合),边双连通具有传递性。

割点与割边的求法

这里要请出 Tarjan 大法了,在这之前,补充一些概念:

1.dfs 树:即 dfs 遍历得到的树,注意是有向的。

2.树边:dfs 树上由祖先节点指向儿子节点的边。

3.非树边:dfs 树上由儿子节点指向祖先节点的边。

割点

定义数组:

1.dfn[]:记录节点的 dfs 序。

2.low[]:记录节点经过至多 1 条非树边和若干条树边后所到点的 dfn 最小值。

类似求解强连通分量,维护上述数组。

关键是判定:令 v 为节点 u 的子节点,若 low[v]≥dfn[u],则 u 为割点。

如何理解?回顾定义,显然 dfn[v]>low[u];如果 v 可以通过绕远路的方式到达 u,则 low[v]≤dfn[u]

若满足判定条件,则说明 v 无法绕远路到达除 u 外已遍历的点,放在原图这代表 v 必须经过 u 才能到达这些点。于是去掉 u 后,原图不再连通,u 便是割点。

代码如下。

注意根节点的特判(若树上根节点的出边大于 1,则根节点为割点)。

void Tarjan(int k)
{
    dfn[k]=low[k]=++df;
    int ch=0;
    for(int i=0; i<e[k].size();i++)
    {       
        int v=e[k][i];
        if(dfn[v]==0)
        {   
            Tarjan(v);
            low[k]=min(low[k],low[v]);
            if(low[v]>=dfn[k]&&k!=root) //根节点特判
            {
                pt[k]=1; //pt数组标记割点
            }
            ch++;
        }
        else
        {
            low[k]=min(low[k],dfn[v]);
        }
    }
    if(k==root&&ch>1) //当根节点是割点时,防止少算根节点
    {
        pt[k]=1;
    }
}

割边

除判定外与割点求法一致。若 low[v]>dfn[u],则边 (u,v) 为割边。

为啥没有等于呢?关键是边双定义为 任意两点间至少存在两条边不同的路径,一点可连接多条边,看图有答案:

如上图,1、3 点之间的两条路径点相同,边不同。

点双

只需遍历时把所有点压入栈,遇到割点就不断弹出直到遇见割点,正确性的证明类比强连通分量。

需要注意的是,割点可能被多个点双包含,因此不能弹出但要记录。推荐写法:不断弹出到点 v,最后记录 u 即可。

代码如下。

void Tarjan(int k)
{
    dfn[k]=low[k]=++df;
    st.push(k);
    int cnt=0;
    for(int i=0; i<e[k].size();i++)
    {
        int v=e[k][i];
        if(dfn[v]==0)
        {
            cnt++;
            Tarjan(v);
            low[k]=min(low[k],low[v]);
            if(low[v]>=dfn[k])
            {
                ans++;
                int p;
                do
                {
                    p=st.top();
                    st.pop();
                    answer[ans].pb(p); //answer数组记录点双
                }while(p^v);
                answer[ans].pb(k);
            }
        }
        else
        {
            low[k]=min(low[k],dfn[v]);
        }
    }
    if(cnt==0&&k==root) //判断孤立点
    {
        ans++;
        answer[ans].pb(k);
        return ;
    }
}

边双

可以求出每条割边,对其标记后再做 dfs 染色即可,但是比较复杂。

仔细想想,Tarjan 已经把无向图化为有向图,边不再具有不确定性。而边双可看作是环,环也是强连通分量,考虑仿照强连通分量的做法对图进行缩点。

区别在于有向图与无向图,无需判断子节点是否已在强连通分量中,vis 就没有存在的意义了。

解释上句:无向图的 dfs 树中不存在横叉边,所以不可能出现 v 在其他边双的情况。

代码如下:

void Tarjan(int fa,int k)
{
    dfn[k]=low[k]=++df;
    st.push(k);
    int fl=0;
    for(int i=0; i<e[k].size();i++)
    {
        int v=e[k][i];
        if(fa==v&&fl==0) //判定重边
        {
            fl=1;
            continue;
        }
        if(!dfn[v]) 
        {
            Tarjan(k,v);
        }
        low[k]=min(low[k],low[v]);
    }
    if(low[k]==dfn[k])
    {
        cnt++;
        int p;
        do
        {
            p=st.top();
            st.pop();
            ans[cnt]++; //记录大小
            ans2[cnt].pb(p); //记录每个点
        }while(p^k);
    }
}

具体应用

题目多与点双、边双有关。注意它们的区别,对题目进行点双缩点或边双缩点(模板)。当然,最重要的是结合其它知识。