首先说明一点:线段树合并不是启发式合并。

启发式合并的大概内容就是:把小的数据结构按照这个数据结构的正常插入方法,一个一个地暴力塞进去。

而线段树合并显然不是这个东西。

这道题的题解太烂了,所以耽误了很长时间。

对于每一次操作,它只有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扫一遍。记得打完这个函数要调用!不要像某个姓吴的名字是两个字的傻子一样。

 void pre_dfs(int p,int fa){
siz[p]=v[p].size();
for(int i=fir[p];i;i=l[i])if(to[i]!=fa){
pre_dfs(to[i],p),siz[p]+=siz[to[i]];
if(siz[to[i]]>siz[hson[p]])ihs[hson[p]]=,hson[p]=to[i],ihs[to[i]]=;
      //hson[p]存p的重儿子,ihs[p]表示p是否为父节点的重儿子
}
}

然后是怎么把vector里面的数据上传。考虑极端情况:一条100000点的链,在最后一个点上有100000个事件。

显然暴力一个一个事件地上传会T成狗,这就是为什么要启发式合并,这样就有了nlog的合并复杂度的保证了。

 void up(int p,int fa){
if(v[ref[p]].size()<v[ref[fa]].size()){//把小的往大的里面逐个插入
for(int i=;i<v[ref[p]].size();++i)v[ref[fa]].push_back(v[ref[p]][i]);
v[ref[p]].clear();//清空,防止爆内存,但是好像不必要
}
else{for(int i=;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]
}

再接下来需要实现的就是线段树了。设t数组表示球的总数量,c数组表示有贡献的球的数量(即每个颜色第一次出现的球的数量)

首先是修改,为了以后还要清空线段树(具体的等会再说),需要可加可减,倒没什么区别

 void insert(int p,int pos,int tt,int cc){
if(cl[p]==cr[p]){t[p]+=tt;c[p]+=cc;return;}//已经到了线段树叶子
if(pos<=cr[p<<])insert(p<<,pos,tt,cc);
else insert(p<<|,pos,tt,cc);
t[p]=t[p<<]+t[p<<|];c[p]=c[p<<]+c[p<<|];
}

询问倒是有一点变化,有人说是二分查找什么的,我倒感觉有点多余。还记得平衡树上查第k大数的操作吗?

 int ask(int p,int tt){
if(tt>=t[p])return c[p];//如果给这个节点分配的可容纳空间比总球数还大那么所有有贡献的球都计入答案
return (tt>t[p<<]?c[p<<]+ask(p<<|,tt-t[p<<]):ask(p<<,tt));
    //如果左子树的球数就比容积大,那就去左子树,否则带上左子树的所有贡献去右子树
}

线段树也完事了,但是怎么清空啊?如果memset显然是在找死了。

线段树上绝大多数的点还是没有值的,怎么避开它们?

你插入过的地方一定是有值的,那么再把vector走一遍走到哪清到哪就好了。

这又引出了一个问题:清空时需要扫所有的时间,但其实你的重儿子并没有上传。

但是它又已经在线段树里面了,你不能再次插入,怎么办?

很简单啊,在处理完父节点的其它事件之后求解答案完毕再上传就好了啊。

再稍稍修改一下dfs框架。

1)扫轻儿子。

2)扫重儿子。

3)把vector里面所有的为插入线段树的事件插入线段树,求解答案。

4)把重儿子的信息上传到自己身上。

5)如果当前节点不是它父节点的重儿子,就清空线段树,把vector上传。

还有,你怎么知道一个球有没有贡献?

开一个数组(没离散化就是map),记录这种颜色出现的最早时间,扫所有操作:

如果这个颜色没出现过,那么插入这个球,球数+1,贡献+1

如果这个颜色出现过,但是是在更晚的时间,那么在那个时间的贡献-1(不再是最早的了),在这个时间的球数和贡献+1

如果这个颜色已经再更早的时间出现过,那么球数+1但不加贡献

清空同理,用刚才的那个数组对照每个操作就行。

dfs的具体实现也有了。

 void dfs(int p,int fa){
for(int i=fir[p];i;i=l[i])if(to[i]!=fa&&to[i]!=hson[p])dfs(to[i],p);//先扫轻儿子
if(hson[p])dfs(hson[p],p);//最后扫重儿子
for(int i=;i<v[ref[p]].size();++i){//处理所有不在树里的事件
int tim=v[ref[p]][i].first,col=v[ref[p]][i].second;
if(!al[col])insert(,tim,,),al[col]=tim;//前面的事件里没有这个颜色,有球有贡献,记录最早的时间
else if(al[col]>tim)insert(,al[col],,-),insert(,tim,,),al[col]=tim;
      //时间比先插入的事件更早,把那个事件的贡献去除,新事件是有球有贡献的,更新最早时间
else insert(,tim,,);//出现晚了,只有球,没有贡献
}
ans[p]=ask(,min(ct[p],t[]));if(hson[p])up(hson[p],p);//统计答案,把重儿子的事件传上来
if(!ihs[p]){//如果不是父节点的重儿子
for(int i=;i<v[ref[p]].size();++i){//清空线段树
int tim=v[ref[p]][i].first,col=v[ref[p]][i].second;
if(al[col]==tim)insert(,tim,-,-),al[col]=;else insert(,tim,-,);
        //如果你就是这个颜色里出现最早的,去掉球和贡献,并且重置出现的时间,如果不是最早的,只扔球,没有贡献
        //好像也可以直接置为0,但我懒得再打个函数了
}
up(p,fa);//把自己的信息上传
}
}

