割点和桥 | 无向图的双连通分量

割点和桥

桥(割边):
给定一无向连通图,对于其中一边 (u,v),若从图中删掉 (u,v)后,原图分裂成2个或以上不相连的子图(也就是图中的连通分量数增加),则称 (u,v)为原图的割边(或桥)。

割点:
给定一无向连通图,对于其中一点 u,若从图中删掉 u 和所有与 u 相连的边后,原图分裂成成 2个或以上不相连的子图(也就是图中的连通分量数增加),则称u 为原图的割点(或割顶)。

边的双连通分量 e-dcc
极大的不包含桥的连通块(每个节点之间至少用两条不含公共边的路径)
点的双连通分量 v-dcc
极大的不包含割点的连通块

tarjan 判断桥 边双连通分量

原文1
原文2

算法理解

和强连通分量一样,引入两个变量:
dfn[u] : 当前到达节点u的时间戳(dfs序)
low[u] : x能达到的时间戳最小的点

dfn很好理解,再理解下low吧。
image

比如说下面的图:圈内的就是dfn[u], 圈外的[1]和[6]就是low[u]。
image

image

如何求图上的桥?

通过观察可知
x和y之间是桥 <====> dnt[x]<low[y]表示y无论如何往上走不到x

虽然找不到但是可以感性理解和证明正确性

根据定义,dnt[x]<low[y]说明从subtree(y)出发,在不经过(x,y)的前提下,无论走哪条边都无法到达x或比x更早访问的节点。

若把(x,y)删除,则subtree(y)与节点x就没有边相连,图就断开成立两部分。

因此tarjan判断桥的模板代码就是:

//防止搜反向边 引入from
void tarjan(int u, int from)
{
//更新时间戳
    dfn[u] = low[u] = ++ timestamp;
//遍历图
    for (int i = h[u]; i!=-1; i = ne[i])
    {
        int j = e[i];
//如果j未遍历过,则遍历且更新low[u]
        if (!dfn[j])
        {
            tarjan(j, i);//dfs(j)
            low[u] = min(low[u], low[j]);//用low[j]更新

//j到不了u,则x-y的边为桥,
            if (dfn[u] < low[j])
                //正向边is_bridge[i] 反向边is_bridge[i ^ 1]都是桥
                is_bridge[i] = is_bridge[i ^ 1] = true;
                // 这里i==idx 如果idx==奇数 则反向边=idx-1 = idx^1
                //            如果idx==偶数 则反向边=idx+1 = idx^1
        }
// 如果j遍历过 且i不是反向边(即i不是指向u的父节点的边),则直接更新low[u]
        else if (i != (from ^ 1))
            low[u] = min(low[u], dfn[j]);//用dfn[j]更新
    }
}

模板题

395. 冗余路径
题意:
新建道路 使得每两个草场之间都至少有两条分离的路径。

思路:
因为 一个边的双连通分量 <=> 任何两个点之间至少存在两个不相交路径。
所以题意就是求解加多少边才能将整个图变成双连通分量。

很明显如果本来就是双连通分量,则不需要加边,需要加边的地方就是桥。

对双连通分量做缩点 此时图上只剩桥和点
         o
        / \
       o   o
      /\   /\
     o  o o  o
    / \  
   o   o
可以发现对左右两个叶子节点连通后,根节点连向左右叶子节点的边就可以删去了
         o
        / \
       o   o
      /\   /\
     o  o-o  o
    / \   |  |
   o   o__|  |
   |_________|
   同理 再把第2个和第4个叶子节点连通后,根节点连向第2个和第4个叶子节点的边也可以删去
   第3个叶子节点随便连

给叶子节点按对称性加上边后就没有桥 <=> 变成边的双连通分量

根据上面的分析,也易得,需要加[(cnt+1)/2]条边,cnt为缩完点后度数==1的点(叶子节点)的个数

上面分析有两个问题未解决,

1. 证明:一个边的双连通分量任何两个点之间至少存在两个不相交路径
充分性: 对于每两个点都有互相分离的路径的话,则必然为强连通分量
反证 假设有桥(非双连通) x,y必然经过中间的桥 则只x→y的路径必在桥上相交
 o-x 桥y-o
 | | ↓ | |
 o-o - o-o
必要性:图是一个边双连通分量 等价于 不包含桥
        则一定对任意两点x,y
        x,y之间至少存在两条互相分离(不相交)的路径
反证:假设存在两条相交路径
     那么x→y中间必然有桥
//  o-o-o-o-o 蓝色路径 (从x出发到y经过边数最少的路径)
//   - - - -  绿色路径
//  x       y

2.如何缩点:
为了方便:将双连通分量缩点为该双联通分量的最高点

判断当前节点是双联通分量的最高点
对某一个点x,他走完所有的子节点之后,他的dnt[x]==low[x],那么就说明x最高遍历得到的节点就是他自己,也就是最高点了。

代码:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 5010, M = 20010;

int n, m;
int h[N], to[M], pre[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;//模拟栈,用于求出最高点
int id[N], dcc_cnt;// id记录节点i所在的双连通分量的编号;dcc_cnt为双连通分量的编号
bool is_bridge[M]; //是不是桥
int d[N];          //度数
void add(int a,int b){
    to[idx]=b,pre[idx]=h[a],h[a]=idx++;
}
void tarjan(int u,int fa){
    dfn[u]=low[u]=++timestamp;
    stk[++top]=u;//将遍历的元素都入栈
    for(int i=h[u];i!=-1;i=pre[i]){
        int j=to[i];
        if(!dfn[j]){
            tarjan(j,i);
            low[u]=min(low[u],low[j]);
            if(dfn[u]<low[j]){
                is_bridge[i]=is_bridge[i^1]=true;
            }
        }
        else if(i!=(fa^1)){
            low[u]=min(low[u],dfn[j]);
        }
    }
//如果u是最高点,则将u上面的节点出栈,并且记录他们位于哪个双连通分量
    if(dfn[u]==low[u]){
        ++dcc_cnt;//双连通分量数量加一
        int y;
        do{
            y=stk[top--];
            id[y]=dcc_cnt;
        }
        while(y!=u);
    }
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);//双向边
    }

    tarjan(1, -1);

    for (int i = 0; i < idx; i++)
     //如果边i是桥 在其所连的出边的点j所在强连通分量的度+1
     // 桥两边的双连通分量各+1
        if (is_bridge[i])
            d[id[to[i]]]++;

    int cnt = 0;
    for (int i = 1; i <= dcc_cnt; i++)
        if (d[i] == 1)//多少个度数为1的节点(强连通分量)
            cnt++;//需要加的边的数量
    cout << (cnt + 1) / 2 << endl;

    return 0;
}

tarjan 判断割点

原文1
原文2

如何求图上的割点

  • \(x\)不是搜索树的根节点(dfs的起点),
    则x是割点当且仅当搜索树上存在 x 的一个子节点y,满足dfn[x] ≤ low[y]
  • \(x\)搜索树的根节点,
    则x是割点当且仅当搜索树上存在至少两个子节点 \(y_1 和 y_2\) 满足上述条件。

下面看图理解下:

如果x不是根节点
         o
         |
         x
        / \
       y   y2
如果删除x,则y及其子节点一定不和x的父节点o相连。

如果x是根节点
   如果只有一个子结点y1
         x
         |
         y1
如果删除x,则y1 子节点部分还是连通的,此时x不是割点

   如果有两个子结点
         x
        / \
       y1  y2
如果删除x,则y1  y2 之间不连通,此时x是割点

模板题

电力
acwing:1183

牛客
题意:
求无向图删除一个节点之后最多还剩下多少连通块

思路:
先统计连通块的个数,

  1. 如果不删除节点,那答案就是连通块的个数
  2. 如果删除节点不是割点,那答案也是连通块的个数。
    因为如果不是割点,图中的连通分量数一定不增加。(割点定义)
  3. 如果删除节点是割点,那答案就是删去割点后连通块的个数。

代码:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 10010, M = 30010;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;  //dfn兼判重数组
int root;  // 记录每个连通块的"根节点"
int ans;  // 记录每个连通块去掉一个点形成的连通块数目的最大值

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void tarjan(int u) {

    dfn[u] = low[u] = ++ timestamp;

    int s = 0;  // 如果当前点u是割点的话,去掉该点u得到的连通分量的个数
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if (dfn[u] <= low[j])  // 说明u是可能是割点, u存在一棵子树(删除割点u)
                s++;
        } else low[u] = min(low[u], dfn[j]);
    }

    //如果不是根节点
    /*
             /
            u    删掉u后 除子节点yi外
           / \           还要要加上父节点部分+1
          o   o
    */
    //最后还要加上父节点部分1
    if (u != root) s++;  // 不用加上&& s的判断,因为u不是割点的话,s要取1

    ans = max(ans, s);
}

int main() {

    while (scanf("%d%d", &n, &m), n || m) {

        memset(dfn, 0, sizeof dfn);  // dfn还具有判重数组的作用
        memset(h, -1, sizeof h);
        idx = timestamp = 0;

        while (m--) {
            int a, b;
            scanf("%d%d", &a, &b);
            add(a, b), add(b, a);
        }

        ans = 0;   //记录删除不同割点之后形成的连通块的最大值
        int cnt = 0;  // 记录连通块的数目
        //每次将其中联通块遍历,用tarjan
        for (root = 0; root < n; root++)  // 节点编号从0~n-1
            if (!dfn[root]) { //dfn数组兼判重数组,求联通块的数量
                cnt++;
                tarjan(root);
            }

        printf("%d\n", cnt + ans - 1);
    }

    return 0;
}

