1. 程式人生 > >無向圖的割頂和橋,無向圖的雙連通分量入門詳解及模板 -----「轉載」

無向圖的割頂和橋,無向圖的雙連通分量入門詳解及模板 -----「轉載」

dbr break nts word 否則 mark push gravity 無向連通圖

https://blog.csdn.net/stillxjy/article/details/70176689

割頂和橋:對於無向圖G,如果刪除某個節點u後,連通分量數目增加,則稱u為圖的割頂;如果刪除某條邊後,連通分量數目增加,則稱該邊為圖的橋。對於連通圖刪除割頂或橋後都會使得圖不再連通

以下我,我們利用dfs的性質來快速找出一個連通圖中的所有的割頂和橋
首先我們要引入”時間戳”這個概念:

時間戳:表示在進行dfs時,每個節點被訪問的先後順序。每個節點會被標記兩次,分別用pre[],和post[]表示。
例如下圖的時間戳表示:(節點左上角為pre[],右上角為post[],子節點的訪問順序按照編號從小到達訪問)

技術分享圖片

圖中的邊分類:
樹邊與反向邊:在進行dfs時某條邊u-v,若v還沒有被訪問,則u-v為樹邊,若v已經被訪問過則u-v為反向邊。
對於上圖的DFS樹,下圖中實線為樹邊,虛線為反向邊
技術分享圖片
在無向圖中除了樹邊就是反向邊,且不存在跨越兩棵子樹的邊
所以對於根節點而言,如果有兩個及以上節點則根節點為割頂,否則不是
對於其他節點:在無向連通圖G的DFS樹中,非根節點u是割頂當且僅當u存在一個子節點v,使得v及其所有後代都沒有反向邊連回u的祖先(不包括u)
以上判斷條件很好想,只要隨便畫畫草圖就可以了

了解以上知識後我們找出圖中所有的割頂和橋
設low[u]為u及其後代所能連回的最早的祖先的pre[]值,則當u存在一個子節點v使得low[v] >= pre[u]時u就為割頂

同理當 low[v] > pre[u]時 u-v為橋

求圖中割頂和橋的代碼:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#include <vector>
using namespace std;

const int maxn = 1000;

int n,m;
vector<int> G[maxn];
int low[maxn],pre[maxn];
int dfs_clock;     //時間戳
int iscut[maxn];   //標記是否為割頂

int dfs(int u,int fa)
{
    int lowu = pre[u] = ++dfs_clock;
    int child = 0;
    for(int i=0;i<G[u].size();i++)
    {
        int v = G[u][i];
        if(!pre[v])     //沒有訪問的v
        {
            child++;    //孩子節點的數目
            int lowv = dfs(v,u);
            lowu = min(lowu,lowv);    //用後代更新lowu
            if(lowv >= pre[u]) iscut[u] = 1;
            if(lowv > pre[u]) cout<<"橋:"<<u<<"-"<<v<<endl;
        }
        else if(pre[v] < pre[u] && v != fa)  //用反向邊更新lowu
        {
            lowu = min(lowu,pre[v]);
        }
    }
    if(fa < 0 && child == 1) iscut[u] = 0;    //對於根節點的處理
    low[u] = lowu;
    return lowu;
}


int main()
{
    freopen("in.txt","r",stdin);
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        memset(pre,0,sizeof(pre));
        memset(iscut,0,sizeof(iscut));
        for(int i=0;i<=n;i++) G[i].clear();
        int u,v;
        for(int i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            G[u].push_back(v);
            G[v].push_back(u);
        }

        dfs(1,-1);
        for(int i=1;i<=n;i++) if(iscut[i])
            cout<<i<<endl;
    }
    return 0;
}


點_雙連通分量 BCC:
對於一個連通圖,如果任意兩點至少存在兩條“點不重復”的路徑,則說圖是點雙連通的(即任意兩條邊都在一個簡單環中),點雙連通的極大子圖稱為點_雙連通分量。
易知每條邊屬於一個連通分量,且連通分量之間最多有一個公共點,且一定是割頂

點_雙連通分量代碼模板:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#include <vector>
#include <stack>
using namespace std;

const int maxn = 1000;

struct Edge   //棧中邊的結構
{
    int u,v;
    Edge(int uu,int vv)
    {
        u = uu;
        v = vv;
    }
};
stack<Edge> s;

struct edge  //鏈式前向星建圖的邊結構
{
    int v,next;
}edges[maxn];

int n,m;        //節點的數目,無向邊的數目
int e,head[maxn];
int pre[maxn];           //第一次訪問的時間戳
int dfs_clock;           //時間戳
int iscut[maxn];         //標記節點是否為割頂
int bcc_cnt;             //點_雙連通分量的數目
int bccno[maxn];         //節點屬於的點_雙連通分量的編號
vector<int> bcc[maxn];   //點_雙連通分量

void addedges(int u,int v)  //加邊
{
    edges[e].v = v;
    edges[e].next = head[u];
    head[u] = e++;
    edges[e].v = u;
    edges[e].next = head[v];
    head[v] = e++;
}

