P5290 [十二省联考2019]春节十二响
考虑一个子树里是怎么划分的,维护划分出来的每个集合的最大值,这个可以用一个 $multiset$ 维护
设 $S[x]$ 表示节点 $x$ 的子树中,最优划分 划分出来的每个块的节点最大值
首先叶子节点的集合显然只有它本身
然后考虑子树之间的合并,设两个子树根节点为 $x,y$,因为两个子树之间一定不会有祖先后代关系
贪心地想,显然 $S[x]$ 的最大值优先跟 $S[y]$ 的最大值合并(取 $max$),然后次大值跟次大值合并...这样一路合并下去是最优的
所以直接启发式合并就好了
$x$ 的子树合并完后还要考虑当前节点 $x$ 也加入进来,显然此节点只能单独分一个块出来
一开始以为复杂度 $O(nlog^2_n)$(启发式合并 $nlog_n$,$multiset$ 单次操作 $log_n$)
但是经过对代码的分析可以发现,这并不是直接合并,而是小的 $S$ 和大的取一个最大值后就没了,并没有增加大的集合的集合大小
所以每个节点只会算一次,复杂度 $O(nlog_n)$
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #include<set> using namespace std; typedef long long ll; inline int read() { int x=0; char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x; } const int N=4e5+7; int fir[N],from[N<<1],to[N<<1],cntt; inline void add(int a,int b) { from[++cntt]=fir[a]; fir[a]=cntt; to[cntt]=b; } int n,val[N]; multiset <int> S[N]; multiset <int>::iterator it,pit,itt; inline void merge(int x,int y)//很多细节的启发式合并操作 { if(S[x].size()<S[y].size()) swap(S[x],S[y]);//启发式合并 if(!S[y].size()) return;//记得特判,不然后面it--时会直接GG pit=S[x].end(); pit--; bool flag=1; it=S[y].end(); it--; while(it!=S[y].begin()) { pit--; itt=pit; pit++; //注意要先用itt存一下pit-1的指针等等erase(pit)后pit就是指向不合法地址的指针了,那时再pit--就GG了 if((*it)>(*pit)) S[x].erase(pit),S[x].insert(*it); it--; pit=itt; } if((*it)>(*pit)) S[x].erase(pit),S[x].insert(*it);//注意最后S[y].begin()的值还没合并 } void dfs(int x) { for(int i=fir[x];i;i=from[i]) dfs(to[i]),merge(x,to[i]); S[x].insert(val[x]); } int main() { n=read(); int a; for(int i=1;i<=n;i++) val[i]=read(); for(int i=2;i<=n;i++) a=read(),add(a,i); dfs(1); ll ans=0; for(it=S[1].begin();it!=S[1].end();it++) ans+=(*it); printf("%lld",ans); return 0; }