點雙連通分量
我們都知道有向圖的強連通分量叫 scc,無向圖也有一種叫雙連通分量的連通分量,分為點雙(v-dcc)&邊雙(e-dcc)。
-
一個圖為點雙連通圖等價於對於任意兩個不同的點 \(u,v\),存在兩條(除端點外)不相交的從 \(u\) 到 \(v\) 的簡單路徑。特別地,僅存在兩個點和一條連接它們的邊的圖也是點雙連通圖。
- 即不存在割點的無向連通圖。
-
一個圖為邊雙連通圖等價於每條邊都在某一個簡單環中。
- 即不存在割邊的無向連通圖。
-
一個圖的極大點雙聯通子圖為它的一個點雙連通分量。
-
一個圖的極大邊雙聯通子圖為它的一個邊雙連通分量。
-
當一個圖被划分為 v-dcc 和 e-dcc 時,相鄰兩個 e-dcc 之間沒有公共點,分開它們的是割邊;相鄰兩個 v-dcc 之間有一個公共點,該點為割點。換句話說,一個點可以存在於多個點雙連通分量中。
圓方樹
對於一個無向連通圖,求出每一個 v-dcc 后,為每一個 v-dcc 建立一個新點,將 v-dcc 內的所有點與該新點相連。則該新點為方點,原圖中的點為圓點,由新邊構成的新圖將成為一顆樹狀結構,發明它的陳俊琨叫它圓方樹。非連通圖將構成森林。圓方樹可以將圖轉為樹,從而實現樹上操作。
【例1】[APIO2018]Duathlon鐵人兩項
求不一定連通的簡單無向圖中,滿足「存在一條路徑 \(s\to f\) 經過 \(c\) 」的 \(\lang s,c,f\rang\) 的個數。\(\lang s,c,f\rang\) 和 \(\lang f,c,s\rang\) 算不同的元組。
點雙的性質:對於一個點雙連通分量中的兩個點 \(u,v\),從 \(u\) 到 \(v\) 的所有路徑的並為點雙連通分量的點集。證明略。
當我們建立圓方樹之后,求固定的 \(s\to f\) 的 \(c\) 的個數等於與「從圓點 \(s\) 到圓點 \(f\) 上的所有方點」相連的圓點的個數 \(-2\)。\(-2\) 是因為 \(s,f\) 不算。
遇到用圓方樹統計路徑數的問題,重要做法是給每一個方點和圓點賦權值。
顯然,每個方點應賦值為其所代表的點雙節點數。所以圓點賦值 \(-1\),以去重兩個點雙的公共部分。
這樣,樹上 \(s\to f\) 的點權值和就代表 \(c\) 的個數。注意,不需要再 \(-2\),道理顯然。
問題轉化為:
求樹上所有有序圓點點對間路徑點權和之和。
這是一個簡單的問題,只需要用“統計貢獻”的角度計算即可。由於文字不方便描述,見下面代碼:
void dfs(int x,int p){
vis[x]=1; //vis[x]標記x到達,不重要
ll sum=0; //sum統計有多少組點對。在這里先不考慮順序,因為s!=f所以最后答案*2即可。
//第12行及以前sum統計的都是x子樹內的路徑條數
for(int i=0;i<T[x].size();i++){
int y=T[x][i];
if(y==p)continue;
dfs(y,x);
sum+=1ll*siz[y]*siz[x]; //這里的siz[x]還是已經遍歷過的兒子節點的子樹中圓點個數的總和
siz[x]+=siz[y];
}
if(x<=n)sum+=siz[x],siz[x]++; //x<=n代表x是圓點,那么由x連向
sum+=1ll*siz[x]*(tmp-siz[x]); //由x子樹內的圓點連向x子樹外的圓點必經過x;tmp是連通塊內節點個數(因為圖不連通)
f[x]*=sum; //f[x]會事先存住x的點權
ans+=f[x];
}
代碼:
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
int n,m,t,dfc,cnt,tmp,stk[N],dfn[N],low[N],siz[N*2],vis[N*2];
ll ans,f[N*2];
vector<int>G[N],T[N*2];
void Tarjan(int x){ //注:圓方樹的Tarjan嚴格講不完全是v-dcc,因x->px的邊也被low使用,即會讓兩個孤立的點之間也用方點串起
stk[++t]=x;
dfn[x]=low[x]=++dfc;
for(int i=0;i<G[x].size();i++){
int y=G[x][i];
if(!dfn[y]){
Tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]==dfn[x]){
cnt++;
while(t){
T[cnt].push_back(stk[t]),T[stk[t]].push_back(cnt);
f[cnt]++;
if(stk[t--]==y)break;
}
T[cnt].push_back(x),T[x].push_back(cnt);
f[cnt]++;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
void dfs1(int x,int p){
tmp+=x<=n;
for(int i=0;i<T[x].size();i++){
int y=T[x][i];
if(y^p)dfs1(y,x);
}
}
void dfs(int x,int p){
vis[x]=1;
ll sum=0;
for(int i=0;i<T[x].size();i++){
int y=T[x][i];
if(y==p)continue;
dfs(y,x);
sum+=1ll*siz[y]*siz[x];
siz[x]+=siz[y];
}
if(x<=n)sum+=siz[x],siz[x]++;
sum+=1ll*siz[x]*(tmp-siz[x]);
f[x]*=sum;
ans+=f[x];
}
int main(){
scanf("%d%d",&n,&m),cnt=n;
for(int i=1,u,v;i<=m;i++)scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
for(int i=1;i<=n;i++)f[i]=-1;
for(int i=1;i<=cnt;i++)if(!vis[i])Tarjan(i),tmp=0,dfs1(i,0),dfs(i,0);
cout<<ans*2;
}
【例2】戰略游戲
一個 \(n\) 點 \(m\) 邊無向圖,有 \(q\) 次詢問,每次給出點集 \(S\),求有多少個 \(c\notin S\),使得存在 \(u,v\in S(u\ne v)\) 滿足刪去 \(c\) 及與之相連的邊后 \(u,v\) 不連通。多組數據。
這個題思維難度其實很低,連我都能獨立想出來,是圓方樹的套路題。
連通性問題可以想關於 Tarjan 的算法。通過點雙的定義和性質可以發現,由於 \(u\) 到 \(v\) 的所有路徑之並等於與圓方樹上的所有方點相連的圓點集,則 \(u\) 到 \(v\) 的路徑上的圓點為必經點。而必經點就是我們要求的點。所以如果固定了 \(u,v\),那么答案就是圓方樹上 \(u\sim v\) 路徑上面圓點個數(不含自己)。
而我們要求的是 \(S\) 中的 \(u,v\) 兩兩之間的路徑並起來形成的那一棵樹里所有圓點個數(不含\(S\))。
很好處理,因為我們都知道一個常見的結論就是把一棵樹的每兩個相鄰葉子之間的距離加起來等於樹邊總權值÷2(第一個和最后一個的葉子也相鄰)。記得一道洛谷月賽題就是用的這個結論可以輕松過掉。
這樣一來就很好想了,我們只需要把相鄰的兩個\(S\)中元素之間的答案(圓點個數,不含自己)加起來除以二即可。但是,什么是“相鄰”呢?當然不是指的在\(S\)中相鄰,而是在那棵我們想像中的樹里面dfn相鄰。不難想到其實這是等價於在現在這棵樹里dfn相鄰的。所以只需要事先對\(S\)按dfn排序即可通過½∑ask(S[i],S[i-1])來求得。具體實現采用樹鏈剖分。
但把我卡了一下的地方是要注意我們的結論是基於邊的,不是點權的,因此需要用邊權的角度考慮,那么就讓x到fa[x]的邊權代表x的點權,在此基礎上執行上面過程;最后,再加上lca(s1,s2,...)是不是圓點(同時特判lca是否屬於s)。注意,維護邊的樹鏈剖分return的時候是不能算最高點的值的。
但是這樣還沒有考慮完,原因在於你該怎么實現“S中的點不算”。是直接每次-2嗎?這是不對的,因為有些路徑在路徑中央還有S中的點。我們需要在一開始的時候就把所有S中點的點權置為0(該次詢問結束后再恢復)。
便可以AC了。
v-dcc&e-dcc std
//e-dcc
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,tp,dfc,cnt,stk[N],dfn[N],low[N],a[N],b[N];
vector<int>G[N];
void Tarjan(int x,int p){
dfn[x]=low[x]=++dfc;
stk[++tp]=x;
for(int i=0;i<G[x].size();i++){
int y=G[x][i];
if(!dfn[y]){
Tarjan(y,x);
low[x]=min(low[x],low[y]);
}
else if(y^p)low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]||!p){
cnt++;
while(tp){
b[cnt]^=a[stk[tp]];
if(stk[tp--]==x)break;
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;i++){
scanf("%d%d",&u,&v);
G[u].push_back(v),G[v].push_back(u);
}
for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i,0);
cout<<cnt;
}
//v-dcc
void Tarjan(int x,int p){
stk[++t]=x;
dfn[x]=low[x]=++dfc;
for(int i=0;i<G[x].size();i++){
int y=G[x][i];
if(!dfn[y]){
Tarjan(y,x);
low[x]=min(low[x],low[y]);
if(low[y]==dfn[x]){
cnt++;
while(t){
T[cnt].push_back(stk[t]),T[stk[t]].push_back(cnt);
f[cnt]++;
if(stk[t--]==y)break;
}
T[cnt].push_back(x),T[x].push_back(cnt);
f[cnt]++;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
推薦題目(v-dcc&e-dcc)
CF639F Bear and Chemistry
給定一張 \(n\) 個點 \(m\) 條邊的初始無向圖。
\(q\) 次詢問,每次詢問給定一個點集 \(V\) 和邊集 \(E\)。
你需要判斷,將 \(E\) 中的邊加入初始無向圖之后,\(V\) 中任意兩個點 \(x,y\) 是否都能在每條邊至多經過一次的情況下從 \(x\) 到 \(y\) 再回到 \(x\)。
\(n,m,q,\sum |V|, \sum |E| \le 3 \times 10^5\),強制在線。
題目等價於問每次加入若干條邊后,一個點集內的點是不是屬於同一個邊雙。
如果兩個點在初始圖當中兩個點就屬於同一個邊雙,那么詢問時也屬於,不難想到最開始就跑一次邊雙縮點,詢問時再加邊在新圖跑邊雙、判連通塊。但這樣做是 \(O(nq)\) 的,考慮每次對縮點后的初圖建出虛樹再加邊、跑邊雙,不難發現建虛樹是不會影響連通性的。
你會直接地感受到此題代碼量應該不小,所以下面的一些注意事項或許能幫你節省調試時間:
- 清空新圖時一定要清空
dfn
。 - 【虛樹+加入 \(E\)】的圖可能會出現重邊(不是你虛樹建錯了,而是 \(E\) 中的邊對應在虛樹上是重邊),這種情況下不要在
tarjan
函數中直接把兩條當成一條。另外原圖中也可能有重邊。 - 一定要注意什么時候是
x
,什么時候是bel[x]
,甚至是新圖的bel[x]
還是初圖的bel[x]
。 rotate
函數的 \(R\) 要開 LL