tarjan算法求割点割边

在上一节我们已经知道tarjan算法可以求联通图,在这里我们也运用tarjan的思想求割点与割边,首先我们先来说说割点,那么什么事割点呢,先来看一张图(a),图片来自网络

 在(a)图中,我们将A点以及与A点相连的边全部去除,会发现这个联通图被分成了俩个联通图,一个是节点F,另外一个是余下的所有的节点组成的图,因此我们将A点称为割点,同理我们发现B点也是割点,因此我们可以这样定义割点,在一个无向联通图中,如果删除某个节点后,图不再连通(即任意俩点之间不能相互到达),我们称这样的顶点为割点(定义来自啊哈算法),那么问题来了,如何求割点呢???

很容易的一个想法就是我们每次删除一个节点后用bfs或者dfs来遍历图是否依然联通,如果不联通则该点是割点,这种方法的复杂度是O(N(N + M)),太暴力了,我们来想想其他的方法

再介绍其他方法前,我们再来了解几个定义(定义来源于网络)

  • DFS搜索树:用DFS对图进行遍历时,按照遍历次序的不同,我们可以得到一棵DFS搜索树,如图(b)所示。
  • 树边:(在[2]中称为父子边),在搜索树中的实线所示,可理解为在DFS过程中访问未访问节点时所经过的边。
  • 回边:(在[2]中称为返祖边后向边),在搜索树中的虚线所示,可理解为在DFS过程中遇到已访问节点时所经过的边。

通过观察图(2),我们发现有俩类节点可能成为割点

  1. 对根节点u,若其有两棵或两棵以上的子树,则该根结点u为割点;(说明删除该节点会产生至少俩棵不联通的子树)
  2. 对非叶子节点u(非根节点),若其子树的节点均没有指向u的祖先节点的回边,说明删除u之后,根结点与u的子树的节点不再连通;则节点u为割点。(在这里可以简单画个图理解一下)

对于这俩类节点,前者我们很容易判断,我们就不做过多的说明了,具体的判断会在代码里面讲解,我们来说说后者的判断

我们用dfn[u]记录节点u在DFS过程中被遍历到的次序号,low[u]记录节点u或u的子树通过非父子边追溯到最早的祖先节点(即DFS次序号最小),那么low[u]的计算过程如下:

low[u]={min{low[u], low[v]},((u,v)为树边)

low【u】 = min{low[u], dfn[v]}    (u,v)为回边且v不为u的父亲节点

至于为什么不为u的父亲节点我暂时还没有想明白......

关于dfn和low数组的计算请参考我另外一篇博客或者去百度上查询资料,这个不好用语言描述,我就暂且大家都知道了

那么对于第二类节点,当(u,v)为树边且low[v] >= dfn[u]时,节点u才为割点。该式子的含义:表示v节点不能绕过u节点从而返回到更早的祖先,因此u节点为割点

下面来看代码实现,在代码里面会做详细的解说

#include<iostream>
#include<cmath>
using namespace std;
int n, m, root, a, b, total;
int e[101][101], dfn[101], low[101], flag[101], head[101];

//链式前向星,不懂的自行百度
struct node{
    int to;
    int next;
}edge[10010];

int cnt = 1;

//前向星建图
void add(int u, int v) {
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}

void tarjan(int u, int father) {
    int child = 0;//child用来记录在生成树中当前顶点的儿子的个数
    dfn[u] = low[u] = ++total;//时间戳
    for (int i = head[u]; i != 0; i = edge[i].next) {//前向星遍历该顶点的所有边
        int v = edge[i].to;//该顶点连接的顶点
        if (!dfn[v]) {//如果时间戳为0说明没有访问过
            child++;
            tarjan(v, u);//继续往下搜索
            low[u] = min(low[u], low[v]);//更新当前时间戳
            //  如果当前节点是根节点并且儿子个数大于等于2,则满足第一类节点,为割点
            if (u == root && child >= 2) {
                flag[u] = 1;
            //不为根结点但是满足第二类条件的节点
            } else if (u != root && low[v] >= dfn[u]) {
                flag[u] = 1;
            }
            //如果顶点被访问过并且不是该节点的父亲,说明此时的v为u的祖先,因此需要更新最早顶点的时间戳
        } else if (v != father) {
            low[u] = min(low[u], dfn[v]);
        }
    }
}


int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        cin >> a >> b;
        add(a, b);
        add(b, a);
    }
    root = 1; //假设1为根节点
