写在前言之前
很早之前我就开始学最基本的线段树操作,最高的只到区间加法。时隔多日才想起来在深入一下,加之很久没有写过线段树的板子,所以写的时候还是比较恶心的
这次主攻的是区间乘法的操作。
前言
对于区间查询这一类问题。如果给定的是一个有序的序列,完全可以使用前缀和求解。求解无序的区间查询是比较常用的有ST表和线段树,今天要说的便是线段树这一数据结构
0x00
线段树是一棵二叉搜索树,每一个节点都储存有一些信息,通过对这些信息的修改和维护可以做到$O(nlogn)$的时间内建树$+$修改$+$查询。
可能下面这张图能够更加直观的解释线段树是啥
每个点下的红色的字体表示区间的左右端点,每个点里面的数是这个店所代表的区间的和,最下面黄色的店里面的是序列中的元素
这张图这么好看,怎么可能是我画的呢QAQ
线段树版本1
0x01
已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上x
- 求出某区间每一个数的和
在这种情况下,我们要用到最普通的线段树,支持区间加法和区间查询。
线段树将树上的节点都看做一条线段,每个节点上都维护着一些信息
如果上面的题目的话,就需要维护下列的信息
- $l$和$r$表示区间(线段)的左端点和右端点
- $sum$表示这个区间的元素的总和
- $lazytag$,先记住,这东西叫做懒标记,在后面会作出解释
本篇文章全部使用数组来实现,指针党请谅解
0x02
我们先来看如何建立一棵线段树。通过一个$build$函数来实现,这个函数的时间复杂度是$O(nlogn)$
首先从根节点开始向下扩展,显然根节点存储的$l$和$r$应该是$l=1,r=n$。每次扩展时计算一个$mid$值$=(l+r)/2$
这个从$l$到$mid$这段区间放到左儿子中,$mid+1$到$r$放到右儿子里。
如果搜索到$l=r$时。已经到底了,这个区间的值就可以确定了,是第$l$个元素。
然后就可以回溯。在回溯的时候维护区间和。另根节点的$sum$等于左右儿子的$sum$的和。
代码如下
inline void build(int k, int ll, int rr) { //k是节点的编号,ll是该节点的左端点,rr是右端点 tree[k].l = ll, tree[k].r = rr; //赋值 if(tree[k].l == tree[k].r) { //如果找到了l等于r的情况证明已经到了最底部。可以直接输入 scanf("%lld", &tree[k].w); return ; //回溯 } long long int mid = (ll+rr)/2; build((k<<1), ll, mid); //建立左儿子 build((k<<1)+1, mid+1, rr); //建立右儿子 tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w; //维护区间和 }
为了方便大家理解我还录制了GIF给大家看看
0x03
再来说区间加法,这里引入一个前文提到过的概念-----懒标记
顾名思义,懒标记的作用就是懒。它要怎么懒呢?
在进行区间修改的时候我们要减少多余的操作。将一个区间全都加上一个数时。我们只对要用到的区间进行操作。对于那些之后要用到的但现在没用到的区间我们可以先不修改,用$lazytag$存储一个值,什么时候用到什么时候下传给儿子,在一步步下传到要用到的区间。
这个操作用一个$down$函数来实现
inline void down(int k) { tree[(k<<1)].f += tree[k].f; tree[(k<<1)+1].f += tree[k].f; //更新左右儿子的懒标记 tree[(k<<1)].w += tree[k].f*(tree[(k<<1)].r-tree[(k<<1)].l+1); tree[(k<<1)+1].w += tree[k].f*(tree[(k<<1)+1].r-tree[(k<<1)+1].l+1); //更新左右儿子的区间和 tree[k].f = 0; //清除父亲结点的懒标记 }
然后这个区间加法就只剩下普通的操作了,看下面的区间修改代码
inline void change_interval(int k) { if(tree[k].l >= a&&tree[k].r <= b) { tree[k].w += (tree[k].r-tree[k].l+1)*y; tree[k].f += y; //更新当前区间的和还有当前区间的懒标记 return ; } if(tree[k].f) down(k); //如果懒标记不为0的话就下传给自己的儿子 int mid = (tree[k].l+tree[k].r)/2; if(a <= mid) change_interval((k<<1)); if(b > mid) change_interval((k<<1)+1); tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w; //维护区间和 }
0x04
至于区间查询,和区间修改是差不多的
inline void ask_interval(int k) { //如果当前的区间被要查询的区间包含的话,直接加到答案中 if(tree[k].l >= a&&tree[k].r <= b) { ans += tree[k].w; return ; } if(tree[k].f) down(k); int mid = (tree[k].l+tree[k].r)/2; //判断左右儿子的区间和要查询的区间是否有交集 if(a <= mid) ask_interval((k<<1)); if(b > mid) ask_interval((k<<1)+1); }
0x05
下面放上我的完整的代码
#include <iostream> #include <cstdio> using namespace std; struct node{ int l, r; long long w, f; //(l, r)区间,区间和w,懒标记f; }tree[400001]; long long int ans, y; int x, n, m; int a, b; inline void build_tree(int k, int ll, int rr) { tree[k].l = ll, tree[k].r = rr; if(tree[k].l == tree[k].r) { scanf("%lld", &tree[k].w); return ; } long long int mid = (ll+rr)/2; build_tree((k<<1), ll, mid); build_tree((k<<1)+1, mid+1, rr); tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w; } inline void down(int k) { tree[(k<<1)].f += tree[k].f; tree[(k<<1)+1].f += tree[k].f; tree[(k<<1)].w += tree[k].f*(tree[(k<<1)].r-tree[(k<<1)].l+1); tree[(k<<1)+1].w += tree[k].f*(tree[(k<<1)+1].r-tree[(k<<1)+1].l+1); tree[k].f = 0; } inline void ask_point(int k) { if(tree[k].l == tree[k].r) { ans = tree[k].w; return ; } if(tree[k].f) down(k); int mid = (tree[k].l+tree[k].r)/2; if(x <= mid) ask_point((k<<1)); else ask_point((k<<1)+1); } inline void change_point(int k) { if(tree[k].l == tree[k].r) { tree[k].w += y; return ; } if(tree[k].f) down(k); int mid = (tree[k].l+tree[k].r)/2; if(x <= mid) change_point((k<<1)); else change_point((k<<1)+1); tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w; } inline void ask_interval(int k) { if(tree[k].l >= a&&tree[k].r <= b) { ans += tree[k].w; return ; } if(tree[k].f) down(k); int mid = (tree[k].l+tree[k].r)/2; if(a <= mid) ask_interval((k<<1)); if(b > mid) ask_interval((k<<1)+1); // tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w; } inline void change_interval(int k) { if(tree[k].l >= a&&tree[k].r <= b) { tree[k].w += (tree[k].r-tree[k].l+1)*y; tree[k].f += y; return ; } if(tree[k].f) down(k); int mid = (tree[k].l+tree[k].r)/2; if(a <= mid) change_interval((k<<1)); if(b > mid) change_interval((k<<1)+1); tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w; } int main() { scanf("%d%d", &n, &m); build_tree(1, 1, n); for(int i=1; i<=m; i++) { int p; ans = 0; scanf("%d", &p); switch(p) { /*ask_point*/case 4: { scanf("%d", &x); ask_point(1); printf("%lld ", ans); break; } /*change_point*/case 3: { scanf("%d%d", &x, &y); change_point(1); break; } /*ask_interval*/case 2: { scanf("%d%d", &a, &b); ask_interval(1); printf("%lld ", ans); break; } /*change_interval*/case 1: { scanf("%d%d%lld", &a, &b, &y); change_interval(1); break; } } } return 0; }
0x06
来几个例题
线段树版本2
0x00
这个版本的线段树呢,就是加入了更多的区间操作。比如区间乘法,但这些操作大致相同,这里以区间乘法为例进行讲解
像上面的加法有加法标记一样,乘法也有乘法标记。
0x01
建树的过程与版本一的建树过程大致相同
这里不再进行详细讲解,只放上代码。唯一要值得注意的是懒标记的初始化,乘法标记要初始化为$1$。
inline void build(int k, int ll, int rr) { tree[k].l = ll, tree[k].r = rr; tree[k].addtag = 0, tree[k].multag = 1; if(tree[k].l == tree[k].r) { tree[k].sum = read(); tree[k].sum %= Mod; return ; } int mid = (tree[k].l + tree[k].r) >> 1; build(Lson, tree[k].l, mid); build(Rson, mid + 1, tree[k].r); tree[k].sum = tree[Lson].sum + tree[Rson].sum; tree[k].sum %= Mod; }
0x02
懒标记的下传是整个线段树中最核心的部分,一般情况下如果你写的线段树WA掉了,那肯定是你写的懒标记下传函数出了锅
带有区间加法的线段树的下传函数非常复杂。通常情况下我们先下传乘法标记,在下传加法标记。因为先进行乘法,对之后的加法不会产生什么影响,如果先进行加法的话,对之后的乘法就会产生影响。所以我们选择先进行乘法标记的下传。在下传加法标记的同时直接将乘法标记也下传给加法标记。
下面给出$down$函数的代码
inline void pushdown(int k) { tree[Lson].multag = tree[k].multag * tree[Lson].multag % Mod; tree[Rson].multag = tree[k].multag * tree[Rson].multag % Mod; tree[Lson].addtag = tree[Lson].addtag * tree[k].multag % Mod; tree[Rson].addtag = tree[Rson].addtag * tree[k].multag % Mod; tree[Lson].sum = tree[Lson].sum * tree[k].multag % Mod; tree[Rson].sum = tree[Rson].sum * tree[k].multag % Mod; tree[Lson].addtag = (tree[k].addtag + tree[Lson].addtag) % Mod; tree[Rson].addtag = (tree[k].addtag + tree[Rson].addtag) % Mod; int L = (tree[Lson].r - tree[Lson].l + 1); int R = (tree[Rson].r - tree[Rson].l + 1); tree[Lson].sum = (tree[Lson].sum + L * tree[k].addtag) % Mod; tree[Rson].sum = (tree[Rson].sum + R * tree[k].addtag) % Mod; tree[k].addtag = 0, tree[k].multag = 1; }
0x03
区间乘法和区间加法的更新都和线段树版本1异曲同工
所以不再进行讲解
直接给出代码
区间加法更新
inline void update_add(int k) { if(tree[k].l >= x && tree[k].r <= y) { tree[k].sum = (tree[k].sum + (tree[k].r - tree[k].l + 1) * z) % Mod; tree[k].addtag = (tree[k].addtag + z) % Mod; return ; } pushdown(k); int mid = (tree[k].l + tree[k].r) >> 1; if(mid >= x) update_add(Lson); if(mid < y) update_add(Rson); tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod; }
区间乘法更新
inline void update_mul(int k) { if(tree[k].l >= x && tree[k].r <= y) { tree[k].sum = tree[k].sum * z % Mod; tree[k].addtag = tree[k].addtag * z % Mod; tree[k].multag = tree[k].multag * z % Mod; return ; } pushdown(k); int mid = (tree[k].l + tree[k].r) >> 1; if(mid >= x) update_mul(Lson); if(mid < y) update_mul(Rson); tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod; }
0x04
还是放上完整的代码
#include <iostream> #include <cstdio> #define Lson (k << 1) #define Rson (k << 1) + 1 typedef long long LL; const int maxn = 4e5+3; LL n, m, Mod, x, y, z, c; struct node { LL l, r, sum, addtag, multag; }tree[maxn]; LL xx, f; char ch; inline LL read() { xx = 0, f = 1; ch = getchar(); while (ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); } while (ch <= '9' && ch >= '0') { xx = xx * 10 + ch - '0'; ch = getchar(); } return xx * f; } inline void build(int k, int ll, int rr) { tree[k].l = ll, tree[k].r = rr; tree[k].addtag = 0, tree[k].multag = 1; if(tree[k].l == tree[k].r) { tree[k].sum = read(); tree[k].sum %= Mod; return ; } int mid = (tree[k].l + tree[k].r) >> 1; build(Lson, tree[k].l, mid); build(Rson, mid + 1, tree[k].r); tree[k].sum = tree[Lson].sum + tree[Rson].sum; tree[k].sum %= Mod; } inline void pushdown(int k) { tree[Lson].multag = tree[k].multag * tree[Lson].multag % Mod; tree[Rson].multag = tree[k].multag * tree[Rson].multag % Mod; tree[Lson].addtag = tree[Lson].addtag * tree[k].multag % Mod; tree[Rson].addtag = tree[Rson].addtag * tree[k].multag % Mod; tree[Lson].sum = tree[Lson].sum * tree[k].multag % Mod; tree[Rson].sum = tree[Rson].sum * tree[k].multag % Mod; tree[Lson].addtag = (tree[k].addtag + tree[Lson].addtag) % Mod; tree[Rson].addtag = (tree[k].addtag + tree[Rson].addtag) % Mod; int L = (tree[Lson].r - tree[Lson].l + 1); int R = (tree[Rson].r - tree[Rson].l + 1); tree[Lson].sum = (tree[Lson].sum + L * tree[k].addtag) % Mod; tree[Rson].sum = (tree[Rson].sum + R * tree[k].addtag) % Mod; tree[k].addtag = 0, tree[k].multag = 1; } inline void update_mul(int k) { if(tree[k].l >= x && tree[k].r <= y) { tree[k].sum = tree[k].sum * z % Mod; tree[k].addtag = tree[k].addtag * z % Mod; tree[k].multag = tree[k].multag * z % Mod; return ; } pushdown(k); int mid = (tree[k].l + tree[k].r) >> 1; if(mid >= x) update_mul(Lson); if(mid < y) update_mul(Rson); tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod; } inline void update_add(int k) { if(tree[k].l >= x && tree[k].r <= y) { tree[k].sum = (tree[k].sum + (tree[k].r - tree[k].l + 1) * z) % Mod; tree[k].addtag = (tree[k].addtag + z) % Mod; return ; } pushdown(k); int mid = (tree[k].l + tree[k].r) >> 1; if(mid >= x) update_add(Lson); if(mid < y) update_add(Rson); tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod; } inline LL check(int k) { if(tree[k].l > y || tree[k].r < x) return 0; if(tree[k].l >= x && tree[k].r <= y) { return tree[k].sum % Mod; } pushdown(k); int mid = (tree[k].l + tree[k].r) >> 1; return (check(Lson) + check(Rson)) % Mod; } int main() { n = read(), m = read(), Mod = read(); build(1, 1, n); for(int i=1; i<=m; i++) { c = read(); switch(c) { case 1: x = read(), y = read(), z = read(); update_mul(1); break; case 2: x = read(), y = read(), z = read(); update_add(1); break; case 3: x = read(), y = read(); printf("%lld ", check(1)); } } }
0x05
还有很多其他类型的线段树,比如超哥线段树、吉司机线段树、zkw线段树什么的。这里不再深入讲解。
感兴趣的同学可以自行百度