整体做完,感觉并不是很难啊。而且这个思路貌似挺暴力的啊。。

但是就是想不到。。。略略略。。。

 #include<cstdio>
#include<vector>
#include<map>
#include<iostream>
using namespace std;
vector<pair<int,int> >v[];
map<int,int>ma;
int al[],fir[],l[],to[],cnt,n,m,q,ct[];
int hson[],ans[],k,siz[],ihs[],ref[];
int cl[],cr[],t[],c[];
void connect(int a,int b){
l[++cnt]=fir[a];fir[a]=cnt;to[cnt]=b;
l[++cnt]=fir[b];fir[b]=cnt;to[cnt]=a;
}
void pre_dfs(int p,int fa){
siz[p]=v[p].size()+;
for(int i=fir[p];i;i=l[i])if(to[i]!=fa){
pre_dfs(to[i],p),siz[p]+=siz[to[i]];
if(siz[to[i]]>siz[hson[p]])ihs[hson[p]]=,hson[p]=to[i],ihs[to[i]]=;
}
}
void build(int p,int l,int r){
cl[p]=l;cr[p]=r;
if(l==r)return;
build(p<<,l,l+r>>);build(p<<|,(l+r>>)+,r);
}
void insert(int p,int pos,int tt,int cc){
if(cl[p]==cr[p]){t[p]+=tt;c[p]+=cc;return;}
if(pos<=cr[p<<])insert(p<<,pos,tt,cc);
else insert(p<<|,pos,tt,cc);
t[p]=t[p<<]+t[p<<|];c[p]=c[p<<]+c[p<<|];
}
int ask(int p,int tt){
if(tt==t[p])return c[p];
return (tt>t[p<<]?c[p<<]+ask(p<<|,tt-t[p<<]):ask(p<<,tt));
}
void up(int p,int fa){
if(v[ref[p]].size()<v[ref[fa]].size()){
for(int i=;i<v[ref[p]].size();++i)v[ref[fa]].push_back(v[ref[p]][i]);
v[ref[p]].clear();
}
else{for(int i=;i<v[ref[fa]].size();++i)v[ref[p]].push_back(v[ref[fa]][i]);v[ref[fa]].clear();ref[fa]=ref[p];}
}
void dfs(int p,int fa){
for(int i=fir[p];i;i=l[i])if(to[i]!=fa&&to[i]!=hson[p])dfs(to[i],p);
if(hson[p])dfs(hson[p],p);
for(int i=;i<v[ref[p]].size();++i){
int tim=v[ref[p]][i].first,col=v[ref[p]][i].second;
if(!al[col])insert(,tim,,),al[col]=tim;
else if(al[col]>tim)insert(,al[col],,-),insert(,tim,,),al[col]=tim;
else insert(,tim,,);
}
ans[p]=ask(,min(ct[p],t[]));if(hson[p])up(hson[p],p);
if(!ihs[p]){
for(int i=;i<v[ref[p]].size();++i){
int tim=v[ref[p]][i].first,col=v[ref[p]][i].second;
if(al[col]==tim)insert(,tim,-,-),al[col]=;else insert(,tim,-,);
}
up(p,fa);
}
}
int main(){
scanf("%d",&n);ihs[]=;
for(int i=,a,b;i<n;++i)scanf("%d%d",&a,&b),connect(a,b);
for(int i=;i<=n;++i)scanf("%d",&ct[i]),ref[i]=i;
scanf("%d",&m);build(,,m);
for(int i=,p,c;i<=m;++i){
scanf("%d%d",&p,&c);if(ma.find(c)==ma.end())ma[c]=++k;
v[p].push_back(make_pair(i,ma[c]));
}
pre_dfs(,);dfs(,);
scanf("%d",&q);
for(int i=,a;i<=q;++i)scanf("%d",&a),printf("%d\n",ans[a]);
}

完整代码

