学习笔记:线段树
线段树
引入
线段树(Segment Tree)几乎是算法竞赛最常用的数据结构了,它主要用于维护区间信息(要求满足结合律)。与树状数组相比,它可以实现 的区间修改,还可以同时支持多种操作(加、乘),更具通用性。
基础版实现
首先来看一道例题。
【模板】线段树 2
已知一个数列,你需要进行下面三种操作:
- 将某区间每一个数乘上 ;
- 将某区间每一个数加上 ;
- 求出某区间每一个数的和。
很显然,这道题要用线段树来做,并且需要支持区间加、区间乘、区间求和三种操作。
线段树的建立
线段树是一棵平衡二叉树。母结点代表整个区间的和,越往下区间越小。注意,线段树的每个节点都对应一条线段(区间),但并不保证所有的线段(区间)都是线段树的节点,这两者应当区分开。如果有一个数组 ,那11么它对应的线段树大概长这个样子:
每个节点 node
的左右子节点的编号分别为 node << 1
和 node << 1 | 1
,假如节点 node
储存区间 的和,设 ,那么两个子节点分别储存 和 的和。可以发现,左节点对应的区间长度,与右节点相同或者比之恰好多 。
这里的 node << 1
和 node << 1 | 1
实际上分别等价于 node * 2
和 node * 2 + 1
,只是采用位运算优化了一些常数而已。
如何从数组建立一棵线段树?我们可以考虑递归地进行。
void pushup(int node){
tree[node] = tree[node << 1] + tree[node << 1 | 1];tree[node] %= m; // 当前节点权值等于左右子节点权值之和
}
void build(int node, int left, int right){
mark1[node] = 1; // 首先维护区间乘懒标记
if(left == right){ // 若到达叶子节点
tree[node] = a[left];return;
}
int mid = left + right >> 1;
build(node << 1, left, mid); // 求出左子节点
build(node << 1 | 1, mid + 1, right); // 求出右子节点
pushup(node); // 维护当前节点权值
}
那么该过程具体是如何实现的呢?还是以 为例:
区间修改
在讲区间修改前,要先引入一个“懒标记”(或者叫做延迟标记)的概念。懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记,将来要用到它的子区间的时候,再向下传递。这一道题需要支持区间加、区间乘操作,所以我们需要记录两个懒标记。
更新时,我们是从最大的区间开始,递归向下处理。注意到,任何区间都是线段树上某些节点的并集。于是我们记目标区间为 ,当前区间为 , 当前节点为 node
,我们会遇到三种情况:
- 若 ,即目标区间与当前区间没有交集,此时可以直接结束递归。
- 若 ,即目标区间包含当前区间,此时可以直接更新线段树并打上懒标记。
- 若 ,即目标区间与当前区间有交集,但不包含,此时需要这时把当前区间一分为二,分别进行处理。(如果存在懒标记,要先把懒标记传递给子节点)。
具体地,对于第一种情况:
return; // 直接返回
对于第二种情况:
tree[node] *= k;tree[node] %= m; // 更行当前节点权值
mark1[node] *= k;mark1[node] %= m; // 维护区间乘懒标记
mark2[node] *= k;mark2[node] %= m; // 维护区间加懒标记
tree[node] += k * (right - left + 1);tree[node] %= m; // 更新当前节点权值
mark2[node] += k;mark2[node] %= m; // 维护区间加懒标记
对于第三种情况:
pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
if(l <= mid)mul(node << 1, left, mid, l, r, k); // 递归更新左子节点
if(r > mid)mul(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
pushup(node); // 维护当前节点权值
pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
if(l <= mid)add(node << 1, left, mid, l, r, k); // 递归更新左子节点
if(r > mid)add(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
pushup(node); // 维护当前节点权值
这个过程并不是递归的,我们只往下传递一层(所以叫“懒”标记啊!),以后要用再才继续传递。其实我们常常把这个传递过程封装成一个函数:
void pushdown(int node, int left, int right){
if(mark1[node] != 1){ // 更新区间乘懒标记
mark1[node << 1] *= mark1[node];mark1[node << 1] %= m;
mark1[node << 1 | 1] *= mark1[node];mark1[node << 1 | 1] %= m;
mark2[node << 1] *= mark1[node];mark2[node << 1] %= m;
mark2[node << 1 | 1] *= mark1[node];mark2[node << 1 | 1] %= m;
tree[node << 1] *= mark1[node];tree[node << 1] %= m;
tree[node << 1 | 1] *= mark1[node];tree[node << 1 | 1] %= m;
mark1[node] = 1; // 恢复初始状态
}
if(mark2[node] != 0){ // 更新区间加懒标记
int mid = left + right >> 1;
mark2[node << 1] += mark2[node];mark2[node << 1] %= m;
mark2[node << 1 | 1] += mark2[node];mark2[node << 1 | 1] %= m;
tree[node << 1] += mark2[node] * (mid - left + 1);tree[node << 1] %= m;
tree[node << 1 | 1] += mark2[node] * (right - mid);tree[node << 1 | 1] %= m;
mark2[node] = 0; // 恢复初始状态
}
}
传递完标记后,再递归地去处理左右两个子节点。
至于单点修改,只需要令左右端点相等即可。
具体地,对于懒标记的操作,这里采取先乘后加的方法。所谓先乘后加就是在做乘法的时候把加法标记也乘上这个数,在后面做加法的时候直接加就行了。至于证明的话,读者自证不难。
先乘后加可(金)好(坷)啦(垃)好处都有啥?谁说对了就给他!
先乘后加可(金)好(坷)啦(垃),一题能顶两题啦!
先乘后加可(金)好(坷)啦(垃),NOIP一千八!
先乘后加啦,时间不⽩撒;先加后乘呀,撒了也⽩搭
先乘后加可(金)好(坷)啦(垃),不费时~!不怕WA~!
出题人,真不傻!时间给了他,对竞赛体验危害大,绝不能给他!
线段树2毒(不)瘤(发)啊(达),我们都要切(支)掉(援)他。先乘后加,毒(你)瘤(们)数(日)据(本)别~!想~!啦~!
区间查询
依然是差不多的实现原理,这里不再赘述。
int query(int node, int left, int right, int l, int r){
if(l <= left && r >= right)return tree[node]; // 直接返回
pushdown(node, left, right);int mid = left + right >> 1;int ans = 0; // 向下传递懒标记
if(l <= mid)ans += query(node << 1, left, mid, l, r);ans %= m; // 递归求左子节点
if(r > mid) ans += query(node << 1 | 1, mid + 1, right, l, r);ans %= m; // 递归求右子节点
return ans; // 返回最终答案
}
一样的递归,一样自顶至底地寻找,一样地合并信息。
这里附上例题的完整代码:
#include <iostream>
#define int long long
#define MAXN 100005
using namespace std;
int n, q, m;
int op, x, y, k;
int a[MAXN];
int tree[MAXN << 2], mark1[MAXN << 2], mark2[MAXN << 2];
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
void write(int x){
if(x < 0){putchar('-');x = -x;}
if(x >= 10)write(x / 10);
putchar(x % 10 + '0');
}
void pushup(int node){
tree[node] = tree[node << 1] + tree[node << 1 | 1];tree[node] %= m; // 当前节点权值等于左右子节点权值之和
}
void pushdown(int node, int left, int right){
if(mark1[node] != 1){ // 更新区间乘懒标记
mark1[node << 1] *= mark1[node];mark1[node << 1] %= m;
mark1[node << 1 | 1] *= mark1[node];mark1[node << 1 | 1] %= m;
mark2[node << 1] *= mark1[node];mark2[node << 1] %= m;
mark2[node << 1 | 1] *= mark1[node];mark2[node << 1 | 1] %= m;
tree[node << 1] *= mark1[node];tree[node << 1] %= m;
tree[node << 1 | 1] *= mark1[node];tree[node << 1 | 1] %= m;
mark1[node] = 1; // 恢复初始状态
}
if(mark2[node] != 0){ // 更新区间加懒标记
int mid = left + right >> 1;
mark2[node << 1] += mark2[node];mark2[node << 1] %= m;
mark2[node << 1 | 1] += mark2[node];mark2[node << 1 | 1] %= m;
tree[node << 1] += mark2[node] * (mid - left + 1);tree[node << 1] %= m;
tree[node << 1 | 1] += mark2[node] * (right - mid);tree[node << 1 | 1] %= m;
mark2[node] = 0; // 恢复初始状态
}
}
void build(int node, int left, int right){
mark1[node] = 1; // 首先维护区间乘懒标记
if(left == right){ // 若到达叶子节点
tree[node] = a[left];return;
}else{
int mid = left + right >> 1;
build(node << 1, left, mid); // 求出左子节点
build(node << 1 | 1, mid + 1, right); // 求出右子节点
pushup(node); // 维护当前节点权值
}
}
void mul(int node, int left, int right, int l, int r, int k){
if(l <= left && r >= right){
tree[node] *= k;tree[node] %= m; // 更行当前节点权值
mark1[node] *= k;mark1[node] %= m; // 维护区间乘懒标记
mark2[node] *= k;mark2[node] %= m; // 维护区间加懒标记
return;
}
pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
if(l <= mid)mul(node << 1, left, mid, l, r, k); // 递归更新左子节点
if(r > mid)mul(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
pushup(node); // 维护当前节点权值
}
void add(int node, int left, int right, int l, int r, int k){
if(l <= left && r >= right){
tree[node] += k * (right - left + 1);tree[node] %= m; // 更新当前节点权值
mark2[node] += k;mark2[node] %= m; // 维护区间加懒标记
return;
}
pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
if(l <= mid)add(node << 1, left, mid, l, r, k); // 递归更新左子节点
if(r > mid)add(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
pushup(node); // 维护当前节点权值
}
int query(int node, int left, int right, int l, int r){
if(l <= left && r >= right)return tree[node]; // 直接返回
pushdown(node, left, right);int mid = left + right >> 1;int ans = 0; // 向下传递懒标记
if(l <= mid)ans += query(node << 1, left, mid, l, r);ans %= m; // 递归求左子节点
if(r > mid) ans += query(node << 1 | 1, mid + 1, right, l, r);ans %= m; // 递归求右子节点
return ans; // 返回最终答案
}
signed main(){
n = read();q = read();m = read();
for(int i = 1 ; i <= n ; i ++)a[i] = read();
build(1, 1, n);
for(int i = 1 ; i <= q ; i ++){
op = read();
switch(op){
case 1:
x = read();y = read();k = read();
mul(1, 1, n, x, y, k);
case 2:
x = read();y = read();k = read();
add(1, 1, n, x, y, k);
case 3:
x = read();y = read();
write(query(1, 1, n, x, y));
putchar('\n');
}
}
return 0;
}
实际上线段树还可以维护区间最值、区间 等等,操作除了区间加、区间乘也可以是区间最值、区间赋值,了解原理后很容易改。
当然,线段树还有很多高级应用。
一些问题
如上述代码所示,我们在写线段树的模板时,开4倍的数组就不会溢出了,然而原因是什么呢?
这里给出一些证明。
证明:
首先线段树是一棵二叉树,最底层有 个叶子节点( 为区间大小)。
那么由此可知,此二叉树的高度为 ,易证 。
由等比数列求和公式 可知,对于一颗线段树有 ,( 为树的层数,即树的高度 )。
化简得 ,整理之后近似为 (近似计算忽略 )。
证毕。
权值线段树
权值线段树即一种线段树,以序列的数值为下标。节点里所统计的值为节点所对应的区间 [l,r][l,r] 中,[l,r][l,r] 这个值域中所有数的出现次数。
举个例子,有一个长度为 的序列 。
那么统计每个数出现的次数。易知 出现了 次, 出现了 次, 出现了 次, 出现了 次, 出现了 次。
那么我们可以建出一棵这样的权值线段树:
权值线段树可以在 的时间内求出全局的第 小值,某数在全局的排名,一个数的前驱或者后继。因为是线段树的一种所以思想也是二分,其本质就是一个可以二分的桶。
单点更新
单点更新的操作和普通线段树一样,递归到某个叶子节点操作即可。
void update(int node, int left, int right, int pos, int val){
if(left == right)tree[node] += val;
else{
int mid = left + right >> 1;
if(pos <= mid)update(node << 1, left, mid, pos, val);
else update(node << 1 | 1, mid + 1, right, pos, val);
pushup(node);
}
}
第 小值
这里利用了线段树的天然二分性,如果左子树的值大于等于 也就是说 小值在左子树,那么就往左子树递归,否则就往右子树递归。
比如查第 小,左子树只有 个,那么就往右子树递归查询第 小。
int kth(int node, int left, int right, int k){
if(left == right)return left;
else{
int mid = left + right >> 1;
if(tree[node << 1] >= k)return kth(node << 1, left, mid, k);
else return kth(node << 1 | 1, mid + 1, right, k - tree[node << 1]);
}
}
求一个数在全局的排名
排名数值上等于全局比那个数小的数的个数 ,那么放在权值线段树里就等价于前缀和查询,因为节点维护的是区间内出现的数的次数。
例如求 的排名,我们只需要查询 这个区间内的前缀和,然后 就是 的排名了。
那也就是普通线段树的区间查询了,只不过左区间恒为 罢了。
int query(int node, int left, int right, int l, int r){
if(l <= left && r >= right)return tree[node];
else{
int mid = left + right >> 1, ans = 0;
if(l <= mid)ans += query(node << 1, left, mid, l, r);
if(r > mid)ans += query(node << 1 | 1, mid + 1, right, l, r);
return ans;
}
}
求前驱
前驱就是小于这个数(令这个数为 )的最大的数,所以我们优先查小于 但尽量大的数,所以当右子树有小于 的时候我们优先递归右子树,直到整个区间小于 ,那该区间的右端点就是我们要求的前驱了。
所以我们分两部,找到整个区间刚好小于 的节点,再查询那个节点的非空最右子树。
int findpre(int node, int left, int right){
if(left == right)return left;
else{
int mid = left + right >> 1;
if(tree[node << 1 | 1] != 0)return findpre(node << 1 | 1, mid + 1, right);
else return findpre(node << 1, left, mid);
}
}
int getpre(int node, int left, int right, int k){
if(right < k){
if(tree[node] != 0)return findpre(node, left, right);
else return 0;
}else{
int mid = left + right >> 1, res;
if(mid + 1 < k && tree[node << 1 | 1] != 0 && (res = getpre(node << 1 | 1, mid + 1, rihgt, k)))
return res;
else return getpre(node << 1, left, mid, k);
}
}
对于小于 的最大的数,我们设 的排名为 ,则 的前驱就是第 小的数,所以我们可以通过 和 结合起来求前驱。
int getpre(int node){
return kth(1, 1, n, getrank(node) - 1);
}
求后继
后继就是找大于数 的最小数,思路和找前驱一样,分两步先找刚好整个区间大于 的节点,然后找节点的最左非空子树。
int findnxt(int node, int left, int right){
if(left == rihgt)return left;
else{
int mid = left + right >> 1;
if(tree[node << 1] != 0)return findnxt(node << 1, left, mid);
return findnxt(node << 1 | 1, mid + 1, right);
}
}
int getnxt(int node, int left, int right, int k){
if(k < left){
if(tree[node] != 0)return findnxt(node, left, right);
else return 0;
}else{
int mid = left + right >> 1, res;
if(mid > k && tree[node << 1] != 0 && (res = getnxt(node << 1, left, mid, k)))
return res;
else return getnxt(node << 1 | 1, mid + 1, right, k);
}
}
对于 的后继也一样,设 的排名为 ,那么 的后继就是第 小的数。
int getnxt(int node){
return kth(1, 1, n, getrank(node + 1)));
}
这里给出模板代码:
#include <iostream>
#include <algorithm>
#define int long long
#define MAXN 100005
using namespace std;
int n, m, op, x;
int tree[MAXN], ans;
void pushup(int node){
tree[node] = tree[node << 1] + tree[node << 1 | 1];
}
void update(int node, int left, int right, int pos, int val){
if(left == right)tree[node] += val;
else{
int mid = left + right >> 1;
if(pos <= mid)update(node << 1, left, mid, pos, val);
else update(node << 1 | 1, mid + 1, right, pos, val);
pushup(node);
}
}
int kth(int node, int left, int right, int k){
if(left == right)return left;
else{
int mid = left + right >> 1;
if(tree[node << 1] >= k)return kth(node << 1, left, mid, k);
else return kth(node << 1 | 1, mid + 1, right, k - tree[node << 1]);
}
}
int query(int node, int left, int right, int l, int r){
if(l <= left && r >= right)return tree[node];
else{
int mid = left + right >> 1, ans = 0;
if(l <= mid)ans += query(node << 1, left, mid, l, r);
if(r > mid)ans += query(node << 1 | 1, mid + 1, right, l, r);
return ans;
}
}
int getrank(int node){
if(node == 1)return 1;
else return query(1, 1, n, 1, node - 1) + 1;
}
int findpre(int node, int left, int right){
if(left == right)return left;
else{
int mid = left + right >> 1;
if(tree[node << 1 | 1] != 0)return findpre(node << 1 | 1, mid + 1, right);
else return findpre(node << 1, left, mid);
}
}
int getpre(int node, int left, int right, int k){
if(right < k){
if(tree[node] != 0)return findpre(node, left, right);
else return 0;
}else{
int mid = left + right >> 1, res;
if(mid + 1 < k && tree[node << 1 | 1] != 0 && (res = getpre(node << 1 | 1, mid + 1, right, k)))
return res;
else return getpre(noed << 1, left, mid, k);
}
}
int findnxt(int node, int left, int right){
if(left == rihgt)return left;
else{
int mid = left + right >> 1;
if(tree[node << 1] != 0)return findnxt(node << 1, left, mid);
return findnxt(node << 1 | 1, mid + 1, right);
}
}
int getnxt(int node, int left, int right, int k){
if(k < left){
if(tree[node] != 0)return findnxt(node, left, right);
else return 0;
}else{
int mid = left + right >> 1, res;
if(mid > k && tree[node << 1] != 0 && (res = getnxt(node << 1, left, mid, k)))
return res;
else return getnxt(node << 1 | 1, mid + 1, right, k);
}
}
代替平衡树使用时,权值线段树代码比较短,但是当值域较大、询问较多时空间占用会比较大。权值线段树需要按值域开空间,当值域过大时需要离散化或动态开点。
动态开点
通常来说,线段树占用空间是总区间长 的常数倍,空间复杂度是 。然而,有时候 很巨大,而我们又不需要使用所有的节点,这时便可以动态开点——不再一次性建好树,而是一边修改、查询一边建立。我们不再用 node*2
和 node*2+1
代表左右儿子,而是用 ls
和 rs
记录左右儿子的编号。设总查询次数为 ,则这样的总空间复杂度为 。
比起普通线段树,动态开点线段树有一个优势:它能够处理零或负数位置。此时,求 mid
时不能用 (l+r)/2
,而要用 (l+r-1)/2
(因为 l
等于 r
时会退出递归 ),pushdown
因此也要相应改一下。
因为缓存命中等原因,动态开点线段树写成结构体形式速度往往更快一些。
这里给出前文例题的动态开点做法。
#include <iostream>
#define int long long
#define MAXN 100005
using namespace std;
int n, m, p, op, x, y, k;
int now, tmp, cnt = 1;
struct node{
int val, mul, add, ls, rs;
}tree[MAXN << 1];
int read(){
int t = 1, x = 0;char ch = getchar();
while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * t;
}
void write(int x){
if(x < 0){putchar('-');x = -x;}
if(x >= 10)write(x / 10);
putchar(x % 10 ^ 48);
}
void pushup(int &node){
tree[node].val = tree[tree[node].ls].val + tree[tree[node].rs].val;
}
void pushdown(int node, int left, int right){
if(tree[node].mul != 1){
if(tree[node].ls == 0)tree[node].ls = ++cnt,tree[tree[node].ls].mul = 1;
if(tree[node].rs == 0)tree[node].rs = ++cnt,tree[tree[node].rs].mul = 1;
tree[tree[node].ls].val *= tree[node].mul;tree[tree[node].ls].val %= p;
tree[tree[node].rs].val *= tree[node].mul;tree[tree[node].rs].val %= p;
tree[tree[node].ls].mul *= tree[node].mul;tree[tree[node].ls].mul %= p;
tree[tree[node].rs].mul *= tree[node].mul;tree[tree[node].rs].mul %= p;
tree[tree[node].ls].add *= tree[node].mul;tree[tree[node].ls].add %= p;
tree[tree[node].rs].add *= tree[node].mul;tree[tree[node].rs].add %= p;
tree[node].mul = 1;
}
if(tree[node].add != 0){
int mid = left + right >> 1;
if(tree[node].ls == 0)tree[node].ls = ++cnt,tree[tree[node].ls].mul = 1;
if(tree[node].rs == 0)tree[node].rs = ++cnt,tree[tree[node].rs].mul = 1;
tree[tree[node].ls].val += tree[node].add * (mid - left + 1);tree[tree[node].ls].val %= p;
tree[tree[node].rs].val += tree[node].add * (right - mid);tree[tree[node].rs].val %= p;
tree[tree[node].ls].add += tree[node].add;tree[tree[node].ls].add %= p;
tree[tree[node].rs].add += tree[node].add;tree[tree[node].rs].add %= p;
tree[node].add = 0;
}
}
void build(int &node, int left, int right, int v, int k){
if(node == 0)node = ++cnt;
if(left == right)tree[node].val = k;
else{
int mid = left + right >> 1;
if(v <= mid)build(tree[node].ls, left, mid, v, k);
else build(tree[node].rs, mid + 1, right, v, k);
pushup(node);
}
}
void mul(int &node, int left, int right, int l, int r, int k){
if(node == 0)node = ++cnt;
if(l <= left && r >= right){
tree[node].val *= k;tree[node].val %= p;
tree[node].mul *= k;tree[node].mul %= p;
tree[node].add *= k;tree[node].add %= p;
}else{
pushdown(node, left, right);int mid = left + right >> 1;
if(l <= mid)mul(tree[node].ls, left, mid, l, r, k);
if(r > mid)mul(tree[node].rs, mid + 1, right, l, r, k);
pushup(node);
}
}
void add(int &node, int left, int right, int l, int r, int k){
if(node == 0)node = ++cnt;
if(l <= left && r >= right){
tree[node].val += k * (right - left + 1);tree[node].val %= p;
tree[node].add += k;tree[node].add %= p;
}else{
pushdown(node, left, right);int mid = left + right >> 1;
if(l <= mid)add(tree[node].ls, left, mid, l, r, k);
if(r > mid)add(tree[node].rs, mid + 1, right, l, r, k);
pushup(node);
}
}
int query(int node, int left, int right, int l, int r){
if(node == 0)return 0;
else if(l <= left && r >= right)return tree[node].val;
else{
pushdown(node, left, right);int mid = left + right >> 1, ans = 0;
if(l <= mid)ans += query(tree[node].ls, left, mid, l, r),ans %= p;
if(r > mid)ans += query(tree[node].rs, mid + 1, right, l, r),ans %= p;
return ans;
}
}
signed main(){
n = read();m = read();p = read();
for(int i = 1 ; i <= n ; i ++)
tmp = read(),now = 1,build(now, 1, n, i, tmp);
for(int i = 0 ; i < MAXN << 1 ; i ++)tree[i].mul = 1;
for(int i = 1 ; i <= m ; i ++){
op = read();
switch(op){
case 1:
x = read();y = read();k = read();
now = 1;mul(now, 1, n, x, y, k);
break;
case 2:
x = read();y = read();k = read();
now = 1;add(now, 1, n, x, y, k);
break;
case 3:
x = read();y = read();
write(query(1, 1, n, x, y));
putchar('\n');break;
default:break;
}
}
return 0;
}
可持久化
大家应该也许或许可能大概用过撤回功能,使用它可以返回之前的某个状态,并在此基础上进行修改。实现这样的功能,便需要可持久化的数据结构,它保存每个历史版本,可以高效地查询和修改历史版本,且因其维持原结构的不变性还在函数式编程领域有很大作用。
可持久化线段树就是这样一种数据结构。想要让线段树可持久化,最朴素的实现方法是每进行一次操作都建一棵新的树,不用说也知道,这样做的时间和空间复杂度都是不可接受的。稍微思考一下会发现,每次修改操作都只会有少数的点被修改,所以大量的点是可以共用的。
要实现这一点,需要我们用动态开点的方法存储每个点的左右子节点。不过,对于最初版本的那棵树,我们应当一次性建好,而不必一个一个插入:
void build(int node, int left, int right){
if(left == right)tree[node].val = a[left];
else{
tree[node].ls = ++cnt;tree[node].rs = ++cnt;
int mid = left + right >> 1;
build(tree[node].ls, left, mid);
build(tree[node].rs, mid + 1, right);
pushup(node);
}
}
假如原始数据为 ,现在我们有了形如这样的一棵线段树:
我们维持这棵树不变,添加额外的节点来代替修改操作。
单点修改
加假如当前我们需要将 加 ,首先创建一个新的根节点。
要修改的 对应的叶子节点在右子树上。所以,这个新节点的左儿子应该与原节点的左儿子相同(即与之共用左子树)。再创建一个新的节点作为右儿子。
接下来处理这个新节点(这是一个递归的过程)。显然 应该在左子树,所以相似地,沿用原树的右儿子,然后创建一个新的节点作为左儿子。
接下来给各个节点赋值,和普通的线段树类似,叶子节点直接赋值,其它节点则利用子节点计算值。
这样就完成了单点修改。事实上我们没有修改任何东西,我们保留了原来的版本,并新增了一个修改后的版本。
void update(int node, int left, int right, int x, int y, int k){
if(left == right)tree[x].val = tree[y].val + k;
else{
tree[y].ls = tree[x].ls;tree[y].rs = tree[x].rs;
int mid = left + right >> 1;
if(node <= mid){
tree[node].ls = ++cnt;
update(node, left, mid, tree[x].ls, tree[y].ls, k);
}else{
tree[node].rs = ++cnt;
update(node, mid + 1, right, tree[x].rs, tree[y].rs, k);
}
pushup(node);
}
}
查询操作和普通的查询操作是一样的,只不过需要指定从哪个根节点开始查询。
总结
没有总结,颓疯了。
一上午都在搞颓,开小窗口还被抓了 qwq。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具