题目大意:
一个 $N$ 个节点的有根树(点 $1$ 为根),节点从 $1$ 到 $N$ 编号,每个节点有一个颜色 $C_i$
对于一个以 $x$ 为根的子树,我们认为颜色 $c$ 在这个子树中出现次数是最多的,则认为 $c$ 支配这个子树
如果多个颜色出现次数相同并且都为最大,则它们都支配这个子树
对每个节点 $x$ ,输出所有支配他的子树的颜色编号之和
数据范围:
对于 $80\%$ 的数据,$1 leq n leq 10^5$
对于 $100\%$ 的数据,$1 leq n leq 10^6$,$1 leq x,y,C_i leq n$
这个题一眼看过去就有一个 $nlog^2_n$ 的优秀做法:
每个节点开一个 $set$ 维护子树出现的各种颜色和出现次数,$set$ 内部数据就按出现次数为关键字排序
然后对于每一个节点,考虑暴力把儿子的 $set$ 和自己的 $set$ 合并,如果用上启发式合并复杂度就是 $nlog^2_n$ ( $set$ 自带一个 $log$ )
可以跑过 $80$ 分的数据,然鹅打 $codeforces$ 的时候会 $80$ 和完全不会是一样的结果...
发现这个 $set$ 的 $log$ 很恶心,考虑更暴力的做法
直接暴力 $dfs$,对于每个节点都去遍历一遍它的子树求答案,这样复杂度是 $n^2$...
然后伪代码大概长这样:
void dfs(int x,int fa) { for(枚举儿子v) dfs(v,x) 枚举每个子树节点计算贡献 求出ans 清空x子树贡献 }
当然并没有什么用,但是如果运气好伪代码会长这样.........:
void dfs(int x,int fa) { for(枚举儿子v) { dfs(v,x) 清空v子树的贡献 } 枚举x子树计算贡献 更新答案 }
然后可以发现一个看似微不足道的优化...枚举儿子 $dfs$ 时的最后一个儿子子树贡献可以不清空,因为之后也要再加入
一眼感觉没什么用,最后一个儿子子树可能没多少节点
但是,我们可以自己决定哪个儿子最后枚举
如果我们把子树最大的儿子放最后,并且把它的贡献保留,复杂度又是多少呢
发现这样其实就相当于重链剖分的过程,把重儿子拿出来最后 $dfs$
复杂度也不难算,考虑启发式合并时的过程,发现其实我们这就相当于把重儿子子树提出来,然后和其他儿子一个个启发式合并
所以复杂度也就是启发式合并的复杂度 $nlog_n$
因为要保留贡献所以还要维护当前的答案,有点不好维护
大概就是维护一个栈(或队列)存当前对答案有贡献的各种颜色
删除的时候也不用枚举整个栈(或队列),打上一个删除标记就行,如果栈顶的颜色有删除标记就一直弹栈直到栈顶颜色没被删除或栈为空
当然也要维护当前答案,当前颜色的最大出现次数什么的...
具体还是看代码吧....
代码有挺多细节,注释比较细
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> using namespace std; typedef long long ll; inline int read() { int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } const int N=2e6+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 son[N],sz[N];//重儿子,子树大小 void dfs1(int x,int fa)//预处理重儿子 { sz[x]++; for(int i=fir[x];i;i=from[i]) { int &v=to[i]; if(v==fa) continue; dfs1(v,x); sz[x]+=sz[v]; if(sz[son[x]]<sz[v]) son[x]=v; } } int id[N],col[N],cnt[N],ans[N],dfs_clock;//dfs序为i的节点编号,节点i的颜色,当前颜色i的出现次数,节点i的答案 int now_ans,now_cnt,st[N],top;//当前答案,当前最大出现次数,st(栈)维护对答案贡献的各种颜色,top是栈顶 bool inst[N];//标记,判断节点是否已对答案有贡献 inline bool clr() { for(int i=1;i<=top;i++) inst[st[i]]=0; top=0; }//清栈 inline void ins(int c)//插入一个节点的贡献 { cnt[c]++; if(cnt[c]>now_cnt) { clr(); now_cnt=cnt[c]; st[++top]=now_ans=c; inst[c]=1; }//注意更新各种变量 else if(cnt[c]==now_cnt&&!inst[c]) { st[++top]=c; inst[c]=1; now_ans+=c; } } inline void del(int c)//删除一个节点的贡献 { if(cnt[c]==now_cnt)//如果在栈中 { inst[c]=0; now_ans-=c;//标记为出栈,更新now_ans while(top&&!inst[ st[top] ]) top--;//把不合法的颜色踢出(只要踢到有合法就行了) now_cnt=cnt[st[top]];//更新now_cnt } cnt[c]--;//别忘了更新cnt } void dfs2(int x,int fa,int flag)//计算每个节点的答案,flag判断最后要不要清空子树贡献 { id[++dfs_clock]=x;/*更新id*/ int L=dfs_clock;//记录当前非重儿子子树的左区间 if(!son[x]) { if(flag) ins(col[x]); ans[x]=col[x]; return; }//注意特判叶节点 for(int i=fir[x];i;i=from[i]) { int &v=to[i]; if(v==fa||v==son[x]) continue; dfs2(v,x,0);//先走轻儿子,要清空子树贡献避免影响之后轻儿子子树的答案 } int R=dfs_clock;//记录非重儿子子树右区间 dfs2(son[x],x,1);//最后遍历重儿子,要保留重儿子的贡献 //此时重儿子的贡献已经处理完毕,把剩下的节点贡献加入就好了 for(int i=L;i<=R;i++) ins(col[id[i]]);//加入剩下的贡献 ans[x]=now_ans;//求出当前节点的答案 if(!flag) for(int i=L;i<=dfs_clock;i++) del(col[id[i]]);//如果flag==0就清空整颗子树的贡献 } int n; int main() { n=read(); for(int i=1;i<=n;i++) col[i]=read(); int a,b; for(int i=1;i<n;i++) { a=read(),b=read(); add(a,b),add(b,a); } dfs1(1,1); dfs2(1,1,1); for(int i=1;i<=n;i++) printf("%d ",ans[i]); putchar(' '); return 0; }
其实上面的代码完全多此一举了
这题完全可以不用动态维护删点,代码瞬间少了一半,简单不少...
因为重儿子都是最后走,那么之前轻儿子子树删除就不用对每一个点的删除都动态维护当前答案,子树清完直接 $now \_ ans=0,now\_ cnt=0$ 就行了