CF960H 题解

题意简述

注:原题翻译有点小锅,题目中的 bc 应该是一个东西。本文暂且写作 c

给出一棵 n(2n5×104) 个节点的有根树,根为 1。点 2n 的父亲为 p2pn。有 m(1m5×104) 种颜色。点 i 的颜色为 fi(1fim),颜色 i 的权值为 ci(1ci100)

有两种操作共 q(1q×5×104) 次:

  • x y,将 fxy

  • x,从树中等概率选取一个点 i,得到 (SicxC)2 的价值。求期望价值。其中 Si 表示以 i 为根的子树中颜色为 x 的节点数。C 是给定的常数。

题目分析

一个比较裸的树剖,甚至只有路径修改单点查询。

先分析操作 2。这一询问实际上是求 1ni=1n(SicxC)2。直接维护不太可行,因此我们对它进行一下转换,把不同次数的项分解开来维护(类似于方差这个题):

    1ni=1n(SicxC)2

=1ni=1n(Si2cx22SicxC+C2)

=1ni=1nSi2cx21ni=1n2SicxC+C2

=cx2ni=1nSi22cxCni=1nSi+C2

cxC 都是固定常数,也就是说我们只用维护 i=1nSi2i=1nSi 就可以,这两个量的合并都十分直观(还是类似方差这个题)。

再看操作 1。这一修改实质上只涉及到从 x 到根的路径上的结点,就是典型的路径修改。只需要把这条路径上旧颜色的 S1,新颜色的 S1 即可。

注意到颜色数不多,很容易想到重链剖分后直接维护 m 棵线段树。但是构造完整的线段树结构肯定会 MLE(空间复杂度 O(nm)),所以考虑动态开点。空间复杂度降到 O(mlog2n),此题空间限制最大能开到 O(1.6×107),已经足够了。

代码实现

#include<bits/stdc++.h>
using namespace std;
int n,m,q,op,x,y,f[50010],b[50010],a[50010],nd,num[50010];
int tot,hd[50010],nt[50010],v[50010];
int cnt,d[50010],top[50010],fa[50010],dfn[50010],son[50010],size[50010];
long long C;
struct node
{
int ls,rs;//左右子结点编号。
long long sqsum,sum,tag;//平方和、和、懒标记。
}tr[16000010];//线段树结点结构体
void rd(int &x)
{
x=0;
char c=getchar();
for(;c>'9'||c<'0';c=getchar());
for(;c<='9'&&c>='0';c=getchar())
x=(x<<3)+(x<<1)+c-'0';
}//快读
void rd(long long &x)
{
x=0ll;
char c=getchar();
for(;c>'9'||c<'0';c=getchar());
for(;c<='9'&&c>='0';c=getchar())
x=(x<<3ll)+(x<<1ll)+c-'0';
}//快读
void add(int x,int y)
{
v[++tot]=y;
nt[tot]=hd[x];
hd[x]=tot;
}//建边
void dfs1(int x)
{
size[x]=1;
d[x]=d[fa[x]]+1;
for(int i=hd[x];i;i=nt[i])
{
int y=v[i];
dfs1(y);
size[x]+=size[y];
if(size[y]>size[son[x]])
son[x]=y;
}
}//第一遍 DFS,计算出结点深度 d、子树大小 size 并找到重儿子 son。
void dfs2(int x,int tp)
{
top[x]=tp;
dfn[x]=++cnt;
if(!son[x])
return;
dfs2(son[x],tp);
for(int i=hd[x];i;i=nt[i])
{
int y=v[i];
if(y!=son[x])
dfs2(y,y);
}
}//第二遍 DFS,计算出重链顶 top、遍历顺序 dfn。
void pushup(int p)
{
tr[p].sqsum=tr[tr[p].ls].sqsum+tr[tr[p].rs].sqsum;//平方和更新
tr[p].sum=tr[tr[p].ls].sum+tr[tr[p].rs].sum;//和更新
}//用子结点更新父结点。
void addtag(int &p,long long val,int len)
{
if(!p)
p=++nd;//原来没有就新建。
tr[p].sqsum+=2ll*tr[p].sum*val+1ll*len*val*val;//平方和更新
tr[p].sum+=1ll*len*val;//和更新
tr[p].tag+=val;//标记更新
}//打标记。
void pushdown(int &p,int len)
{
if(tr[p].tag)
{
addtag(tr[p].ls,tr[p].tag,(len+1)/2);//左儿子
addtag(tr[p].rs,tr[p].tag,len/2);//右儿子
tr[p].tag=0;//清空标记
}
}//下传懒标记。
void change(int ql,int qr,int &p,int l,int r,long long val) //ql:当前结点左端点,qr:当前结点右端点,p:当前结点,l:询问左端点,r:询问右端点,val:更新的值。
{
if(!p)
p=++nd;//原来没有就新建。
if(ql>=l&&qr<=r)//全包含直接修改。
{
addtag(p,val,qr-ql+1);
return;
}
pushdown(p,qr-ql+1);
int mid=ql+qr>>1;
if(mid>=l)
change(ql,mid,tr[p].ls,l,r,val);//左子结点
if(mid<r)
change(mid+1,qr,tr[p].rs,l,r,val);
pushup(p);
}//线段树区间修改。
void change_to_root(int x,int col,long long val)//从 x 修改到根。
{
while(top[x]!=1)
{
change(1,n,num[col],dfn[top[x]],dfn[x],val);
x=fa[top[x]];//一条重链一条重链地改
}
change(1,n,num[col],1,dfn[x],val);
}
int main()
{
rd(n),rd(m),rd(q),rd(C);
for(int i=1;i<=n;i++)
rd(a[i]);
for(int i=2;i<=n;i++)
rd(fa[i]),add(fa[i],i);
for(int i=1;i<=m;i++)
rd(b[i]);
dfs1(1);
dfs2(1,1);
for(int i=1;i<=n;i++)
change_to_root(i,a[i],1);//初始化
while(q--)
{
rd(op),rd(x);
if(op==1)
{
rd(y);
change_to_root(x,a[x],-1);//把原来颜色的删掉 1
a[x]=y;
change_to_root(x,a[x],1);//新颜色加上 1
}
else
printf("%.10lf\n",(double)(b[x]*b[x]*tr[num[x]].sqsum-2*b[x]*C*tr[num[x]].sum)/n+C*C);
}
return 0;
}
posted @   Hadtsti  阅读(6)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示