题解 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\) 的贡献。

QQ截图20211008212447.png

  • \(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;
}
posted @ 2021-10-10 21:05  Sure042  阅读(43)  评论(0编辑  收藏  举报