tarjan複習筆記 雙連通分量,強連通分量
宣告:圖自行參考割點和橋QVQ
雙連通分量
-
如果一個無向連通圖\(G=(V,E)\)中不存在割點(相對於這個圖),則稱它為點雙連通圖
-
如果一個無向連通圖\(G=(V,E)\)中不存在割邊(相對於這個圖),則稱它為邊雙連通圖
-
無向圖的極大點雙連通子圖稱為點雙連通分量,簡稱\(v-DCC\)
-
無向圖的極大邊雙連通子圖稱為邊雙連通分量,簡稱\(e-DCC\)
-
如果稱一個雙連通子圖\(G'=(V',E')\)極大,當且僅當不存在\(G\)的另外一個子圖\(G''=(V'',E'')\neq G'\),使得\(G'\)是\(G''\)的子圖且\(G''\)是雙連通子圖
\(e-DCC\) (邊雙)
求法
-
刪除原圖中所有的橋,剩下的連通塊均為\(e-DCC\).
-
先用\(Tarjan\)標記所有的橋,在DFS每個連通塊,給各個點分配所在的\(e-DCC\)的編號即可
縮點法
-
在有些具有特殊性質的問題中,可以把一個\(e-DCC\)看做一個點進行處理
-
可以考慮一下求得所有的\(e-DCC\),然後建一張新圖,僅保留所有的\(e-DCC\)和橋
-
這種將一個雙連通分量收縮為一個節點的方法稱為縮點
-
程式碼實現中我們可以把每一個邊雙的編號看做是節點編號,如果兩個邊雙之間有橋,那麼在新圖中在這兩個點之間連邊即可
-
新圖是一棵樹或者森林
程式碼簡述(求無向圖中的橋,邊雙連通分量,並進行縮點)
void tarjan(int x,int in_edge) { low[x]=dfn[x]=++poi; for(int i=head[x];i;i=e[i].last) { int y=e[i].to; if(!dfn[y]) { tarjan(y,i); low[x]=min(low[x],low[y]); if(low[y]>dfn[x]) bridge[i]=bridge[i^1]=1; } else if(i!=(in_edge^1))//如果這個邊不是上次的反向邊 low[x]=min(low[x],dfn[y]); } }
首先根據\(Tarjan\)求橋的原理\(dfn[n]<low[y]\)求出橋,但是要注意的是這是雙向邊,所以正邊和反邊都要打標記,在這裡我們可以用位運算"^"實現反邊的操作,奇-1,偶+1
void dfs(int x)
{
c[x]=dcc;
for(int i=head[x];i;i=e[i].last)
{
int y=e[i].to;
if(c[y]||bridge[i]) continue;//如果這個點已經有了儲存的值或者這條邊是橋就不進行
//是橋的話要是弄進去那說明這個子圖中就有橋了。不符合
dfs(y);
}
}
然後用深搜給每個都進行標號,如果這個點是已經有編號了或者該邊是橋,那麼就繼續找
int main()
{
int n,m;
cin>>n>>m;
cnt=1;//保證運算簡便,邊的編號從2開始
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,0);//“^”運算,奇數-1,偶數+1
for(int i=2;i<cnt;i+=2)
if(bridge[i])
cout<<e[i^1].to<<" "<<e[i].to<<endl;
for(int i=1;i<=n;i++)
{
if(!c[i])
{
++dcc;
dfs(i);
}
}
cout<<"There are "<<dcc<<" e-DCCs"<<endl;
for(int i=1;i<=n;i++)
{
cout<<i<<" belongs to DCC "<<c[i]<<endl;
}
c_cnt=1;//邊還是從2開始,便於計數
for(int i=2;i<=cnt;i++)
{
int x=e[i^1].to;
int y=e[i].to;
if(c[x]==c[y])continue;
c_add(c[x],c[y]);//縮點建圖
}
cout<<"縮點以後的森林,點數為 "<<dcc<<" 邊數為 "<<c_cnt/2<<endl;
for(int i=2;i<c_cnt;i++)
{
cout<<ce[i^1].to<<" "<<ce[i].to<<endl;
}
return 0;
}
- 主函式裡面,首先我們把邊的計數值設為\(1\),那麼邊的編號就是從\(2\)開始,便於用"^"進行運算
- 然後先進行\(Tarjan\)把所有的橋找出來,進行深搜
- 當然因為是雙向的,所以反向邊一塊處理了即可,都標記為橋
- 然後就開始進行標號啦,深搜進行標號
- 然後我們就可以計算出有多少個邊雙連通分量以及他們的從屬關係
- 然後就開始建立新的圖,編號還是從2開始,便於計算
- 至於為什麼只建立有向邊,因為這個編號是+1+1處理的,它的反向邊一定會建立
- 最後就看結果就好啦QVQ
\(v-DCC\)(點雙)
上圖!
求法
-
如果一個點被孤立了,那麼它就自己構成一個點雙,否則點雙的大小至少為\(2\)
-
一個割點可以被多個點雙包含,其餘點只能在一個點雙裡面
-
看上面的圖,圖中的割點為\(1,6\)
-圖中的點雙為\([1,2,3,4,5],[1,6],[6,7],[6,8,9]\)
-
得出構造方法
1、先在原圖中削除所有的割點
2、列舉剩下的所有連通塊,然後向每一個連通塊中新增原圖中與該連通塊相連的割點
3、然後一個點雙就誕生了 -
於是偉大的哲人"他姐"發明了一個基於棧的做法
-
我們可以在\(Tarjan\)的過程中維護一個棧,並且按照如下的元素維護
1、當一個節點第一次被訪問到時,入棧
2、當搜到一個節點\(x\)且發先一個兒子\(y\)滿足割點法則\(dfn_x<=low_y\)時,無論\(x\)是否為根,都要從棧頂不斷彈出棧,然後直到\(y\)出棧,並將剛才的元素與\(x\)共同構成一個點雙
3、用vector維護即可
縮點法
-
保留割點,並且將所有的點雙都縮成一個點
-
每個點雙向自身包含的割點中進行連邊
-
如果原圖中一共有\(x\)個割點,\(y\)個點雙,新圖中一共有\(x+y\)個點
-
新圖中是一個樹或者是森林
程式碼實現
void tarjan(int x)
{
dfn[x]=low[x]=++poi;
suk[++top]=x;//將第一遍搜過的點入棧
if(x==root&&head[x]==0)//判斷孤立點
{
dcc[++sum].push_back(x);
return;
}
int flag=0;
for(int i=head[x];i;i=e[i].last)
{
int y=e[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]=1;//割點
int z;
sum++;
do{
z=suk[top--];
dcc[sum].push_back(z);
}while(z!=y);
dcc[sum].push_back(x);//形成一個新的v-DCC
}
}
else low[x]=min(low[x],dfn[y]);
}
}
首先還是進行\(Tarjan\)處理,求出每個點雙並且將割點標記。
int main()
{
cin>>n>>m;
cnt=1;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
if(x==y) continue;
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
{
root=i;
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
if(cut[i])
cout<<i<<" ";
}
cout<<"are cut-vertexes"<<endl;
for(int i=1;i<=sum;i++)
{
cout<<"e-DCC #"<<i<<": ";
for(int j=0;j<dcc[i].size();j++)
{
cout<<dcc[i][j]<<" ";
}
cout<<endl;
}
int js=sum;
for(int i=1;i<=n;i++)
{
if(cut[i])
new_id[i]=++js;//建立新的
}
c_cnt=1;//從2開始方便計算
for(int i=1;i<=sum;i++)// 建新圖,從每個v-DCC到它包含的所有割點連邊
{
for(int j=0;j<dcc[i].size();j++)
{
int x=dcc[i][j];
if(cut[x])
{
c_add(i,new_id[x]);
c_add(new_id[x],i);
}
else c[x]=i;
}
}
cout<<"縮點後的森林,點數為"<<js<<" 邊數為 "<<c_cnt/2<<endl;
printf("編號 1~%d 的為原圖的v-DCC,編號 >%d 的為原圖割點\n", sum, sum);
for(int i=2;i<c_cnt;i+=2)
printf("%d %d ",ce[i^1].to,ce[i].to);
return 0;
}
然後就是龐大的主函數了(提醒一下,在我的理解中root應該只出現在求割點的時候,其他時候基本木有)
- 首先,初始值設為1,編號從\(2\)開始建立雙邊(如果是單向的你也要建立,因為割點只存在於無向圖中)
- 然後進行\(Tarjan\)處理
- 當我們找完的時候,所有的點雙和割點都已經被我們求出來啦
- 然後我們就可以愉快的找出每一個點雙裡的元素
- 此時,我們的所有點雙已經被標完編號了,然後就開始給所有的割點建立新編號\(new-id\)
- 建立完以後還是編號從\(2\)開始分別找每個點雙裡面的割點,然後向他連雙向邊,然後把其他不是割點的點統計一下所在的點雙編號
- 最後輸出就好了!完美結束
強連通分量
-
對於一個有向圖,若關於任意的兩節點\(x,y\),既存在從\(x\)到\(y\)的路徑,同時也存在\(y\)到\(x\)的路徑,則稱該有向圖是強連通圖。
-
對於有向圖的極大強連通子圖稱為強連通分量,記為\(SCC\)
-
\(Tarjan\)演算法能夠線上性時間內求解有向圖所有的強連通分量
特殊定義
-
給定一個有向圖\(G=(V,E)\),存在\(r\in V\),\(r\)能到達\(V\)中的任何點,則稱\(G\)是一個流圖,記為\((G,r)\),\(r\)稱作\(G\)的源點
-
與無向圖類似,在流圖上從\(r\)出發開始DFS,每個節點只訪問一次
-
所有發生遞迴的邊構成一棵以\(r\)為根的樹,稱之為流圖\((G,r)\)的搜尋樹
-
按照每個節點第一次訪問的時間順序依次標號,該整數標號稱為時間戳,記為\(dfs_x\)
流圖中的邊
-
流圖中的有向邊\((x,y)\)一定是一下四種之一:
1、樹枝邊,搜尋樹上的
2、前向邊,不存在於搜尋樹上,且在搜尋樹中\(x\)是\(y\)的祖先
3、後向邊,不存在與搜尋樹上,且在搜尋樹中\(y\)是\(x\)的祖先
4、橫叉邊,不是上述三種情況的邊,那麼一定有\(dfn_y<=\)dfn_x,否則會在DFS的時候經過\(y\)從而構成樹枝邊 -
看圖理解一下
SCC的求法
定義梳理
-
根據定義,這一定是一個環,那麼所有的環一定是強連通圖
-
\(Tarjan\)演算法的基本思路就是對於每一個點,都儘量找到與它一起能構成環的所有節點
不同的邊的貢獻
- 對於一條邊\((x,y)\)我們討論一下他的型別
1、前向邊,對找環沒有用,因為在搜尋樹中本來就存在\(x->y\)的路徑
2、後向邊,對找環很有用,因為在搜尋樹中可以和\(x->y\)的路徑構成一個環
3、橫叉邊,對找環可能有用,如果從\(y\)出發能找到一條路徑回到\(x\)的祖先節點,則可以構成一個環,它就是有用的
遍歷
-
為了找到通過後向邊和橫叉邊構成的環,\(Tarjan\)演算法在DFS是維護一個棧
-
當第一次訪問到這個點時,入棧
-
訪問到\(x\)時,棧中儲存了一下的兩類點:
1、搜尋樹上\(x\)的祖先節點
2、已經訪問過,存在一條路徑能夠到達\(x\)的祖先的點 -
這些節點都存在一條到達\(x\)的路徑,如果\(x\)也能到達他們,那麼就構成了一個環
追溯值
- 可以理解為\(x\)的搜尋子樹上\(x\)能到達的時間戳最小的能到達\(x\)的點
構建方法
- 當第一次訪問到\(x\)是,首先令\(low_x=dfn_x\)
-在考慮與\(x\)相連的每一條邊,DFS回溯的時候更新\(low_x\)
-
如果\(y\)沒有被訪問,那麼就遞迴的訪問,則\(low_x=min(low_x,low_y)\)
-
但如果\(y\)在棧中,那麼\(low_x=min(low_x,dfn_y)\)
-
(重點)當\(x\)回溯以前,首先先判斷是否有\(low_x=dfn_x\)
-
如果有,那麼久不斷彈棧直到\(x\)出棧
-
彈棧的所有節點構成了一個SCC
構建理解
-
當我們回溯完畢時,已經考慮了\(x\)能到達的所有節點
-
\(y\)被訪問過並且不再棧中:\(x\)能達到\(y\),但\(y\)無法到達\(x\)
-
由回溯完畢可知已經考慮了所有\(y\)能到達的節點,所以如果\(y\)能到達\(x\),那麼\((x,y)\)不會是橫叉邊,所以\(y\)對\(low_x\)沒有貢獻
縮點法
-
將所有的強連通分量看做一個節點
-
將每個\(SCC\)的編號看做節點的編號,如果兩個強連通分量之間有有向邊,那麼在新圖中這兩個點之間連上同一個方向的邊即可
-
縮掉所有的環之後,就會得到一張DAG,我們就可以在上面做處理
程式碼實現
void tarjan(int x)
{
low[x]=dfn[x]=++poi;
suk[++top]=x;
ins[x]=1;
for(int i=head[x];i;i=e[i].last)
{
int y=e[i].to;
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(ins[y])
low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x])
{
sum++;
int y;
do{
y=suk[top--];
ins[y]=0;
c[y]=sum;
scc[sum].push_back(y);
}while(x!=y);
}
}
首先進行\(Tarjan\)求出所有的量,然後在回溯之前判斷一下即可
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
add(x,y);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i])
tarjan(i);
}
for(int x=1;x<=n;x++)
{
for(int i=head[x];i;i=e[i].last)
{
int y=e[i].to;
if(c[x]==c[y]) continue;
c_add(c[x],c[y]);
}
}
}
在主函式中,遍歷完一遍以後,開始列舉每個點的所有編號,根據有向圖的變得方向進行建邊
例題
P3387 P3388 P2341 P3469 P2194 P1262
P1262 P2002 P2746 P5058