Loading

树上游戏

前言

研究完 \(\rm{T2}\), 做树上游戏, 做树剖

关于淀粉质和尺取法

求线性数组上的区间点对问题是尺取法的典型应用, 求树上的点对问题是淀粉质的典型应用

对于每层点分治, 需要计算其它点对分治中心的贡献和经过分支中心的路径对其它每个点的贡献

思路

题意和淀粉质板子题其实挺像的, 换成了颜色种数, 点对换成了确定一个起始点
分开研究怎么做

要求输出每个节点的 \(sum_i\)

淀粉质模板中, 我们在统计距离为 \(k\) 的点对的时候, 是直接双指针把其它点对分治中心的贡献和经过分支中心的路径对其它每个点的贡献合起来算
本题中可以考虑在统计重心 \(p\) 的时候, 分开算出来上面两种情况, 各自加入各自答案
具体实现后面再研究

「颜色数量」怎么处理

考虑处理到重心 \(\rm{root}\)

处理 其它点 到 分治中心 的贡献

考虑颜色 \(k\) , 如果一个颜色为 \(k\) 的节点 \(v\) 为其到 \(\rm{root}\) 的路径中的第一个颜色 \(k\) , 则其对 \(\rm{root}\) 贡献为 \(size_v\)
具体的, 其子树中的点都可以这么走到 \(\rm{root}\) , 产生一个贡献

\(col_k = \sum size_v\) , \(sum = \sum col_k\)
那么 \(\rm{root}\) 增加答案 \(sum\) , 不难理解

顺手记录 \(tot_i\) 表示 \(i \to \rm{root}\) 中有多少种颜色
那么每个点初始答案为 \(tot_i \times (size_{\textrm{root}} - size_{\textrm{now}})\)
即对一个点来说, 随便一个子树外的点和他都可以产生这么多路径

处理 经过分支中心的路径 对 其它每个点 的贡献

考虑当前处理的子树, 如果其出现了第一次出现某种颜色的路径, 那么一定可以和子树外的其他子树中不存在这种颜色的路径凑在一起产生新的路径

也就是说, 我们利用上面处理的 \(col_k\) , 把当前子树中的贡献清除, 然后其他子树只要出现了当前节点到根不存在的颜色, 就可以顺手处理


好的好的, 看懂了, 直接开始进行一个总结

首先归纳到这个形式
pEMZneI.png

把现在需要计算的问题分开

  • \(\textrm{root}\) 到 子树中的点
    不难发现对于颜色为 \(k\) 的节点 \(r\) , 倘若 \(\textrm{root} \to r\) 上只有 \(r\) 颜色为 \(k\) , 那么其对 \(\textrm{root}\) 的贡献就是 \(size_r\)
  • 子树 到 子树外的点 \((\)经过 \(\textrm{root}\)\()\)
    不妨记录 \(\textrm{root} \to r\) 上只有 \(r\) 颜色为 \(k\)\(size_r\) 之和为 \(col_k\) , 记录 \(tot_r\) 表示 \(\textrm{root} \to r\) 中有多少种颜色
    那么如果不管这棵子树, 我们可以计算出两种情况
    • 子树到根节点 \(\&\) 到其他子树, 但是不出现新的颜色
      不难发现答案就是 \(tot_r \times (size_{\textrm{root}} - size_{\textrm{now}})\) , 也就是不在本子树上的点的数量
    • 子树到其他子树, 但是出现了新的颜色
      也就是说, 我们可以通过找到其他子树中的 \(col_k\) 来加入路径, 以此新增答案

会不会出现重复?
不会的, 因为我们避免统计同子树之中的点对贡献

实现

框架

首先处理完当前的重心, 一定要是 \(\mathcal{O} (size)\)

然后衔接到子问题的重心, 需要用递归的方法计算
具体怎么计算子问题的重心?
我们考虑进行 \(\rm{dp}\) , 首先重新计算新的 \(size\) , 然后更新最优的重心


处理当前的 \(\rm{dp}\) , 我们分成以下几个 \(\rm{dfs}\)

  • 处理 \(tot, col\) , 顺手更新 \(\textrm{root}\) 到 子树中的点的贡献
  • 处理 子树 到 其他子树 \((\)经过 \(\textrm{root}\)\()\)
    • 一遍 \(\rm{dfs}\) 去掉当前子树对 \(col\) 的贡献
    • 一遍 \(\rm{dfs}\) 处理答案
      • 处理没有新颜色出现的
      • 处理有新颜色出现的
    • 一遍 \(\rm{dfs}\) 加上当前子树对 \(col\) 的贡献
  • 清空使用过的数组