tarjan 求点双连通分量

算法理解

什么是点双连通分量?
首先要明确点双连通分量和“删除割点后图中剩余的连通块”不同

比如下图:有四个点双连通分量
image
很明显,对于节点\(1,6\)两个节点对于其他连通块都是割点,但是对于\(1,6\)因为两者都不是割点,因此是一个双连通分量。

如何求点双连通分量?
在tarjan求割点过程中,当节点满足 dfn[u] <= low[j]时,则说明,对于栈中的u节点的子树,如果u连接一个其他节点,u就是割点,因此此时就是一个极大的不包含割点的图。
此时栈中的u节点的子树、加上u节点就是一个点双连通分量。

如何缩点?
因为一个割点可能属于多个v-DCC,缩点方式和之前不同。
设图 中共有 p 个割点和 t 个 v-DCC,我们建立一张包含 p+t个节点的新图,把每个 vDCC和每个割点都作为新图中的节点,并在每个割点与包含它的所有v-DCC之间连边。
(容易发现,这张新图其实是一棵树(或森林))
如下图所示:

image

模板题

原文
J - 矿场搭建 洛谷 - P3225

题意:
给一个不一定连通的无向图
问最少在几个点设置出口
使得任意一个出口坏掉后,其他所有点都可以和某个出口连通

分析:
首先考虑****

  1. 出口数量>=2
    因为:如果只有一个出口 那这个出口坏了就没有可以用的出口了

  2. 最终方案数 = 各连通块方案数乘积
    因为:不同连通块之间相互独立。

对一个连通块

  1. 无割点 <=> 度数==0的点<=> 孤立的点 <=> 不管我删掉哪个点 图剩余部分都是连通的
    因此我们只需要设置2个出口就可以满足。

  2. 有割点 ,先缩点,建新图,看V-DCC度数(割点的数量)

    • 如果V-DCC==1 意味着它只包含一个割点,必须在V-DCC内(非割点)放置一个出口
      因为如果这个割点是出口且坏掉,这个V-DCC就无法连到其他出口了
    • 如果V-DCC>1,就不需要设置出口
      如果其中一个割点坏了,则还可以到另一个割点联通的V-DCC的出口。

代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef unsigned long long ULL;
const int N = 510, M = 1010;
int n, m;
int to[N], pre[N], h[M], idx;
int dfn[N], low[N], times;
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N];//记录双连通分量的节点
int root;
bool cut[N];//判断是否为割点
void add(int a, int b)
{
    to[idx] = b, pre[idx] = h[a], h[a] = idx++;
}
void tarjan(int u)
{
    dfn[u] = low[u] = ++times;
    stk[++top] = u;
    //u是孤立点时  特殊判断
    if (u == root && h[u] == -1)
    {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
    int cnt = 0;
    for (int i = h[u]; i != -1; i = pre[i])
    {
        int j = to[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if (dfn[u] <= low[j])
            {
                cnt++;
                 // 判断u是否是割点 
                if (u != root || cnt > 1)
                    cut[u] = true;
                //双连通分量缩点
                dcc_cnt++;
                int y;
                do
                {
                    y = stk[top--];
                    dcc[dcc_cnt].push_back(y);
                } while (y != j);
                //弹出栈不是弹到u为止 而是弹到j为止
                dcc[dcc_cnt].push_back(u);
            }
        }
        else
            low[u] = min(low[u], dfn[j]);
    }
}
int T = 1;
signed main()
{

    while (cin >> m && m)
    {
        memset(h, -1, sizeof h);
        for (int i = 0; i <= n; i++)
            dcc[i].clear();
        memset(dfn, 0, sizeof dfn);
        memset(low, 0, sizeof low);
        memset(cut, 0, sizeof cut);
        n = idx = times = top = dcc_cnt = 0;

        while (m--)
        {
            int a, b;
            cin >> a >> b;
            n = max(m, max(a, b));
            add(a, b), add(b, a);
        }
        for (root = 1; root <= n; root++)
        {
            if (!dfn[root])
                tarjan(root);
        }
        int res = 0;
        ULL num = 1;
        for (int i = 1; i <= dcc_cnt; i++)
        {

            int cnt = 0;
            for (int t : dcc[i])
            {
                if (cut[t])
                    cnt++;
            }

            if (cnt == 0)
            {
                if (dcc[i].size() > 1)
                    res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2;
                else
                    res++;
            }
            if (cnt == 1)
            {
                res++, num *= dcc[i].size() - 1;
            }
        }
        cout << "Case " << T++ << ": " << res << " " << num << endl;
    }

    return 0;
}
posted @ 2022-07-13 11:10  kingwzun  阅读(134)  评论(0编辑  收藏  举报