模板(ac):启发式合并的更多相关文章

  1. [CSP-S模拟测试]:模板(ac)(线段树启发式合并)

    题目描述 辣鸡$ljh\ NOI$之后就退役了,然后就滚去学文化课了.他每天都被$katarina$大神虐,仗着自己学过一些姿势就给$katarina$大神出了一道题.有一棵$n$个节点的以$1$号节 ...

  2. DSU模板(树的启发式合并)

    摘自Codeforces博客 With dsu on tree we can answer queries of this type: How many vertices in subtree of ...

  3. SCUT - 106 - 花式ac - 主席树/启发式合并Treap

    https://scut.online/p/106 错在这组样例,发现是离散化之后,对k访问的时候也是应该访问离散化之后的k. 12 4 1 1 2 2 5 5 4 4 3 3 2 1 1 3 3 5 ...

  4. BZOJ2888 资源运输(LCT启发式合并)

    这道题目太神啦! 我们考虑他的每一次合并操作,为了维护两棵树合并后树的重心,我们只好一个一个的把节点加进去.那么这样一来看上去似乎就是一次操作O(nlogn),但是我们拥有数据结构的合并利器--启发式 ...

  5. Love Live!-01字典树启发式合并

    链接:https://ac.nowcoder.com/acm/contest/201/D?&headNav=www 思路:题目要求的是每个等级下的最大 简单路径中的最大异或值,那么我们为了保证 ...

  6. bzoj 3674: 可持久化并查集加强版 (启发式合并+主席树)

    Description Description:自从zkysb出了可持久化并查集后……hzwer:乱写能AC,暴力踩标程KuribohG:我不路径压缩就过了!ndsf:暴力就可以轻松虐!zky:…… ...

  7. BZOJ3123[Sdoi2013]森林——主席树+LCA+启发式合并

    题目描述 输入 第一行包含一个正整数testcase,表示当前测试数据的测试点编号.保证1≤testcase≤20. 第二行包含三个整数N,M,T,分别表示节点数.初始边数.操作数.第三行包含N个非负 ...

  8. Bzoj2534:后缀自动机 主席树启发式合并

    国际惯例的题面:考虑我们求解出字符串uvu第一个u的右端点为i,第二个u的右端点为j,我们需要满足什么性质?显然j>i+L,因为我们选择的串不能是空串.另外考虑i和j的最长公共前缀(也就是说其p ...

  9. 【BZOJ-3123】森林 主席树 + 启发式合并

    3123: [Sdoi2013]森林 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 2738  Solved: 806[Submit][Status] ...

随机推荐

  1. vue路由跳转的方式

    vue路由跳转有四种方式 1. router-link 2. this.$router.push() (函数里面调用) 3. this.$router.replace() (用法同push) 4. t ...

  2. 运维自动化神器ansible之user模块

    运维自动化神器ansible之user模块 一.概述   user模块 可管理远程主机上的 用户,比如创建用户.修改用户.删除用户.为用户创建密钥对等操作. 二.参数介绍   name: 用于指定操作 ...

  3. 复杂模型可解释性方法——LIME

    一.模型可解释性     近年来,机器学习(深度学习)取得了一系列骄人战绩,但是其模型的深度和复杂度远远超出了人类理解的范畴,或者称之为黑盒(机器是否同样不能理解?),当一个机器学习模型泛化性能很好时 ...

  4. NAT模式实现虚拟机共享主机网络

    上一节我们在虚拟机上搭建了linux系统,并利用桥接模式访问互联网,这一节,我们来配置一下通过NAT模式访问互联网.说到这里有些小伙伴可能要问了,NAT模式和桥接模式有什么区别呢? 桥接模式: 虚拟机 ...

  5. 从零开始的vue学习笔记(八)

    前言 今天花一天时间阅读完Vue Router的官方文档的基础部分,简单的做一下总结和记录 Vue Router是什么 Vue Router 是 Vue.js 官方的路由管理器,用于构建单页应用(SP ...

  6. 【前端词典】4 个实用有趣的 JS 特性

    前言 最近在学习的过程中发现了我之前未曾了解过的一些特性,发现有些很有趣并且在处理一些问题的时候可以给我一个新的思路. 这里我将这些特性介绍给大家. 4 个有趣的 JS 特性 利用 a 标签解析 UR ...

  7. Java学习笔记之抽象类与接口

    抽象类(abstract) 抽象类概述:一个类被abstract修饰表示这个类是抽象类, 自己定义方法但是不实现方法,后代去实现 抽象方法:   一个方法被abstract修饰表示这个方法是抽象方法 ...

  8. php反序列化漏洞复现

    超适合小白的php反序列化漏洞复现 写在前头的话 在OWASP TOP10中,反序列化已经榜上有名,但是究竟什么是反序列化,我觉得应该进下心来好好思考下.我觉得学习的时候,所有的问题都应该问3个问题: ...

  9. HDU 3873 Invade the Mars(带限制条件的Dijkstra)

    题目网址:http://acm.hdu.edu.cn/showproblem.php?pid=3873 思路: 军队可以先等待在城市外面,等保护该城市的城市都被攻破后,直接进城(即进城不用耗费时间). ...

  10. python-利用freeze生成requirements文件

    使用场景:本地电脑开发完成的python自动化项目,需要导出python相关的依赖包以便后续迁移项目使用. C:\Users\acer>e: E:\>pip freeze >requ ...