动态开点与主席树

(初三写的,有、naiive,还是搬了)

一、引入(无兴趣看的可以略过)
经典例题:BRT Contract Codeforces Round #119 Div.1D
题⽬描述:
⼀条笔直的马路上有 \(n\) 个红绿灯,它们从时刻 \(0\) 开始,持续 \(g\) 秒的绿灯状态,然后持续 \(r\) 秒的红灯状态,并这样循环下去。给出每个红绿灯与马路起点处的距离 \(l_i\)
\(q\) 个询问,每个询问给出⼀个 \(t_i\),当⼀辆车在 \(t_i\) 时刻从起点处出
发并以单位速度⾏驶时,问需要多少秒才能通过最后⼀个红绿灯。

其中 \(1 \le n,q \le 10^5\)\(1 \le l_i,t_i \le 10^5\)

题目分析:
所有的与计算答案⽆关的距离与时间都可以看作是模 \(g+r\) 意义下
的。其中,\([0,g-1]\) 时刻到达⼀个红绿灯处,这个灯下⼀秒的状态为绿灯,可以继续前进;\([g,g+r-1]\) 时刻到达⼀个红绿灯处,这个灯下⼀秒的状态为红灯,不能继续前进。

首先考虑 \(O(nq)\) 暴力。
当到达⼀个灯时为红灯并等待到其变化为绿灯后,接下来所花的时间只与这个灯的位置有关(速度均为单位速度1),所以预处理 \(f_i\) 表⽰在模 \(g+r\) 意义下为 \(0\) 的时刻从第 \(i\) 个红绿灯出发的答案,对于每个询问求得它下一个红绿灯再加上此点的f即可。

于是考虑⽤线段树优化转移OvO

辣么问题来了:点的坐标过大可能会MLE
此时有两种方法
1、离散化+普通线段树
相信大家都会吧,这也不是现在我们要讲的重点
2、动态开点线段树
初始时⼀棵空的线段树
倒着求 \(f_i\),每求完⼀个将这个红绿灯插⼊线段树

二、动态开点线段树

1、为什么要用动态开点线段树?
(1 区间范围过大
(2 要开多棵线段树
总之就是空间不够!
2、怎么构建?
(1 存储线段树的左右⼉⼦(不能用i<<1和i<<1|1!)
(2 动态分配节点编号
来看几张图 :
1.jpg
2.jpg
3.jpg
4.jpg
5.jpg
图中有颜色的点位分配了空间的,其他暂时不用的点可以存位虚点(不管),每次期望的分配空间就是logn(甚至更小)!

上代码:(话说mjy只在课上展示了单点插入的代码啊喂)

struct node{
int l,r,x;
}tr[size];//线段树储存,会稍微麻烦一点。。
int cnt=0,lazy[size];
const int inf=1926081700;
void insert_point(int &p,int l,int r,int pos,int w){
if(!p){
p=++cnt;
tr[p].l=tr[p].r=0;
tr[p].x=-inf;
}
if(l==r) return;
int mid=(l+r)/2;
if(pos<=mid) insert_point(tr[p].l,l,mid,pos,w);
else insert(tr.p[r],mid+1,r,pos,w);
push_up(p);
}
void insert_zone(int &p,int l,int r,int nl,int nr,int w){ //原谅我想不起来区间的英文
if(!p){
p=++cnt;
tr[p].l=tr[p].r=0;
tr[p].x=-inf;
}
if(Lazy[p]!=0) push_down(p,l,r);//下推标记
if(l<=nl && r>=nr){
tr[p].x+=(r-l+1)*w;
lazy[pos]+=w;
return;
}
int mid=(nl+nr)/2;
if(L<=mid) insert_zone(tr[p].l,l,r,nl,mid,w);
if(R>mid) insert_zone(tr[p].r,l,r,mid+1,nr,w);
push_up(p);
}//随手打的,可能有错!就当伪代码好了(逃

3、主席树 ( 到了最难的部分啦!)

现在给你⼀个长度为 \(n\) 的序列 \(a\),和 \(m\) 个询问,每个询问给出两个参数 \(x_i,y_i\) ,你需要回答序列前 \(x_i\) 个数中 \(\le y_i\) 的数有多少个。

其中 \(n,m \le 10^5\)\(1 \le a_i,y_i\le 10^9\)

考虑离线的话,将询问按照 \(x_i\) 排序,依次从左到右加⼊序列中的数,此时只要维护当前集合中 \(\le y_i\) 的数的个数
此时使⽤任意⼀种数据结构即可解决,总复杂度 \(O(nlog_2n)\)

但是这种做法无法处理在线情况。

于是我们就需要一种强大的数据结构——主席树!
按照顺序建⽴ \(n\) 棵线段树,每棵线段树存储\([1,i]\)的区间。
由于第 \(i\) 棵线段树与第 \(i-1\) 棵线段树在⼤部分结构上是相同的,相同的部分分别⽤多个节点储存造成浪费。
所以考虑直接从上⼀次的线段树继承,只有在 ai 修改经过的路线上新建节点
由于每棵线段树最多新建 \(O(log_2n)\) 个节点,总空间复杂度 \(O(nlog_2n)\) ,可开接近1e6!

经典例题:区间第k小
给出⼀个长度为 \(n\) 的序列 \(a\),和 \(m\) 个询问,每个询问给出两个参数 \(l_i,r_i\) ,你需要回答一段区间\([l,r]\) 第k小的数。

其中 \(n,m \le 10^5\)\(a_i\le 10^9\)
建出表⽰前 \(1\)\(i\) 的数的主席树。\([l,r]\) 可以使⽤ \([1,r]\)-\([1,l-1]\)
第⼀种做法:⼆分。
⼆分区间 \(k\) ⼩值,每次查询 \(l\)\(r\)\(\le mid\)的个数,如果\(k\le\)个数,说明答案 \(\le\) mid,否则答案必定 \(≥\) mid。
时间复杂度 \(O(mlog_2^2n)\)

第⼆种做法:在主席树上⼆分。
时间复杂度 \(O(mlog_2n)\)։
上代码:

int query(int px, int py, int l, int r, int k) {
// 查询线段树上[l,r]区间中第k⼤的数
// px表⽰在第L-1棵线段树上当前节点,py表⽰在第R棵线段树上的当前节点
if (l==r) returnl ;
int mid=(l+r)/2;
int cnt=tr[tr[py].l].cnt-tr[tr[px].l].cnt;
if (cnt>=k) return query(tr[px].l,tr[py].l,l,mid,k) ; else
return query(tr[px].r,tr[py].r,mid+1,r,k-cnt) ;
}

细节就自己处理啦QwQ
推荐一道我现在还在做的主席树好题(树剖+主席树)SPOJ#10628/ luogu2633 / hdu Count on a tree

posted @ 2019-07-27 22:30  lcyfrog  阅读(340)  评论(0编辑  收藏  举报