1. 程式人生 > 其它 >題解 P4630 [APIO2018] Duathlon 鐵人兩項

題解 P4630 [APIO2018] Duathlon 鐵人兩項

圓方樹模板題

一道圓方樹模板題

推薦閱讀/參考資料 圓方樹學習筆記——小粉兔

題意簡述

給定一張簡單無向圖,問有多少對三元組 \(<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\) 為它的子樹中的點,此時貢獻為
\[2\times \sum size_j\times (size_i-size_j)\times w_i \]
  • \(j\)\(i\) 子樹中的節點

圖示為 \(j=2\)\(i=1\) 時,紅色為 \(size_2\) ,藍色為 \(size_1-size_2\)

,相乘即為節點 \(2\) 作為節點 \(1\) 的子節點時對 \(1\) 的貢獻。

  • \(s,f\) 有且僅有一個為 \(i\) 的子節點時,所有點 \(j\) 產生的總貢獻為:
\[2\times size_i\times (num-size_i)\times w_i \]
  • 其中 \(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;
}