权值线段树
所谓权值线段树,就是一种维护值而非下标的线段树,我个人倾向于称呼它为值域线段树。
举个栗子:对于一个给定的数组,普通线段树可以维护某个子数组中数的和,而权值线段树可以维护某个区间内数组元素出现的次数。
在实现上,由于值域范围通常较大,权值线段树会采用离散化或动态开点的策略优化空间。
更新操作:
更新的时候,我们向线段树中插入一个值v,那么所有包含v的区间值都需要+1。(每个节点维护对应区间中出现了多少个数)
1 int update (long long v,long long l,long long r,int pos) { // 插入v,当前区间为[l,r]。
2 if (!pos) pos=++tot_node;
3 // 如果该节点不存在,则新建节点。
4 if (l<=v&&v<=r) {
5 // 如果当前区间包含插入值。
6 tree[pos].val++;
7 // 出现次数+1。
8 if (l==r) return pos;
9 // 如果递归到叶子节点,退出。
10 }
11 long long mid=(l+r)>>1; 12 if (v<=mid) tree[pos].ls=update(v,l,mid,tree[pos].ls); 13 else tree[pos].rs=update(v,mid+1,r,tree[pos].rs); 14 // 判断插入值是在当前区间的哪一半。 15 pushup(pos); 16 // 回溯。 17 return pos; 18 }
查询操作:
查询操作类似二叉树。
1 long long query (long long l,long long r,long long L,long long R,int pos) { // 查询区间[L,R]中数字的数量,当前区间为[l,r]。
2 if (!pos) return 0;
3 // 如果该节点不存在,必然没有到达过。
4 if (L<=l&&r<=R) {
5 // 如果当前区间属于查询区间。
6 return tree[pos].val;
7 // 直接返回区间中数字的数量。
8 }
9 long long mid=(l+r)>>1,ans=0;
10 if (L<=mid) ans+=query(l,mid,L,R,tree[pos].ls); 11 if (mid,r,L,R,tree[pos].rs); 12 // 统计区间和。 13 return ans; 14 }
练习题:
作为练习模板,可以考虑逆序对。大体思路是每次查询a[i]+1~n的元素个数。
线段树合并
所谓线段树合并,就是通过将合并两颗线段树获得信息,其正确性由线段树的稳定结构保证。
线段树合并通常是一个自底向上的过程,在深搜的途中将子节点的树合并到父节点上,从而实现对父节点值的统计。
线段树合并的复杂度是$nlogn$,比启发式合并少一个$logn$。
不难发现,如果按照线段树合并的原始思想直接在每一个需要遍历的节点上都单独建立一个线段树肯定会爆空间。在这里可以使用被称为“回收内存”的方法:由于在合并之后子节点的信息已经归入父节点,所以子节点没有用处,那么可以将其所有节点回收丢入一个内存池,往后更新的时候可以从内存池里取节点而非新建节点。
回收内存:
1 inline int newId () { 2 if (pool_top) return mempool[pool_top--]; 3 return ++tot_node; 4 } 5 inline void killId (int &x) { 6 mempool[++pool_top]=x; 7 tree[x].ls=tree[x].rs=tree[x].val=0; 8 x=0; 9 }
合并操作:
合并操作与左偏树的合并有一点像,就是递归合并每一个节点。
1 int merge (int l,int r,int x,int y) { 2 if (!x||!y) return x+y; 3 int now=newId(),mid=(l+r)>>1; 4 if (l==r) { 5 tree[now].val=tree[x].val+tree[y].val; 6 } else { 7 tree[now].ls=merge(l,mid,tree[x].ls,tree[y].ls); 8 tree[now].rs=merge(mid+1,r,tree[x].rs,tree[y].rs); 9 tree[now].val=tree[tree[now].ls].val+tree[tree[now].rs].val; 10 } 11 killId(x),killId(y); 12 return now; 13 }
练习题:
可以考虑做一下Tree Rotation,大致题意是给一棵二叉树,可以交换每个点的左右子树,要求前序遍历叶子的逆序对最少。
由于左右儿子的交换不会影响更上层的值,所以在每次合并的时候直接统计即可。