洛谷4630 BZOJ APIO2018 鐵人兩項 圓方樹 dp (圓方樹學習筆記)
題目連結
題意:給你一個n個點m條邊的無向圖,求所有的能從s到c再到t的三元組個數,其中每個點在一條路徑上至多經過一次。n,m1e5量級。
題解:
首先介紹一下圓方樹。
還記得zyb大佬憑藉圓方樹在APIO拿AU並在SD二輪進隊,近年來圓方樹也成為了一個熱門演算法,於是還是很有必要學的。
圓方樹的作用是把一個圖變成一個樹,並且能正確地記錄一些資訊。
把一個圖變成一棵樹的方法是我們首先利用tarjan,找到所有的點雙連通分量。點雙連通分量是值去掉圖上任何一個點都不會改變連通性的連通分量。我們對於每一個點雙連通分量,我們建一個新的點,表示整個點雙,這種點成為方點,並且可能會在方點記錄整個點雙的資訊,原圖上的點成為圓點。特殊地,把兩個點互相連通的也視作一個點雙。對於這些點,我們的連邊方式是把每一個圓點與他所在的每一個點雙代表的方點連邊。當然圓方樹上的方點所維護的資訊可能不包含在圓方樹上處於這個方點父節點的位置的圓點的資訊。
用網上別人的一張圖,可能比較有助於理解。
這樣對一個圖建出圓方樹之後我們就可以解決一些圖上簡單路徑的問題了。似乎還經常用來解決一些仙人掌上的問題。圓方樹題目經常需要在LCA處分圓點和方點分類討論。
圓方樹的一個性質是所有方點相連的點一定都是圓點,所有圓點相連的點一定都是方點。
另一個性質是無論以哪個點為根,圓方樹的形態不變。
下面開始寫這個題的做法。
我們會發現,如果在一個點雙內選擇兩個點,那麼任意地在點雙裡再選一個點作為中間點,一定能形成一個合法的三元組。如果兩個不在同一個點雙裡的點的路徑上經過了某個點雙,那麼把這個點雙上的任意的一個點作為中間點形成的三元組都是合法的。那麼我們考慮每一個點作為這個中間點對答案的貢獻。
我們發現答案相當於對於每一個
,求有多少
是合法的。
我們對原圖建出圓方樹,樹上圓點的點權是
,方點的點權是度數,也就是點雙的大小,這裡算是用到一點容斥的思想。這樣之後
和
之間的點數是就變成了圓方樹上兩點之間的權值和。我們設
為
的子樹內無序圓點對的路徑權值和之和,
為
到
的子樹內所有圓點的距離和之和,
為以
為根的子樹內圓點的個數,
表示
的權值。我們需要有順序的轉移來保證我們每個量轉移前後的正確性。我們有以下轉移式:
由於圖可能不連通,並且我們求的是無序點對(x,y)的答案,所以最終的結果是
然後就做完了。複雜度是 。
程式碼:
#include <bits/stdc++.h>
using namespace std;
int n,m,hed[400010],cnt,ct,hed2[500010],cnt2,z,dfn[400010];
int low[400010],sta[400010],tp;
long long ans,f[400010],sum[400010],g[400010],sz[400010];
struct node
{
int to,next;
}a[400010],aa[400010];
inline void add(int from,int to)
{
a[++cnt].to=to;
a[cnt].next=hed[from];
hed[from]=cnt;
}
inline void add2(int from,int to)
{
aa[++cnt2].to=to;
aa[cnt2].next=hed2[from];
hed2[from]=cnt2;
}
inline void tarjan(int x)
{
low[x]=dfn[x]=++z;
sta[++tp]=x;
for(int i=hed[x];i;i=a[i].next)
{
int y=a[i].to;
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
if(dfn[x]<=low[y])
{
++ct;
do
{
add2(ct,sta[tp]);
add2(sta[tp],ct);
--tp;
sz[ct]++;
}while(sta[tp+1]!=y);
add2(ct,x);
add2(x,ct);
++sz[ct];
}
}
else
low[x]=min(low[x],dfn[y]);
}
}
inline void dfs(int x,int fa)
{
for(int i=hed2[x];i;i=aa[i].next)
{
int y=aa[i].to;
if(y!=fa)
dfs(y,x);
}
if(x<=n)
{
sum[x]=1;
g[x]=sz[x];
}
for(int i=hed2[x];i;i=aa[i].next)
{
int y=aa[i].to;
if(y!=fa)
{
f[x]+=f[y]+g[x]*sum[y]+g[y]*sum[x];
g[x]+=g[y]+sum[y]*sz[x];
sum[x]+=sum[y];
}
}
}
int main()
{
scanf("%d%d",&n,&m);
ct=n;
for(int i=1;i<=m;++i)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for(int i=1;i<=n;++i)
sz[i]=-1;
for(int i=1;i<=n;++i)
{
if(!dfn[i])
{
tarjan(i);
dfs(i,0);
ans+=f[i]*2;
}
}
printf("%lld\n",ans);
return 0;
}