CF960H 题解
题意简述
注:原题翻译有点小锅,题目中的 $b$ 和 $c$ 应该是一个东西。本文暂且写作 $c$。
给出一棵 $n(2\le n\le 5\times 10^4)$ 个节点的有根树,根为 $1$。点 $2\sim n$ 的父亲为 $p_2\sim p_n$。有 $m(1\le m\le 5\times 10^4)$ 种颜色。点 $i$ 的颜色为 $f_i(1\le f_i\le m)$,颜色 $i$ 的权值为 $c_i(1\le c_i\le 100)$。
有两种操作共 $q(1\le q\times 5\times 10^4)$ 次:
-
$\texttt{1 }x\text{ }y$,将 $f_x\leftarrow y$。
-
$\texttt{2 }x$,从树中等概率选取一个点 $i$,得到 $(S_ic_x-C)^2$ 的价值。求期望价值。其中 $S_i$ 表示以 $i$ 为根的子树中颜色为 $x$ 的节点数。$C$ 是给定的常数。
题目分析
一个比较裸的树剖,甚至只有路径修改单点查询。
先分析操作 $2$。这一询问实际上是求 $\displaystyle \frac{1}{n}\sum_{i=1}^n(S_ic_x-C)^2$。直接维护不太可行,因此我们对它进行一下转换,把不同次数的项分解开来维护(类似于方差这个题):
$\ \ \ \ \displaystyle\frac{1}{n}\sum_{i=1}^n(S_ic_x-C)^2$
$\displaystyle=\frac{1}{n}\sum_{i=1}^n(S_i^2c_x^2-2S_ic_xC+C^2)$
$\displaystyle=\frac{1}{n}\sum_{i=1}^nS_i^2c_x^2-\frac{1}{n}\sum_{i=1}^n2S_ic_xC+C^2$
$\displaystyle=\frac{c_x^2}{n}\sum_{i=1}^nS_i^2-\frac{2c_xC}{n}\sum_{i=1}^nS_i+C^2$
$c_x$ 和 $C$ 都是固定常数,也就是说我们只用维护 $\displaystyle\sum_{i=1}^nS_i^2$ 和 $\displaystyle\sum_{i=1}^nS_i$ 就可以,这两个量的合并都十分直观(还是类似方差这个题)。
再看操作 $1$。这一修改实质上只涉及到从 $x$ 到根的路径上的结点,就是典型的路径修改。只需要把这条路径上旧颜色的 $S$ 减 $1$,新颜色的 $S$ 加 $1$ 即可。
注意到颜色数不多,很容易想到重链剖分后直接维护 $m$ 棵线段树。但是构造完整的线段树结构肯定会 MLE(空间复杂度 $O(nm)$),所以考虑动态开点。空间复杂度降到 $O(m \log^2 n)$,此题空间限制最大能开到 $O(1.6\times 10^7)$,已经足够了。
代码实现
#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;
}