我竟然开了这么多坑。。。ε=(´ο`*)))唉,慢慢填吧。。。
线段树 进阶
然后我们来讲一个好玩的东西,叫权值线段树。
权值线段树&动态开点
有一个数列,数列里的每个不同的 a i a_i ai 都有一个对应的数 v i v_i vi ,表示数列中的 a i a_i ai 的个数。
所以我们的任务就是,把 v v v 挂到线段树上。
众所周知,线段树是可以单点修改的,为了方便,我们把单点修改 x x x 规定为往数列里增加一个 x x x。
我们让线段树中的 [ l , r ] [l,r] [l,r] 区间表示 a l , a l + 1 , a l + 2 , ⋯ , a r a_l,a_{l+1},a_{l+2},\cdots,a_r al,al+1,al+2,⋯,ar 每个数出现次数的和。 [ x , x ] [x,x] [x,x] 就表示 v x v_x vx。
注意,由于这里每一个数都有一个对应的结点,所以对于权值线段树来讲,区间不叫区间,叫值域。
咦咦咦?那干嘛要用线段树?用桶他不香嘛?
桶是挺香的,所以我们必须学线段树。 桶的区间修改和区间查询都是 O ( n ) O(n) O(n) 的,可以用线段树将其优化到 O ( log 2 n ) O(\log_2n) O(log2n) 。
代码呼之欲出。(就是一个简单的单点修改,区间查询,建议打一打,当复习)
Code
#include<cstdio>
#define int long long //清秀的写法
const int maxn=1e5+5;//这里 a[i] 的最大范围是 1e5
struct segment_tree{
int l,r;
int sum;//sum 表示 [l,r] 中每个数出现次数的和
}a[maxn<<2];//二倍值域
void read(int&x){
x=0;bool f=0;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&15);ch=getchar();}
if(f)x=-x;return;
}
void build(int p,int l,int r){
a[p].l=l,a[p].r=r;
if(l==r)return;
int mid=l+r>>1;
build(p<<1,l,mid);
build((p<<1)|1,mid+1,r);
return;
}
void Update(int p,int x){
if(a[p].l==a[p].r){
a[p].sum++;//找到 x 的对应结点,将其对应个数 ++ 。
return;
}
int mid=a[p].l+a[p].r>>1;
if(x<=mid)Update(p<<1,x);
else Update((p<<1)|1,x);
a[p].sum++;//左子树和右子树中的其中之一会 ++, 会且仅会 +1, 所以干脆直接 ++ 。
return;
}
int Query(int p,int l,int r){
if(a[p].l>=l&&a[p].r<=r)
return a[p].sum;
int val=0;
int mid=a[p].l+a[p].r>>1;
if(l<=mid)
val+=Query(p<<1,l,r);
if(r>mid)
val+=Query((p<<1)|1,l,r);
return val;
}
signed main(){
//略
return 0;
}
以上是数据较小的情况。我们想,当 a i a_i ai 的范围很大,比如 1 e 8 1e8 1e8 的时候,我们还能这么玩吗?显然不能。于是,我们来加个名叫“动态开点”的玩意。
回顾区间修改,我们是怎么对一系列数进行修改的?
用一个懒惰标记,需要时将懒惰标记向下延伸。
那么,在不是每一个数都需要时,我们为什么要将 1 1 1 ~ m a x n maxn maxn 的每一个值域都计算一次呢?我们何不像区间修改那样,到了需要的时候再建点并计算呢?
但这样并不能实质性地优化空间,下标最大还是 n × 4 n\times 4 n×4 。于是,我们考虑换一种方式求下标。
我们定义一个 t o t tot tot,代表下标。在插入一个 a i a_i ai 时,如果 a i a_i ai 所处于的子树是空的,也就是还没有建这个子树,我们将这个子树的下标设置为 ++tot
。
咦?那这样还怎么确定一个结点的左子树和右子树的下标?
存呀!
我们让一个结点存放: l , r , l l , r r , s u m l,r,ll,rr,sum l<