天天看点

无向图的割点与桥【基本概念】【Tarjan求桥】【Tarjan求割点】【参考资料】

目录

  • 【基本概念】
    • 1.连通图与连通分量
    • 2.无向图的割点与桥
  • 【Tarjan求桥】
    • 1.算法思想
    • 2.模板
  • 【Tarjan求割点】
    • 1.算法思想
    • 2.模板
  • 【参考资料】

【基本概念】

1.连通图与连通分量

  1. 连通图:无向图 G G G 中,若对任意两点,从顶点 V i V_i Vi​ 到顶点 V j V_j Vj​ 有路径,则称 V i V_i Vi​ 和 V j V_j Vj​ 是连通的,图 G G G 是一连通图。
  2. 连通分量:无向图 G G G 的连通子图称为 G G G 的连通分量。
  3. 任何连通图的连通分量只有一个,即其自身,而非连通的无向图有多个连通分量。

    以下图为例,总共有四个连通分量,分别是: A B C D ABCD ABCD、 E E E、 F G FG FG、 H I HI HI。

无向图的割点与桥【基本概念】【Tarjan求桥】【Tarjan求割点】【参考资料】

2.无向图的割点与桥

给定无向连通图 G = ( V , E ) G=(V,E) G=(V,E):

  1. 若对于 x ∈ V x∈V x∈V,从图中删去节点 x x x 以及所有与 x x x 关联的边之后, G G G 分裂成两个或两个以上不相连的子图,则称 x x x 为 G G G 的割点。

    如下图,若去掉 0 0 0,则图被分成 12 12 12 和 34 34 34 两个连通分量;若去掉 3 3 3,则图被分成 012 012 012 和 4 4 4 两个连通分量。故: 0 、 3 0、3 0、3 是割点,两者是一个割点集,点连通度(最小割点集合点数)是 2 2 2。

无向图的割点与桥【基本概念】【Tarjan求桥】【Tarjan求割点】【参考资料】
  1. 若对于 e ∈ E e∈E e∈E,从图中删去边 e e e 之后, G G G 分裂成两个不相连的子图,则称 e e e 为 G G G 的桥或割边。

    如下图,去掉 5 − 6 5-6 5−6, 2 − 5 2-5 2−5 后不再连通,图被分为 1234 、 5 、 6 1234、5、6 1234、5、6三个连通分量。故: 5 − 6 、 2 − 5 5-6、2-5 5−6、2−5 是割边,两者组成的集是割边集,边连通度(最小割边集合边数)是 2 2 2。

无向图的割点与桥【基本概念】【Tarjan求桥】【Tarjan求割点】【参考资料】

一般无向图 (不一定连通)的割点和桥就是它的各个连通块的割点和桥。

【Tarjan求桥】

1.算法思想

时间戳

在图的深度优先遍历过程中,按照每个节点第一次被访问的时间顺序,依次给予 N N N 个节点 1 ∼ N 1 \sim N 1∼N 的整数标记,该标记就被称为时间戳,记为 d f n [ x ] dfn[x] dfn[x]。

搜索树

在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次。

所有发生递归的边 ( x , y ) (x,y) (x,y)(换言之,从 x x x 到 y y y 是对 y y y 的第一次访问)构成一棵树,我们把它称为无向连通图的搜索树。

当然,一般无向图(不一定连通)的各个连通块的搜索树构成无向图的搜索森林。

追溯值

除了时间戳之外,Tarjan算法还引入了一个追溯值 l o w [ x ] low[x] low[x]。

设 s u b t r e e ( x ) subtree(x) subtree(x) 表示搜索树中以 x x x 为根的子树。 l o w [ x ] low[x] low[x] 定义为以下节点的时间戳的最小值:

  1. s u b t r e e ( x ) subtree(x) subtree(x) 中的节点。
  2. 通过 1 1 1 条不在搜索树上的边,能够到达 s u b t r e e ( x ) subtree(x) subtree(x) 的节点。

根据定义,为了计算 l o w [ x ] low[x] low[x],应该先令 l o w [ x ] = d f n [ x ] low[x] = dfn[x] low[x]=dfn[x],然后考虑从 x x x 出发的每条边 ( x , y ) (x,y) (x,y):

  1. 若在搜索树上 x x x 是 y y y 的父节点,则令 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x] = min(low[x], low[y]) low[x]=min(low[x],low[y])。
  2. 若无向边 ( x , y ) (x, y) (x,y) 不是搜索树上的边,则令 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x] = min(low[x], dfn[y]) low[x]=min(low[x],dfn[y])。
无向图的割点与桥【基本概念】【Tarjan求桥】【Tarjan求割点】【参考资料】
无向图的割点与桥【基本概念】【Tarjan求桥】【Tarjan求割点】【参考资料】

割边判定法则

无向边 ( x , y ) (x, y) (x,y) 是桥,当且仅当搜索树上存在 x x x 的一个子节点 y y y,满足:

d f n [ x ] < l o w [ y ] dfn[x] < low[y] dfn[x]<low[y]

