題解 P4630 [APIO2018] Duathlon 鐵人兩項
阿新 • • 發佈:2021-10-10
圓方樹模板題
,相乘即為節點 \(2\) 作為節點 \(1\) 的子節點時對 \(1\) 的貢獻。
一道圓方樹模板題
推薦閱讀/參考資料 圓方樹學習筆記——小粉兔
題意簡述
給定一張簡單無向圖,問有多少對三元組 \(<s,c,f>\) (互不相同)使得存在一條簡單路徑從 \(s\) 出發,經過 \(c\) 到達 \(f\) 。
思路
每一對固定的從起點 \(s\) 到終點 \(f\) 的 \(c\) 的數量顯然相當於這兩點間所有簡單路徑上的點數之和減去 $2 (s,f $ 本身 \()\) ,但對於題目中的無規律圖(無規律在於含有環、可能為森林等),這樣的點數顯然不好求,所以我們想到了圓方樹。
圓方樹可以做到把簡單無向圖轉換為我們熟悉的樹結構,從而進行一些樹上的操作,所以我們在遇到這種圖時會想到圓方樹
利用差分的思想,我們將圓方樹上的每個圓點權值賦為 \(-1\) ,每個方點的權值賦為其點雙的大小,最終答案轉化為:
\[\sum w_i,i\in T \]( \(T\) 為圓方樹, \(G\) 為原圖)
考慮到每個點 \(i\) 作為中轉點 \(c\) ,它的貢獻為以下兩種情況之和:
- \(s\) 與 \(f\) 都 為它的子樹中的點,此時貢獻為
- \(j\) 為 \(i\) 子樹中的節點
圖示為 \(j=2\) , \(i=1\) 時,紅色為 \(size_2\) ,藍色為 \(size_1-size_2\)
- \(s,f\) 有且僅有一個為 \(i\) 的子節點時,所有點 \(j\) 產生的總貢獻為:
- 其中 \(num\) 為圓方樹總節點數。
需要注意的是: \(\text{dfs}\) 遍歷每個點作為中轉點時,子樹中的節點既可作為起點也可作為終點,所以它的貢獻要乘 \(2\) 。
這樣做的好處在於在統計貢獻時不用專門區分方點和圓點,只需通過特殊點權 \(w\) 就可以實現統計,比較好理解。
具體實現看程式碼
\(Code\)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+7,M=2e5+7;//圓方樹點數記得開雙倍
int w[N];//點權
int n,m;
inline void read(int &x)
{
char ch=getchar();x=0;
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int fst[N],tot=0;
int fst1[N],tot1=0;
struct rec{int to,nxt;}e[M<<1];
struct rec1{int to,nxt;}e1[M<<1];
inline void add(int u,int v,bool o)
{
if(!o)
{
e[++tot].to=v;
e[tot].nxt=fst[u];
fst[u]=tot;
e[++tot].to=u;
e[tot].nxt=fst[v];
fst[v]=tot;
}
else
{
e1[++tot1].to=v;
e1[tot1].nxt=fst1[u];
fst1[u]=tot1;
e1[++tot1].to=u;
e1[tot1].nxt=fst1[v];
fst1[v]=tot1;
}
}
int dfn[N],low[N],st[N];
int sub=0,cnt=0,top=0,num=0;
int min(int a,int b){return a<b?a:b;}
void Tarjan(int u)//圓方樹經典操作
{
dfn[u]=low[u]=++sub,st[++top]=u;
num++;
for(int i=fst[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(!dfn[v])
{
Tarjan(v);
low[u]=min(low[v],low[u]);
if(low[v]==dfn[u])
{
w[++cnt]=1;//更新方點權值
while(st[top]!=v)
{
w[cnt]++;
add(cnt,st[top],1);
top--;
}
add(cnt,u,1);
top--,w[cnt]++,add(v,cnt,1);
}
}
else
low[u]=min(dfn[v],low[u]);
}
}
ll ans=0;
int size[N<<1];
void dfs(int x,int fa)
{
size[x]=(x<=n);//方點不具實際大小
for(int i=fst1[x];i;i=e1[i].nxt)
{
int y=e1[i].to;
if(y==fa) continue;
dfs(y,x);
ans+=(ll)2*(ll)size[x]*(ll)size[y]*(ll)w[x];//節點作為x子節點的貢獻,注意此時的size[x]在未加上size[y]前與size[y]相乘 size[x]+=size[y];
}
ans+=(ll)2*(ll)size[x]*(ll)(num-size[x])*(ll)w[x];//子樹內的節點與外部節點作為s,f的貢獻
}
int main()
{
read(n),read(m);
cnt=n;
for(int i=1;i<=n;i++) w[i]=-1;
for(int i=1,x,y;i<=m;i++)
{
read(x),read(y);
add(x,y,0);
}
for(int i=1;i<=n;i++)//注意處理森林
if(!dfn[i])
{
num=0;
Tarjan(i);
--top;
dfs(i,0);
}
cout<<ans;
return 0;
}