模板:启发式合并
Posted $DeepinC$
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模板:启发式合并相关的知识,希望对你有一定的参考价值。
首先说明一点:线段树合并不是启发式合并。
启发式合并的大概内容就是:把小的数据结构按照这个数据结构的正常插入方法,一个一个地暴力塞进去。
而线段树合并显然不是这个东西。
这道题的题解太烂了,所以耽误了很长时间。
对于每一次操作,它只有3个参数:起始位置,作用时间,颜色。
把颜色离散化一下,让它们的编号分布在1e5以内。也可以不离散化,略麻烦一些而已。注意有负数。
现在我们维护一个以时间为区间下标的线段树,里面维护两个权值,一个是第一次出现的小球的个数,另一个是所有小球的个数。
我们另开一组vector存储发生在这个节点上的操作,需要存储时间/颜色这2个参数。
刚开始我们读入所有操作,直接把它放进起始位置的vector里。
然后就可以一遍dfs求解答案。考虑在每个点要做什么事。
1)递归求解所有子树
2)求解所有在这个节点上的操作得到本节点的答案
3)把与自身相关的所有事件上传给父节点
这其实是暴力思路。
我们考虑一下,如果按照上述思路,要么就是需要清空线段树,要么就是开100000个树爆内存。
也只有第一个能考虑优化。
如果1号节点有2个儿子,其中2号节点上有99995次操作,3号节点上有5次。
还要清空线段树?有什么想法吗?
对于3号节点,它暴力上传的复杂度并不高,关键就是怎么处理2号节点。
如果先扫3号节点,清空,再2号节点,信息不清空,事件也暂时不上传。
那么回溯到了1号节点,我们有一个现成的已经存好了99995个操作的线段树,而vector里面只有5次操作需要插入。多好啊
考虑这样一个问题:因为父节点只是一个点,而子树里可能有很多点,所以某个儿子身上可能聚集了很多操作。
那么对于每一个节点,我们称其“子树中操作数最多的儿子”为它的重儿子。
那么按照刚才举例子的这个思路,优化一下上面的那个dfs流程。
1)扫所有的轻儿子,把轻儿子的vector中的全部时间放进父节点额vector里,清空线段树。
2)求解重儿子,保留线段树,暂时不加入重儿子vector里面的事件。
3)把vector里面所有的为插入线段树的事件插入线段树,求解答案。
然而我的代码实现把清空和信息上传放在了dfs的最后,为了代码与题解统一,稍微改一下。
1)扫轻儿子。
2)扫重儿子。
3)把vector里面所有的为插入线段树的事件插入线段树,求解答案。
4)如果当前节点不是它父节点的重儿子,就清空线段树,把vector上传。
其实本质上是一样的,个人感觉我这个挺好理解。
然而上面这个思路仍然不够成熟,有一些具体实现会让时间复杂度产生一些差别。
首先是如何求出重儿子。这个还是比较简单的,dfs扫一遍。记得打完这个函数要调用!不要像某个姓吴的名字是两个字的傻子一样。
1 void pre_dfs(int p,int fa){ 2 siz[p]=v[p].size(); 3 for(int i=fir[p];i;i=l[i])if(to[i]!=fa){ 4 pre_dfs(to[i],p),siz[p]+=siz[to[i]]; 5 if(siz[to[i]]>siz[hson[p]])ihs[hson[p]]=0,hson[p]=to[i],ihs[to[i]]=1;//hson[p]存p的重儿子,ihs[p]表示p是否为父节点的重儿子 6 } 7 }
然后是怎么把vector里面的数据上传。考虑极端情况:一条100000点的链,在最后一个点上有100000个事件。
显然暴力一个一个事件地上传会T成狗,这就是为什么要启发式合并,这样就有了nlog的合并复杂度的保证了。
1 void up(int p,int fa){ 2 if(v[ref[p]].size()<v[ref[fa]].size()){//把小的往大的里面逐个插入 3 for(int i=0;i<v[ref[p]].size();++i)v[ref[fa]].push_back(v[ref[p]][i]); 4 v[ref[p]].clear();//清空,防止爆内存,但是好像不必要 5 } 6 else{for(int i=0;i<v[ref[fa]].size();++i)v[ref[p]].push_back(v[ref[fa]][i]);v[ref[fa]].clear();ref[fa]=ref[p];}
//如果是父节点的vector比儿子小,那么把父亲节点的vector倒进子节点的之后,要把代表元素修改一下,即节点fa对应的vector不再是v[fa]而是v[p] 7 }
再接下来需要实现的就是线段树了。设t数组表示球的总数量,c数组表示有贡献的球的数量(即每个颜色第一次出现的球的数量)
首先是修改,为了以后还要清空线段树(具体的等会再说),需要可加可减,倒没什么区别
1 void insert(int p,int pos,int tt,int cc){ 2 if(cl[p]==cr[p]){t[p]+=tt;c[p]+=cc;return;}//已经到了线段树叶子 3 if(pos<=cr[p<<1])insert(p<<1,pos,tt,cc); 4 else insert(p<<1|1,pos,tt,cc); 5 t[p]=t[p<<1]+t[p<<1|1];c[p]=c[p<<1]+c[p<<1|1]; 6 }
询问倒是有一点变化,有人说是二分查找什么的,我倒感觉有点多余。还记得平衡树上查第k大数的操作吗?
1 int ask(int p,int tt){ 2 if(tt>=t[p])return c[p];//如果给这个节点分配的可容纳球的空间比总球数还大,那么所有有贡献的球都计入答案 3 return (tt>t[p<<1]?c[p<<1]+ask(p<<1|1,tt-t[p<<1]):ask(p<<1,tt));//如果左子树的球数就比容积大,那就去左子树,否则带上左子树的所有贡献去右子树 4 }
线段树也完事了,但是怎么清空啊?如果memset显然是在找死了。
线段树上绝大多数的点还是没有值的,怎么避开它们?
你插入过的地方一定是有值的,那么再把vector走一遍走到哪清到哪就好了。
这又引出了一个问题:清空时需要扫所有的时间,但其实你的重儿子并没有上传。
但是它又已经在线段树里面了,你不能再次插入,怎么办?
很简单啊,在处理完父节点的其它事件之后求解答案完毕再上传就好了啊。
再稍稍修改一下dfs框架。
1)扫轻儿子。
2)扫重儿子。
3)把vector里面所有的为插入线段树的事件插入线段树,求解答案。
4)把重儿子的信息上传到自己身上。
5)如果当前节点不是它父节点的重儿子,就清空线段树,把vector上传。
还有,你怎么知道一个球有没有贡献?
开一个数组(没离散化就是map),记录这种颜色出现的最早时间,扫所有操作:
如果这个颜色没出现过,那么插入这个球,球数+1,贡献+1
如果这个颜色出现过,但是是在更晚的时间,那么在那个时间的贡献-1(不再是最早的了),在这个时间的球数和贡献+1
如果这个颜色已经再更早的时间出现过,那么球数+1但不加贡献
清空同理,用刚才的那个数组对照每个操作就行。
dfs的具体实现也有了。
1 void dfs(int p,int fa){ 2 for(int i=fir[p];i;i=l[i])if(to[i]!=fa&&to[i]!=hson[p])dfs(to[i],p);//先扫轻儿子 3 if(hson[p])dfs(hson[p],p);//最后扫重儿子 4 for(int i=0;i<v[ref[p]].size();++i){//处理所有不在树里的事件 5 int tim=v[ref[p]][i].first,col=v[ref[p]][i].second; 6 if(!al[col])insert(1,tim,1,1),al[col]=tim;//前面的事件里没有这个颜色,有球有贡献,记录最早的时间 7 else if(al[col]>tim)insert(1,al[col],0,-1),insert(1,tim,1,1),al[col]=tim;//时间比先插入的事件更早,把那个事件的贡献去除,新事件是有球有贡献的,更新最早时间 8 else insert(1,tim,1,0);//出现晚了,只有球,没有贡献 9 } 10 ans[p]=ask(1,min(ct[p],t[1]));if(hson[p])up(hson[p],p);//统计答案,把重儿子的事件传上来 11 if(!ihs[p]){//如果不是父节点的重儿子 12 for(int i=0;i<v[ref[p]].size();++i){//清空线段树 13 int tim=v[ref[p]][i].first,col=v[ref[p]][i].second; 14 if(al[col]==tim)insert(1,tim,-1,-1),al[col]=0;else insert(1,tim,-1,0);
//如果你就是这个颜色里出现最早的,去掉球和贡献,并且重置出现的时间,如果不是最早的,只扔球,没有贡献
//好像也可以直接置为0,但我懒得再打个函数了 15 } 16 up(p,fa);//把自己的信息上传 17 } 18 }
整体做完,感觉并不是很难啊。而且这个思路貌似挺暴力的啊。。
但是就是想不到。。。略略略。。。
1 #include<cstdio> 2 #include<vector> 3 #include<map> 4 #include<iostream> 5 using namespace std; 6 vector<pair<int,int> >v[100005]; 7 map<int,int>ma; 8 int al[100005],fir[100005],l[200005],to[200005],cnt,n,m,q,ct[100005]; 9 int hson[100005],ans[100005],k,siz[100005],ihs[100005],ref[100005]; 10 int cl[400005],cr[400005],t[400005],c[400005]; 11 void connect(int a,int b){ 12 l[++cnt]=fir[a];fir[a]=cnt;to[cnt]=b; 13 l[++cnt]=fir[b];fir[b]=cnt;to[cnt]=a; 14 } 15 void pre_dfs(int p,int fa){ 16 siz[p]=v[p].size()+1; 17 for(int i=fir[p];i;i=l[i])if(to[i]!=fa){ 18 pre_dfs(to[i],p),siz[p]+=siz[to[i]]; 19 if(siz[to[i]]>siz[hson[p]])ihs[hson[p]]=0,hson[p]=to[i],ihs[to[i]]=1; 20 } 21 } 22 void build(int p,int l,int r){ 23 cl[p]=l;cr[p]=r; 24 if(l==r)return; 25 build(p<<1,l,l+r>>1);build(p<<1|1,(l+r>>1)+1,r); 26 } 27 void insert(int p,int pos,int tt,int cc){ 28 if(cl[p]==cr[p]){t[p]+=tt;c[p]+=cc;return;} 29 if(pos<=cr[p<<1])insert(p<<1,pos,tt,cc); 30 else insert(p<<1|1,pos,tt,cc); 31 t[p]=t[p<<1]+t[p<<1|1];c[p]=c[p<<1]+c[p<<1|1]; 32 } 33 int ask(int p,int tt){ 34 if(tt==t[p])return c[p]; 35 return (tt>t[p<<1]?c[p<<1]+ask(p<<1|1,tt-t[p<<1]):ask(p<<1,tt)); 36 } 37 void up(int p,int fa){ 38 if(v[ref[p]].size()<v[ref[fa]].size()){ 39 for(int i=0;i<v[ref[p]].size();++i)v[ref[fa]].push_back(v[ref[p]][i]); 40 v[ref[p]].clear(); 41 } 42 else{for(int i=0;i<v[ref[fa]].size();++i)v[ref[p]].push_back(v[ref[fa]][i]);v[ref[fa]].clear();ref[fa]=ref[p];} 43 } 44 void dfs(int p,int fa){ 45 for(int i=fir[p];i;i=l[i])if(to[i]!=fa&&to[i]!=hson[p])dfs(to[i],p); 46 if(hson[p])dfs(hson[p],p); 47 for(int i=0;i<v[ref[p]].size();++i){ 48 int tim=v[ref[p]][i].first,col=v[ref[p]][i].second; 49 if(!al[col])insert(1,tim,1,1),al[col]=tim; 50 else if(al[col]>tim)insert(1,al[col],0,-1),insert(1,tim,1,1),al[col]=tim; 51 else insert(1,tim,1,0); 52 } 53 ans[p]=ask(1,min(ct[p],t[1]));if(hson[p])up(hson[p],p); 54 if(!ihs[p]){ 55 for(int i=0;i<v[ref[p]].size();++i){ 56 int tim=v[ref[p]][i].first,col=v[ref[p]][i].second; 57 if(al[col]==tim)insert(1,tim,-1,-1),al[col]=0;else insert(1,tim,-1,0); 58 } 59 up(p,fa); 60 } 61 } 62 int main(){ 63 scanf("%d",&n);ihs[1]=1; 64 for(int i=1,a,b;i<n;++i)scanf("%d%d",&a,&b),connect(a,b); 65 for(int i=1;i<=n;++i)scanf("%d",&ct[i]),ref[i]=i; 66 scanf("%d",&m);build(1,1,m); 67 for(int i=1,p,c;i<=m;++i){ 68 scanf("%d%d",&p,&c);if(ma.find(c)==ma.end())ma[c]=++k; 69 v[p].push_back(make_pair(i,ma[c])); 70 } 71 per_dfs(1,0);dfs(1,0); 72 scanf("%d",&q); 73 for(int i=1,a;i<=q;++i)scanf("%d",&a),printf("%d\n",ans[a]); 74 }
以上是关于模板:启发式合并的主要内容,如果未能解决你的问题,请参考以下文章
CF 600ELomsat gelral(树上启发式合并, dsu on tree, 静态链分治,模板题)
2019 ICPC 南昌 K. Tree(树上启发式合并,平衡树 treap)
2019 ICPC 南昌 K. Tree(树上启发式合并,平衡树 treap)