int dfs(int u,int fa)
{
    int lowu = pre[u] = ++dfs_clock;
    int child = 0;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v = edges[i].v;
        Edge e = (Edge){u,v};
        if(!pre[v])
        {
            s.push(e);
            child++;
            int lowv = dfs(v,u);
            lowu = min(lowu,lowv); //用後代更新lowu
            if(lowv >= pre[u])     //找到了一個子樹滿足割頂的條件
            {
                iscut[u] = 1;
                bcc_cnt++;
                bcc[bcc_cnt].clear();
                for(;;)            //保存bcc信息
                {
                    Edge x = s.top(); s.pop();
                    if(bccno[x.u] != bcc_cnt) {bcc[bcc_cnt].push_back(x.u); bccno[x.u] = bcc_cnt;}
                    if(bccno[x.v] != bcc_cnt) {bcc[bcc_cnt].push_back(x.v); bccno[x.v] = bcc_cnt;}
                    if(x.u == u && x.v == v) break;
                }
            }
        }
        else if(pre[v] < pre[u] && v != fa)   //用反向邊更新lowu
        {
            s.push(e);
            lowu = min(lowu,pre[v]);
        }
    }
    if(fa < 0 && child == 1) iscut[u] = 0;    //對於根節點若只有一個子樹則不是割頂
    return lowu;
}

void init()
{
    memset(pre,0,sizeof(pre));
    memset(iscut,0,sizeof(iscut));
    memset(head,-1,sizeof(head));
    memset(bccno,0,sizeof(bccno));
    e = 0; dfs_clock = 0; bcc_cnt = 0;
}

int main()
{
    int u,v;
    freopen("in.txt","r",stdin);
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        init();
        for(int i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            addedges(u,v);
        }
        dfs(1,-1);
        for(int i=1;i<=bcc_cnt;i++)
        {
            for(int j=0;j<bcc[i].size();j++)
                cout<<bcc[i][j]<<" ";
            cout<<endl;
        }

    }
    return 0;
}

代碼講解:在理解了上面找割頂的代碼後,以上求BCC的代碼就是用一個棧保存所有的訪問的邊,然後在找到一個割頂之後就將該割頂信息全部出棧後保存起來即可。(具體實現細節要自己手寫代碼驗證最好,詳見代碼)

邊_雙連通分量 EBC:
對於邊_雙連通分量的求解簡單多了,我們先找出所有的橋,並將其做上標記。然後在利用dfs遍歷連通分量即可,只需在遍歷時不能訪問橋即可。

邊_雙連通分量代碼模板

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#include <vector>
using namespace std;

const int maxn = 1000;
struct Edge
{
    int no,v,next;      //no:邊的編號
}edges[maxn];

int n,m,ebcnum;         //節點數目,無向邊的數目,邊_雙連通分量的數目
int e,head[maxn];
int pre[maxn];          //第一次訪問的時間戳
int dfs_clock;          //時間戳
int isbridge[maxn];     //標記邊是否為橋
vector<int> ebc[maxn];  //邊_雙連通分量

void addedges(int num,int u,int v)    //加邊
{
    edges[e].no = num;
    edges[e].v = v;
    edges[e].next = head[u];
    head[u] = e++;
    edges[e].no = num++;
    edges[e].v = u;
    edges[e].next = head[v];
    head[v] = e++;
}

int dfs_findbridge(int u,int fa)    //找出所有的橋
{
    int lowu = pre[u] = ++dfs_clock;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v = edges[i].v;
        if(!pre[v])
        {
            int lowv = dfs_findbridge(v,u);
            lowu = min(lowu,lowv);
            if(lowv > pre[u])
            {
                isbridge[edges[i].no] = 1; //橋
            }
        }
        else if(pre[v] < pre[u] && v != fa)
        {
            lowu = min(lowu,pre[v]);
        }
    }
    return lowu;
}

void dfs_coutbridge(int u,int fa)     //保存邊_雙連通分量的信息
{
    ebc[ebcnum].push_back(u);
    pre[u] = ++dfs_clock;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v = edges[i].v;
        if(!isbridge[edges[i].no] && !pre[v]) dfs_coutbridge(v,u);
    }
}

void init()
{
    memset(pre,0,sizeof(pre));
    memset(isbridge,0,sizeof(isbridge));
    memset(head,-1,sizeof(head));
    e = 0; ebcnum = 0;
}

int main()
{
    int u,v;
    freopen("in.txt","r",stdin);
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        init();
        for(int i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            addedges(i,u,v);
        }
        dfs_findbridge(1,-1);
        memset(pre,0,sizeof(pre));
        for(int i=1;i<=n;i++)
        {
            if(!pre[i])
            {
                ebc[ebcnum].clear();
                dfs_coutbridge(i,-1);
                ebcnum++;
            }
        }
        for(int i=0;i<ebcnum;i++)
        {
            for(int j=0;j<ebc[i].size();j++)
                cout<<ebc[i][j]<<" ";
            cout<<endl;
        }
    }
    return 0;
}

以上都是本人看了白書(《算法競賽入門經典——訓練指南》)後對相關知識點的總結,若有不清楚的地方可以直接去看課本,或者留下評論,謝謝

無向圖的割頂和橋,無向圖的雙連通分量入門詳解及模板 -----「轉載」