//从1号顶点开始进行深度优先搜索(tarjan)
    tarjan(1, root);
    for (int i = 1; i <= n; i++) {
        if(flag[i]) {
            cout << i << " ";
        }
    }
    return 0;
}

给一组数据拿来测试一下该代码

6 7(6个顶点,7条无向边)

1 4

1 3

4 2

3 2

2 5

2 6

5 6

输出为 2

如果有多组图,那么我们使用另外的一组代码,见下

#include<iostream>
using namespace std;
const int maxn = 20010;
const int maxv = 200010;
int cnt = 1, n, m, total, a, b;
int head[maxn], flag[maxn], dfn[maxn], low[maxn], sum;

struct node{
    int to;
    int next;
}edge[maxv];

void add(int u, int v) {
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}

void tarjan(int u, int father) {
    int child = 0;
    dfn[u] = low[u] = ++total;
    for (int i = head[u]; i != 0; i = edge[i].next) {
        int v = edge[i].to;
        if (!dfn[v]) {
            tarjan(v, father);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u] && u != father) {
                flag[u] = 1;
            }
            if (u == father) child++;
        }
        low[u] = min(low[u], dfn[v]);
    }
    if (child >= 2 && u == father) {
        flag[u] = 1;
    }
}

int main () {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        cin >> a >> b;
        add(a, b);
        add(b, a);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, i);
        }
    }
    for (int i = 1; i <= n; i++) {
        if (flag[i]) sum++;
    }
    cout << sum << endl;
    for (int i = 1; i <= n; i++) {
        if (flag[i]) {
            cout << i << " ";
        }
    }
    return 0;
}

了解完割点后我们再来了解什么是割边,其实很简单,即在一个无向联通图中,如果删除某条边后,图不在联通,那么该条边称为割边,代码和上面的代码基本上一模一样,只要改一个小地方,就是将low【v】 >= dfn[u] 改为 low【v】> dfs

[u]即可,前者便是还可以回到父亲,后者表示连父亲都回不到了,倘若顶点v不能回到祖先,也没有另外一条路可以回到父亲,那么u-v这条边就是割边,其实仔细想想模拟个图就明白了,代码实现如下

#include<iostream>
#include<cmath>
using namespace std;
int n, m, root, a, b, total;
int e[101][101], dfn[101], low[101], flag[101], head[101];

struct node{
    int to;
    int next;
}edge[10010];

int cnt = 1;
void add(int u, int v) {
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}

void tarjan(int u, int father) {
    int child = 0;
    dfn[u] = low[u] = ++total;
    for (int i = head[u]; i != 0; i = edge[i].next) {
        int v = edge[i].to;
        if (!dfn[v]) {
            child++;
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if  (low[v] > dfn[u]) {
                cout << u << "->" << v << endl;
            }
        } else if (v != father) {
            low[u] = min(low[u], dfn[v]);
        }
    }
}


int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        cin >> a >> b;
        add(a, b);
        add(b, a);
    }
    root = 1;
    tarjan(1, root);
    for (int i = 1; i <= n; i++) {
        if(flag[i]) {
            cout << i << " ";
        }
    }
    return 0;
}

如果输入上面的那组数据那么不会有输出,因为没有割边,再给出一组数据

6 6

1 4

1 3

4 2

3 2

2 5

5 6

输出5->6

2->5

割点和割边就解释到这里了,完毕

猜你喜欢

转载自blog.csdn.net/LanQiLi/article/details/85009526