题解[CF674D]Bearish_Fanpages
题意
原意简述版
有 \(n\) 个公司,每个公司在某社交媒体拥有一个粉丝专页。该社交媒体推出了“关注”功能,每个粉丝专页必须“关注”一个粉丝专页,保证不会有自己关注自己和互关的事情出现。第 \(i\) 个公司的粉丝专页有 \(v_i\) 位订阅者。每个粉丝专页上都有一些广告,分别来自:
- 该粉丝专页的所有者公司
- 该粉丝专页“关注”的粉丝专页的所有者公司
- “关注”该粉丝专页的粉丝专页的所有者公司
当第 \(i\) 个公司的粉丝专页上有 \(k\) 条广告时,每条广告都会被点击 \(\left\lfloor\dfrac {v_i}k\right\rfloor\) 次。每位订阅者会点一条广告,所以多余的还没有点广告的订阅者就会全部点该粉丝专页的所有者公司的广告。每一位订阅者的点击会为广告的所有者公司带来 \(1\) 的收益。现在要求你支持以下三个操作:
1 x y
让第 \(x\) 个公司的粉丝专页改为“关注”第 \(y\) 个公司的粉丝专页。2 x
查询假如每位订阅者按照上述规则进行一次点击,第 \(x\) 个公司的收益。3
查询假如每位订阅者按照上述规则进行一次点击,收益最低的公司的收益和收益最高的公司的收益。
压缩版
\(n\) 个带权点,每个点有一条出边。不会出现自环和互为出边指向点。要查询的值被称为收益。设和点 \(u\) 直接有边相连(与边的方向无关)的点数为 \(k_u\),点 \(u\) 的权值为 \(v_u\)。则每一个和 \(u\) 直接有边相连的点收益增加 \(\left\lfloor\dfrac {v_u}{k_u+1}\right\rfloor\),对自己贡献 \(v_u-k_u\times\left\lfloor\dfrac {v_u}{k_u+1}\right\rfloor\)。注意每个点只会在每个独立询问时对周围的点有收益贡献,收益不会保留到下次询问继续累加,而是会清空。三种操作:
1 x y
修改点 \(x\) 的出边指向点为 \(y\)。2 x
查询点 \(x\) 的收益。3
查询所有点中的最小收益和最大收益。
思路
暴力怎么做?修改时暴力修改对周围所有点的贡献,查询时直接输出或枚举所有点求最值。修改最劣 \(O(n)\)。
仔细考虑,发现这个 \(O(n)\) 来自于该点连接的边数,而边数是 \(O(n)\) 级别的。也就是说,随机下均摊是 \(O(1)\) 级别的。那我们有没有方法可以平衡复杂度呢?
要做到严格 \(O(1)\),可以想到要强制某点修改时只对相连的边中 \(O(1)\) 条边进行操作,其他的部分在查询时另行 \(O(1)\) 查询。
这时我们就发现每个点只有一条出边,看作基环树就可以理解为每个点只有一个父亲。那么我们就强制他只处理对其父亲的贡献,而对儿子的贡献等查询儿子时再计算。这样一来,每个点只有一个父亲则修改父亲 \(O(1)\),查询时也只需要加上父亲的贡献。这下就 \(O(n)\) 解决前两个操作了。(很多大佬能一眼看出这是个经典 trick:所有相连点都会造成贡献时,儿子的贡献和父亲的贡献分开计算。)
如何处理 3
操作?暴力开 set
维护或者暴力扫维护都不可行,怎么办?回到上面的思路,我们只计算儿子对自己的贡献可以做到 \(O(1)\) 修改。可是我们不能只看儿子贡献啊?每个点的父亲贡献不同,所以不能单纯用儿子贡献做关键字存 set
。考虑到一个点的所有儿子的父亲贡献相等,则自然而然想到将该点的儿子们的信息维护在该点的 set
中。那么我们只需把该点的 set
的最大最小取出,加上该点对儿子的贡献丢进全局 set
,就能做到 \(O(q\log n)\) 了。常数较大。
实现
注意到 \(n\) 比较小,时间充裕,我们以防万一,也为了代码简洁,维护最大最小值时,可以先把有关点的 set
中放入全局 set
中的元素删除,修改了该点的 set
后再取最大最小值放入全局 set
中。善于使用函数的话,可以写的比较简洁。
core code:
int n,m;
LL v[N+10],ic[N+10]; //value and income 权值与来自儿子的收益
int fa[N+10],sc[N+10]; //父亲与儿子数
multiset<LL> ch[N+10],mm; //每个点的儿子 set 与全局 set
inline void Addm(int u){ //将点 u 的 set 取最大最小值加上 u 对其贡献放入全局 set 中
if(sc[u]){
mm.insert(*ch[u].begin()+v[u]/(sc[u]+2));
if(sc[u]>1)
mm.insert(*--ch[u].end()+v[u]/(sc[u]+2));
}
}
inline void Delm(int u){ //同上函数,但是是删除
if(sc[u]){
mm.erase(mm.find(*ch[u].begin()+v[u]/(sc[u]+2)));
if(sc[u]>1)
mm.erase(mm.find(*--ch[u].end()+v[u]/(sc[u]+2)));
}
}
inline void Change(int u,LL x){ //修改一个点的来自儿子的收益
int f=fa[u];
Delm(f);
ch[f].erase(ch[f].find(ic[u]));
ic[u]+=x;
ch[f].insert(ic[u]);
Addm(f);
}
inline void Del(int u){ //断开 u 到父亲的边
int f=fa[u];
Delm(f);
ch[f].erase(ch[f].find(ic[u]));
Change(fa[f],v[f]/(sc[f]+1)-v[f]/(sc[f]+2));
Change(f,(sc[f]+1)*(v[f]/(sc[f]+2))-sc[f]*(v[f]/(sc[f]+1))-v[u]/(sc[u]+2));
--sc[f];
fa[u]=0;
Addm(f);
}
inline void Link(int u,int f){ //连接 u 到 f 的边,内容与上函数类似,相反而已
Delm(f);
fa[u]=f;
++sc[f];
Change(f,v[u]/(sc[u]+2)-(sc[f]+1)*(v[f]/(sc[f]+2))+sc[f]*(v[f]/(sc[f]+1)));
Change(fa[f],v[f]/(sc[f]+2)-v[f]/(sc[f]+1));
ch[f].insert(ic[u]);
Addm(f);
}
int main(){
n=Read(),m=Read();
for(int i=1;i<=n;++i)
v[i]=Read();
for(int i=1;i<=n;++i)
++sc[fa[i]=Read()];
for(int i=1;i<=n;++i)
ic[fa[i]]+=v[i]/(sc[i]+2),ic[i]+=v[i]-(sc[i]+1)*(v[i]/(sc[i]+2));
for(int i=1;i<=n;++i)
ch[fa[i]].insert(ic[i]);
for(int i=1;i<=n;++i)
Addm(i);
++m;
while(--m){
int op=Read();
if(op==1){
int x=Read(),y=Read();
Del(x);
Link(x,y);
}
else if(op==2){
int x=Read();
printf("%lld\n",ic[x]+v[fa[x]]/(sc[fa[x]]+2));
}
else
printf("%lld %lld\n",*mm.begin(),*--mm.end());
}
return 0;
}