無向圖:橋和割點
橋的概念:無向圖刪去邊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;//染色 } //... }