無向連通圖小結


無向圖:橋和割點

橋的概念:無向圖刪去邊e后分裂成兩個不相連的子圖

割點概念:無向圖刪去點v以及和v相連的所有邊后分裂成兩個及以上的子圖

一些概念:

  搜索樹:在無向圖中任意選擇一點作為起點進行dfs,每個點訪問一次,每次發生遞歸的邊(x,y),即訪問到之前沒有訪問到的點所經過的邊,組成的搜索樹

  時間戳:在dfs過程中無向圖中的結點被訪問到的時間,用dfn數組來表示

  追溯值:用low數組表示追溯值,low[x]定義為以下結點的時間戳的最小值:

      1.x 子孫結點的時間戳

      2.x子孫能通過非搜索樹上的邊所達到的點的時間戳

橋的判定法則:無向邊(x,y)是橋 <==> 搜索樹上存在x的一個子節點y,有dfn[x]<low[y]

  推論:橋一定是搜索樹中的邊,一個簡單環中的邊一定都不是橋

割點的判定法則:如果x是root,那么如果x有兩個及以上兒子,x就是割點

        如果x非root,那么如果搜索樹上存在x的一個子節點y,有dfs[x]<=low[y],那么x就是割點

 求橋時要特別注意一下重邊和父邊!

下面是求橋和割點的代碼

 

void Tarjan(int u,int pre){
    low[u]=dfn[u]=++index;
    int son=0,pre_cnt=0;//處理重邊和回邊
    for(int i=head[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(v==pre&&pre_cnt==0){
            pre_cnt++;continue;
        }
        if(!dfn[v]){//如果v未被訪問過 
            son++;
            Tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(dfn[u]<low[v]){//
                bridge++;
                edge[i].cut=true;
                edge[i^1].cut=true;
            } 
            if(u!=pre && dfn[u]<=low[v]){//非樹根的割點 
                cut[u]=true;
                add_block[u]++;
            }
        }
        else low[u]=min(low[u],dfn[v]);//v被訪問過了 
    } 
    if(u==pre && son>1)cut[u]=true;//樹根割點 
    if(u==pre)add_block[u]=son-1; 
}

 

無向圖:雙聯通分量

點雙聯通圖:無向連通圖不存在割點

邊雙聯通圖:無向連通圖不存在橋

點雙聯通分量:無向圖的極大點雙聯通子圖v-DCC

邊雙聯通分量:無向圖的極大邊雙聯通子圖e-DCC

定理

  點雙聯通圖的判定條件:

    1.圖的頂點數不超過2

    2.圖中任意兩點至少包含在一個簡單環中

  邊雙聯通圖的判定條件:

    1.圖中任意一條邊都包含在一個簡單環中

 

e-DCC的求法:只要求出圖中所有橋,把橋刪掉后圖分裂成的聯通塊就是e-DCC

先把橋都求出來,然后一次dfs進行染色,數組c[x]表示x所在的聯通塊的編號

int c[maxn],dcc;
void dfs(int u){
    c[u]=dcc;
    for(int i=head[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(c[v] || bridge[i])continue;//碰到橋就不往下dfs
        dfs(v); 
    }
}
int main(){
    //...
    dcc=0;
    for(int i=1;i<=n;i++)
        if(!c[i]){
            ++dcc;
            dfs(i);
        }
    //...
}

e-DCC的縮點法,把每個邊雙聯通分量縮成一個點,將原圖變成一棵樹或森林,存在新的鄰接表中

這個過程中同樣可以求出每個縮點的度數

struct Edge{
    int to,nxt;
}edge_c[maxn<<1];
int head_c[maxn],tot_c;
void init(){
    memset(head_c,-1,sizeof head_c);
    tot_c=0;
}
void add_c(int u,int v){
    edge_c[tot_c].to=v;
    edge_c[tot_c].nxt=head_c[u];
    head_c[u]=tot_c++;
}
int main(){
    //...
    for(int i=0;i<tot;i+=2){
        int v=edge[i].to,u=edge[i^1].to;//無向圖的兩條邊一定連在一起 
        if(c[u]==c[v])continue;
        add_c(c[u],c[v]);
    }
    //...    
}

 

v-DCC的求法:需要在Tarjan算法中維護一個棧

  1.當結點x第一次被訪問時,把該節點入棧

  2.當dfn[x]<=low[y]條件成立時,無論x是否為根都要

    a.從棧頂不斷彈出結點,直到結點 y 被彈出!!!(注意是y,不用擔心同一個聯通分量的點會被分成兩個)

    b.剛才彈出的所有結點與結點x一起構成一個v-DCC

  開一個vector數組dcc,dcc[i]保存編號為i的v-DCC中的所有結點

vector<int>dcc[maxn];
int cnt;
void tarjan(int x){
    dfn[x]=low[x]=++index;
    stack[++top]=x;
    if(x==root && head[x]==-1){//x是個孤立點 
        dcc[++cnt].push_back(x);
        return;
    }
    int flag=0;
    for(int i=head[x];i!=-1;i=edge[i].nxt){
        int y=edge[i].to;
        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;//獨立判斷一次x是割點 
                cnt++;
                int z;
                do{//退棧 
                    z=stack[top--];
                    dcc[cnt].push_back(z);
                }while(z!=y);
                dcc[cnt].push_back(x);//x也要進棧,但是x可能作為割點會被兩個及以上聯通分量包含,暫時不出棧 
            }
        }
        else low[x]=min(low[x],dfn[y]); 
    }
} 

v-DCC的縮點

設圖中有p個割點,t個v-DCC,縮點后的圖有p+t個點

把每個DCC和每個割點都作為新圖中的結點,並在每個割點與包含它的所有v-DCC之間連邊

 先給所有的割點新編號

int main(){
    //...
    num=cnt;
    for(int i=1;i<=n;i++)
        if(cut[x])new_id[x]=++num;//給割點新編號
         
    tot_c=0;
    for(int i=1;i<=cnt;i++)//枚舉每個聯通分量 
        for(int j=0;j<dcc[i].size();i++){
            int x=dcc[i][j];
            if(cut[x]){//割點和聯通分量連邊 
                add_c(i,new_id[x]);
                add_c(new_id[x],i); 
            } 
            else c[x]=i;//染色 
        }
    //...
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM