0x66.圖論 - Tarjan算法與無向圖連通性

聲明:
本系列博客是《算法競賽進階指南》+《算法競賽入門經典》+《挑戰程序設計競賽》的學習筆記,主要是因爲我三本都買了 按照《算法競賽進階指南》的目錄順序學習,包含書中的少部分重要知識點、例題解題報告及我個人的學習心得和對該算法的補充拓展,僅用於學習交流和複習,無任何商業用途。博客中部分內容來源於書本和網絡(我儘量減少書中引用),由我個人整理總結(習題和代碼可全都是我自己敲噠)部分內容由我個人編寫而成,如果想要有更好的學習體驗或者希望學習到更全面的知識,請於京東搜索購買正版圖書:《算法競賽進階指南》——作者李煜東,強烈安利,好書不火系列,謝謝配合。


下方鏈接爲學習筆記目錄鏈接(中轉站)

學習筆記目錄鏈接


ACM-ICPC在線模板


一、無向圖的割點與橋

割點

對於圖中一點,從圖中刪掉這個點和所有與這個點有關聯的邊之後,圖就不是連通圖,分裂成爲了兩個或兩個以上不相連的子圖,這樣的點被稱作該圖的割點。

橋/割邊

對於圖中一邊,刪除這條邊後,圖就不是連通圖,分裂成爲兩個不相連的子圖. ,這樣的邊被稱作該圖的橋或者割邊。

時間戳

按照圖的深度優先遍歷的過程,以每一個結點第一次被訪問的順序,依次賦值1~N的整數標記,該標記就被稱爲時間戳,標記了每一個結點的訪問順序,記作dfn[x]dfn[x]

搜索樹

具體定義詳解見《算法競賽進階指南》P395

下圖左側是一張無向連通圖,加粗的邊是 “ 發生遞歸 ” 的邊。
右側是一棵搜索樹,並標註了結點的時間戳。
在這裏插入圖片描述

追溯值

追溯值 low[i]low[i]

dfn[i]dfn[i]:表示第i個點的時間戳。

low[i]low[i]:表示點i及i的子樹所能追溯到的最早的節點的時間戳。

low[x]low[x]定義爲下列兩種結點的時間戳的最小值。

  1. 以x爲根的子樹上的結點。
  2. 通過1條不在搜索樹上的邊,能夠到達以x爲根的子樹上結點的結點。

首先令low[x]=dfn[x]low[x] = dfn[x],考慮從x出發的每條邊(x,y)(x,y)

若在搜索樹上x是y的父結點(注意是無向圖,雙邊),則令low[x]=min(low[x],low[y])low[x] = min(low[x],low[y])

若無向邊(x,y)(x,y)不是搜索樹上的邊,則令low[x]=min(low[x],dfn[y])low[x] = min(low[x],dfn[y])

下圖【 】中爲該點的追溯值。

在這裏插入圖片描述

二、割邊判定法則

割邊(u,v)(u, v)刪去後變爲兩個連通塊,v無法到達u前面的點,即
low[v]>dfn[u]low[v] > dfn[u]

具體證明解析等見《算法競賽進階指南》P396

下面程序求出一張無向圖中所有的橋,爲了處理重邊的情況,我們使用成對變換的技巧。

const int N = 1e5+7;//點數
const int M = 5e5+7;//邊數
int head[M], ver[M], nex[M], tot = 1;//成對變換tot要初始化1,從2開始
int dfn[N], low[N];
int n, m, num;
bool bridge[M];

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

void tarjan(int x,int in_edge){//當前結點 x 和前向星的編號 in_edge
    dfn[x] = low[x] = ++num;
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i];
        if(!dfn[y]){
            tarjan(y,i);
            low[x] = min(low[x],low[y]);

            if(dfn[x] < low[y])
                bridge[i] = bridge[i ^ 1] = true;//利用成對變換的時候都是用的編號
        }
        else if(i != (in_edge ^ 1)){//若不是父結點
            low[x] = min(low[x],dfn[y]);//就更新
        }
    }
}

int main()
{
    cin>>n>>m;
    over(i,1,m){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);add(y,x);
    }
    over(i,1,n)
        if(!dfn[i])//和有向圖的操作一樣
            tarjan(i,0);//注意是(i,0)
    for(int i = 2;i < tot;i += 2)//每次兩個
        if(bridge[i])
            printf("%d %d\n",ver[i ^ 1],ver[i]);
    return 0;
}

三、割點判定法則

割點u刪去後會有至少一個子樹中的點無法到達u前面的點,即
存在至少一條樹枝邊(u,v)(u, v)滿足
low[v]dfn[u] low[v]≥dfn[u]
對於根結點需要特別判斷,只要有多於一條樹枝邊(兩條及以上)則爲割點。
由於割點判定法則爲 ,所以不需要考慮父結點及重邊。
具體代碼實現見下面給出的模板題

1.luogu P3388 【模板】割點(割頂)

在這裏插入圖片描述
割點模板題

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 1e5+7;//點數
const int M = 5e5+7;//邊數

int head[N], nex[M], ver[M], tot;
int n, m, num, root;
int stk[N], top;
int dfn[N], low[N];
bool cut[N];
int ans;

void add(int x,int y){
    ver[++tot] = y;
    nex[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 = nex[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;
    over(i,1,m){
        int x,y;
        scanf("%d%d",&x,&y);
        if(x == y)continue;
        add(x,y);add(y,x);
    }
    over(i,1,n)
        if(!dfn[i])
            root = i,tarjan(i);
    over(i,1,n)if(cut[i])ans++;+
    printf("%d\n",ans);
    over(i,1,n)
    if(cut[i])
        printf("%d ",i);
    puts("");
    return 0;
}

2.luogu P3469 [POI2008]BLO-Blockade(割點判定 + 思維計算)

在這裏插入圖片描述
解題報告:luogu P3469 [POI2008]BLO-Blockade(割點判定 + 思維計算)

四、無向圖的雙連通分量

開始前的基礎概念:

若一張無向連通圖不存在割點,則稱它爲“點雙連通圖”,不存在橋則稱爲“邊雙連通圖”。

無向圖的極大雙連通子圖就是“點雙連通分量”:vDCCv-DCC,極大雙連通子圖就是“邊雙連通分量”:eDCCe-DCC

定理:

  1. 一張無向連通圖是點雙連通圖當且僅當 圖的頂點數<=2 或者 圖中任意兩點都同時包含在至少一個簡單環中。

  2. 一張無向連通圖是邊雙連通圖當且僅當任意一條邊都包含在至少一個簡單環中。

1.邊雙連通分量(eDCCe-DCC)的求法

e-DCC的求法很簡單,通過一遍Tarjan算法找到所有的橋,把橋刪除後,無向圖會分裂成一個個連通塊。

每一個連通塊都是一個e-DCC。

具體實現就是先用Tarjan算法標記所有橋,然後對整張圖dfs一遍(不訪問橋邊),劃分出所有連通塊。

一般可以用一個數組c,表示每個節點所在的e-DCC的編號。

老規矩, 給出模板題以及AC代碼

luogu T103489 【模板】邊雙連通分量

在這裏插入圖片描述

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 5e5+7;
const int M = 5e6+7;

int ver[M],head[N],nex[M],tot = 1;
int c[N];// DCC 的編號
int dcc;//DCC 的數量,且是用來編號的
int dfn[N],low[N],cnt,num;
int n,m;
bool bridge[N];

inline void add(int x,int y){
    ver[++tot] = y;
    nex[tot] = head[x];
    head[x] = tot;
}
//in_edge是前向星的編號
void tarjan(int x,int in_edge){
    dfn[x] = low[x] = ++num;
    for(int i = head[x];i;i = nex[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]);
        }
    }
}

void dfs(int x){
    c[x] = dcc;
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i];
        if(c[y] || bridge[i])continue;
        dfs(y);
    }
}