简单证明:

    根据定义, d f n [ x ] < l o w [ y ] dfn[x] < low[y] dfn[x]<low[y] 说明从 s u b t r e e ( y ) subtree(y) subtree(y) 出发,在不经过 ( x , y ) (x,y) (x,y) 的前提下,不管走哪条边,都无法到达 x x x 或比 x x x 更早访问的节点。若把 ( x , y ) (x, y) (x,y) 删除,则 s u b t r e e ( y ) subtree(y) subtree(y) 就好像形成了一个封闭的环境,与节点 x x x 没有边相连,图断开成了两部分,因此 ( x , y ) (x,y) (x,y) 是割边。

    反之,若不存在这样的子节点 y y y,使得 d f n [ x ] < l o w [ y ] dfn[x] < low[y] dfn[x]<low[y],则说明每个 s u b t r e e ( y ) subtree(y) subtree(y) 都能绕行其他边到达 x x x 或比 x x x 更早访问的节点, ( x , y ) (x,y) (x,y) 自然就不是割边。

桥一定是搜索树中的边,并且一个简单环中的边一定都不是桥。

2.模板

特别需要注意,因为我们遍历的是无向图,所以从每个点 x x x 出发,总能访问到它的父节点 f a fa fa。根据 l o w low low 的计算方法, ( x , f a ) (x,fa) (x,fa) 属于搜索树上的边,且 f a fa fa 不是 x x x 的子节点,故不能用 f a fa fa 的时间戳来更新 l o w [ x ] low[x] low[x]。

但是,如果仅记录每个节点的父节点,会无法处理重边的情况——当 x x x 与 f a fa fa 之间有多条边时, ( x , f a ) (x,fa) (x,fa) 一定不是桥。

一个好的解决方案是:改为记录递归进入每个节点的边的编号。编号可认为是边在邻接表中存储的下标位置。利用成对变换技巧,把无向图的每条边看作双向边,成对存储在下标"2和3" “4和5” “6和7”…处。若沿着编号为 i i i 的边递归进入了节点 x x x,则忽略从 x x x 出发的编号为 i   x o r   1 i\ xor\ 1 i xor 1 的边,通过其他边计算 l o w [ x ] low[x] low[x] 即可。

const int N = 100010;
int head[N], ver[N * 2], Next[N * 2];
int dfn[N], low[N], n, m, tot, num;
bool bridge[N * 2];

void add(int x, int y)
{
    ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}

void tarjan(int x, int in_edge)
{
    dfn[x] = low[x] = ++num; 
    for(int i = head[x]; i; i = Next[i])
    {
        int y = ver[i];
        if(!dfn[y])
        {
            tarjan(y, i);
            low[x] = min(low[x], low[y]);
            
            if(low[y] > dfn[x])
                bridge[i] = bridge[i ^ 1] = true;
        }
        else if(i != (in_edge ^ 1))
            low[x] = min(low[x], dfn[y]);
    }
}

int main()
{
    cin >> n >> m;
    tot = 1;
    for(int i = 1; i <= m; i++)
    {
        int x, y;
        scanf("%d%d", &x, &y);
        add(x, y), add(y, x);
    }
    
    for(int i = 1; i <= n; i++)
        if(!dfn[i])
            tarjan(i, 0);
    
    for(int i = 2; i < tot; i += 2)
        if(bridge[i])
            printf("%d %d\n", ver[i ^ 1], ver[i]);
    
}
           

【Tarjan求割点】

1.算法思想

割点判定法则

若 x x x 不是搜索树的根节点(深度优先遍历的起点),则 x x x 是割点当且仅当搜索树上存在 x x x 的一个子节点 y y y,满足:

d f n [ x ] ≤ l o w [ y ] dfn[x]≤low[y] dfn[x]≤low[y]

特别地,若 x x x 是搜索树的根节点,则 x x x 是割点当且仅当搜索树上存在至少两个子节点 y 1 , y 2 y_1,y_2 y1​,y2​ 满足上述条件。

证明方法与割边的情形类似,这里就不再赘述。

2.模板

因为割点判定法则是小于等于号,所以在求割点时,不必考虑父节点和重边的问题,从 x x x 出发能访问到的所有点的时间戳都可以用来更新 l o w [ x ] low[x] low[x]。

下面的程序求出一张无向图中所有的割点。

const int SIZE = 100010;
int head[SIZE], ver[SIZE * 2], Next[SIZE * 2];
int dfn[SIZE], low[SIZE], stack[SIZE];
int n, m, tot, num, root;
bool cut[SIZE];

void add(int x, int y)
{
    ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}

void tarjan(int x)
{
    dfn[x] = low[x] = ++num;
    int flag = 0;
    for(int i = head[x]; i; i = Next[i])
    {
        int y = ver[i];
        if(!dfn[y])
        {
            tarjan(y);
            low[x] = min(low[x], low[y]);
            
            if(low[y] >= dfn[x])
            {
                flag++;
                if(x != root || flag > 1)   cut[x] = true;
            }
        }
        else
            low[x] = min(low[x], dfn[y]);
    }
}

int main()
{
    cin >> n >> m;
    tot = 1;
    for(int i = 1; i <= m; i++)
    {
        int x, y;
        scanf("%d%d", &x, &y);
        if(x == y)  continue;
        add(x, y), add(y, x);
    }
    
    for(int i = 1; i <= n; i++)
        if(!dfn[i])
            root = i, tarjan(i);
    
    for(int i = 1; i <= n; i++)
        if(cut[i])
            printf("%d ", i);
    
    puts("are cut-vertexes");
}
           

【参考资料】

  1. 《算法竞赛进阶指南》
  2. 图的连通性