总复杂度是 \(\mathcal{O} (size)\)

代码

#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
const int N = 100005;
int gi()
{
    int x=0,w=1;char ch=getchar();
    while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
    if (ch=='-') w=0,ch=getchar();
    while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
struct edge{int to,next;}a[N<<1];
int n,c[N],head[N],cnt,sz[N],w[N],vis[N],sum,root;
int num[N],fst[N],col[N],tot[N];
ll sigma,ans[N];
void getroot(int u,int f)
{
    sz[u]=1;w[u]=0;
    for (int e=head[u];e;e=a[e].next)
    {
        int v=a[e].to;if (v==f||vis[v]) continue;
        getroot(v,u);
        sz[u]+=sz[v];w[u]=max(w[u],sz[v]);
    }
    w[u]=max(w[u],sum-sz[u]);
    if (w[u]<w[root]) root=u;
}
void dfs(int u,int f,ll &Ans)
{
    sz[u]=1;num[c[u]]++;
    if (num[c[u]]==1) fst[u]=1,cnt++;else fst[u]=0;
    tot[u]=cnt;Ans+=tot[u];
    for (int e=head[u];e;e=a[e].next)
    {
        int v=a[e].to;if (v==f||vis[v]) continue;
        dfs(v,u,Ans);sz[u]+=sz[v];
    }
    if (fst[u]) col[c[u]]+=sz[u],sigma+=sz[u],cnt--;
    num[c[u]]--;
}
void change(int u,int f,int b)
{
    if (fst[u]) col[c[u]]+=b*sz[u],sigma+=b*sz[u];
    for (int e=head[u];e;e=a[e].next)
    {
        int v=a[e].to;if (v==f||vis[v]) continue;
        change(v,u,b);
    }
}
void calc(int u,int f,int k)
{
    if (fst[u]) sigma-=col[c[u]];
    ans[u]+=1ll*tot[u]*k+sigma;
    for (int e=head[u];e;e=a[e].next)
    {
        int v=a[e].to;if (v==f||vis[v]) continue;
        calc(v,u,k);
    }
    if (fst[u]) sigma+=col[c[u]];
}
void clear(int u,int f)
{
	col[c[u]]=0;
	for (int e=head[u];e;e=a[e].next)
	{
		int v=a[e].to;if (v==f||vis[v]) continue;
		clear(v,u);
	}
}
void solve(int u)
{
    vis[u]=1;
    dfs(u,0,ans[u]);
    col[c[u]]-=sz[u];sigma-=sz[u];
    for (int e=head[u];e;e=a[e].next)
    {
        int v=a[e].to;if (vis[v]) continue;
        change(v,0,-1);
        calc(v,0,sz[u]-sz[v]);
        change(v,0,1);
    }
    clear(u,0);sigma=0;
    for (int e=head[u];e;e=a[e].next)
    {
        int v=a[e].to;if (vis[v]) continue;
        sum=sz[v];
        root=0;
        getroot(v,0);
        solve(root);
    }
}
int main()
{
    n=gi();
    for (int i=1;i<=n;i++) c[i]=gi();
    for (int i=1;i<n;i++)
    {
        int u=gi(),v=gi();
        a[++cnt]=(edge){v,head[u]};head[u]=cnt;
        a[++cnt]=(edge){u,head[v]};head[v]=cnt;
    }
    sum=w[0]=n;cnt=0;
    getroot(1,0);
    solve(root);
    for (int i=1;i<=n;i++) printf("%lld\n",ans[i]);
    return 0;
}

自己的还在调, 先放一放

总结

本题按照颜色拆贡献更好理解
拆掉贡献之后, 不难想到把路径分成 有新颜色加入 / 无新颜色加入 两种

静态点分治研究的主要问题就是如何在 \(\mathcal{O} (size)\) 的复杂度中处理子问题
一般可以归纳为

  • 处理 其它点 到 分治中心 的贡献
  • 处理 经过分支中心的路径 对 其它每个点 的贡献

树上不好计算的问题, 往往转化成可以递推的算法

posted @ 2025-02-17 21:07  Yorg  阅读(11)  评论(0)    收藏  举报