int main()
{
    cin>>n>>m;
    over(i,1,m){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);add(y,x);
    }
    over(i,1,n)
        if(!dfn[i])//還是沒有遍歷到,說明是一個新的連通分量
            tarjan(i,0);
    over(i,1,n)
        if(!c[i])
            ++dcc,dfs(i);
    //over(i,1,n)
    //printf("%d belongs to DCC %d.\n",i,c[i]);
    cout<<dcc<<endl;
    return 0;
}

2.eDCCe-DCC的縮點

我們把每一個e-DCC都看成一個結點(只是看成結點),把所有橋邊(x,y)看成連接編號爲c[x]和c[y]的兩個e-DCC間的邊,這樣我們就會得到一棵樹或者森林(原圖不連通)。並把e-DCC縮點生成的樹(森林)儲存在另一個鄰接表中(新開一個鏈式前向星來存樹)。

以下代碼加在上面的那道邊雙連通分量模板的代碼裏即可

int hc[N],vc[M],nc[M],tc;
void add_c(int x,int y){
    vc[++tc] = y;
    nc[tc] = hc[x];
    hc[x] = tc;
}

//以下代碼片段加在main函數裏
{
    tc = 1;//因爲要用到成對變換所以要初始化爲1
    for(int i = 2;i <= tot;++i){//成對變換所以從2開始
        int x = ver[i ^ 1],y = ver[i];
        if(c[x] == c[y])continue;
        add_c(c[x],c[y]);
    }
    printf("縮點以後的森林,點數%d,邊數%d(可能有重邊)\n",dcc,tc / 2);
    for(int i = 2 ;i < tc;i += 2)//一對一對的
        printf("%d %d\n",vc[i ^ 1],vc[i]);
    return 0;
}

3.點雙連通分量(vDCCv-DCC)的求法

v-DCC是一個很容易混淆的概念。

由於v-DCC定義中的“極大”,一個割點可能屬於多個v-DCC。

爲了求出v-DCC,我們需要在Tarjan的過程中維護一個棧。

當一個點第一次被訪問時,我們將它入棧。而當割點判定法則成立時,無論x是否爲根,都要從棧頂不斷彈出節點直到y節點被彈出,這些被彈出的節點包括x節點一起構成一個v-DCC。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 1e5+7;
const int M = 5e5+7;

int ver[M],head[N],nex[M],tot = 1;
int c[N];// DCC 的編號
vector<int> dcc[N];//DCC 的數量,且是用來編號的
int dfn[N],low[N],cnt,num;
int n,m,root;
int stk[N],top;
int cut[N];

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

void tarjan(int x){
    dfn[x] = low[x] = ++num;
    stk[++top] = x;
    if(x == root && head[x] == 0){//孤立點
        dcc[++cnt].push_back(x);
        return ;
    }
    int flag = 0;
    for(int i = head[x];i;i = nex[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;
                cnt++;
                int z;
                do{
                    z = stk[top--];
                    dcc[cnt].push_back(z);
                }while(z != y);
                dcc[cnt].push_back(x);//最後把x放進去
            }
        }
        else low[x] = min(low[x],dfn[y]);
    }
}

int main()
{
    cin>>n>>m;
    over(i,1,m){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);add(y,x);
    }
    over(i,1,n)
        if(!dfn[i])
            root = i,tarjan(i);//點雙連通分量。不用判父節點
    over(i,1,cnt){
        printf("v-DCC #%d:",i);//第幾個
        over(j,0,dcc[i].size())
        printf(" %d",dcc[i][j]);
        puts("");
    }
    return 0;
}

4.vDCCv-DCC的縮點

v-DCC的縮點由於一個割點可能在很多個v-DCC中而更加麻煩,但是我們也有辦法縮。

假設圖中有x個割點和y個v-DCC,我們就直接建(x+y)個點的新圖。

每一個v-DCC和割點都作爲新圖的節點存在。建完後我們讓每個割點和包含它的v-DCC連邊。

五、歐拉路問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章