# 权值线段树 --- ## 权值线段树的概念 我们知道,普通线段树维护的信息是**数列的区间信息**,比如区间和、区间最大值、区间最小值等等。在维护序列的这些信息的时候,我们更关注的是这些数本身的信息,换句话说,我们要维护区间的最值或和,我们最关注的是这些数统共的信息。而权值线段树**维护一列数中数的个数**。 我们来看这样一个数列: $111233455667$ 一棵权值线段树的叶子节点维护的是“有几个1”,“有几个2”...,他们的父亲节点维护的是“有几个1和2”。 这个东西类似我们之前学过的“桶”。 也就是说,我们的权值线段树就是用线段树维护了一堆桶。 这就是权值线段树的概念。 --- ## 权值线段树和简单线段树的区别 权值线段树维护的是桶,**按值域开空间,维护的是个数**。 简单线段树维护的是信息,**按个数开空间,维护的是特定信息**。 --- ## 权值线段树的用途 权值线段树可以解决**数列第k大/小**的问题。 这里要注意!我们只能对给定数列解决**整个数列**的第k大/小,并不能解决**数列的子区间**的第k大/小。 (剧透一下,解决数列子区间的第k大/小需要**主席树**(可持久化线段树) 一些直觉强的读者可能已经隐约体会到权值线段树是如何维护数列第k大/小了。没错,因为我们的权值线段树维护的是一堆桶,每个节点储存的是节点维护区间(就是值域)的数出现的次数(再次强调定义),那么,根据这个定义,整棵线段树的根节点就表示整个值域有几个数。对于整列数中第k大/小的值,我们从根节点开始判断(这里按第k大为例),如果k比右儿子大,就说明第k大的数在左儿子所表示的值域中。然后,k要减去右儿子。(这是因为继续递归下去的时候,正确答案,整个区间的第k大已经不再是左儿子表示区间的第k大了,很好理解)。 依次递归下去,到了叶子节点的时候,叶子节点就是我们要找的正确答案。 --- ## 权值线段树的相关操作及权值线段树模板 与一般线段树一样。 遇值域较大时,可以考虑动态开点线段树。 --- # 动态开点线段树 前面讲到堆式储存的情况下,需要给线段树开 $4n$ 大小的数组。为了节省空间,我们可以不一次性建好树,而是在最初只建立一个根结点代表整个区间。当我们需要访问某个子区间时,才建立代表这个区间的子结点。这样我们不再使用 $2p$ 和 $2p+1$ 代表 $p$ 结点的儿子,而是用 $\text{ls}$ 和 $\text{rs}$ 记录儿子的编号。总之,动态开点线段树的核心思想就是:**结点只有在有需要的时候才被创建**。 单次操作的时间复杂度是不变的,为 $O(\log n)$。由于每次操作都有可能创建并访问全新的一系列结点,因此 $m$ 次单点操作后结点的数量规模是 $O(m\log n)$。最多也只需要 $2n-1$ 个结点,没有浪费。 --- 单点修改: ```cpp // root 表示整棵线段树的根结点;cnt 表示当前结点个数 int n, cnt, root; int sum[n * 2], ls[n * 2], rs[n * 2]; // 用法:update(root, 1, n, x, f); 其中 x 为待修改节点的编号 void update(int& p, int s, int t, int x, int f) { // 引用传参 if (!p) p = ++cnt; // 当结点为空时,创建一个新的结点 if (s == t) { sum[p] += f; return; } int m = s + ((t - s) >> 1); if (x <= m) update(ls[p], s, m, x, f); else update(rs[p], m + 1, t, x, f); sum[p] = sum[ls[p]] + sum[rs[p]]; // pushup } ``` --- 区间询问: ```cpp // 用法:query(root, 1, n, l, r); int query(int p, int s, int t, int l, int r) { if (!p) return 0; // 如果结点为空,返回 0 if (s >= l && t <= r) return sum[p]; int m = s + ((t - s) >> 1), ans = 0; if (l <= m) ans += query(ls[p], s, m, l, r); if (r > m) ans += query(rs[p], m + 1, t, l, r); return ans; } ``` 区间修改也是一样的,不过下放标记时要注意如果缺少孩子,就直接创建一个新的孩子。或者使用标记永久化技巧。 --- ## 一些优化 这里总结几个线段树的优化: - 在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。 - 下放懒惰标记可以写一个专门的函数 `pushdown`,从儿子节点更新当前节点也可以写一个专门的函数 `maintain`(或者对称地用 `pushup`),降低代码编写难度。 - 标记永久化:如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。 --- ## C++ 模板 SegTreeLazyRangeAdd 可以区间加/求和的线段树模板 ```cpp #include
using namespace std; template
class SegTreeLazyRangeAdd { vector
tree, lazy; vector
*arr; int n, root, n4, end; void maintain(int cl, int cr, int p) { int cm = cl + (cr - cl) / 2; if (cl != cr && lazy[p]) { lazy[p * 2] += lazy[p]; lazy[p * 2 + 1] += lazy[p]; tree[p * 2] += lazy[p] * (cm - cl + 1); tree[p * 2 + 1] += lazy[p] * (cr - cm); lazy[p] = 0; } } T range_sum(int l, int r, int cl, int cr, int p) { if (l <= cl && cr <= r) return tree[p]; int m = cl + (cr - cl) / 2; T sum = 0; maintain(cl, cr, p); if (l <= m) sum += range_sum(l, r, cl, m, p * 2); if (r > m) sum += range_sum(l, r, m + 1, cr, p * 2 + 1); return sum; } void range_add(int l, int r, T val, int cl, int cr, int p) { if (l <= cl && cr <= r) { lazy[p] += val; tree[p] += (cr - cl + 1) * val; return; } int m = cl + (cr - cl) / 2; maintain(cl, cr, p); if (l <= m) range_add(l, r, val, cl, m, p * 2); if (r > m) range_add(l, r, val, m + 1, cr, p * 2 + 1); tree[p] = tree[p * 2] + tree[p * 2 + 1]; } void build(int s, int t, int p) { if (s == t) { tree[p] = (*arr)[s]; return; } int m = s + (t - s) / 2; build(s, m, p * 2); build(m + 1, t, p * 2 + 1); tree[p] = tree[p * 2] + tree[p * 2 + 1]; } public: explicit SegTreeLazyRangeAdd
(vector
v) { n = v.size(); n4 = n * 4; tree = vector
(n4, 0); lazy = vector
(n4, 0); arr = &v; end = n - 1; root = 1; build(0, end, 1); arr = nullptr; } void show(int p, int depth = 0) { if (p > n4 || tree[p] == 0) return; show(p * 2, depth + 1); for (int i = 0; i < depth; ++i) putchar('\t'); printf("%d:%d\n", tree[p], lazy[p]); show(p * 2 + 1, depth + 1); } T range_sum(int l, int r) { return range_sum(l, r, 0, end, root); } void range_add(int l, int r, int val) { range_add(l, r, val, 0, end, root); } }; ``` --- SegTreeLazyRangeSet 可以区间修改/求和的线段树模板" ```cpp #include
using namespace std; template
class SegTreeLazyRangeSet { vector
tree, lazy; vector
*arr; int n, root, n4, end; void maintain(int cl, int cr, int p) { int cm = cl + (cr - cl) / 2; if (cl != cr && lazy[p]) { lazy[p * 2] = lazy[p]; lazy[p * 2 + 1] = lazy[p]; tree[p * 2] = lazy[p] * (cm - cl + 1); tree[p * 2 + 1] = lazy[p] * (cr - cm); lazy[p] = 0; } } T range_sum(int l, int r, int cl, int cr, int p) { if (l <= cl && cr <= r) return tree[p]; int m = cl + (cr - cl) / 2; T sum = 0; maintain(cl, cr, p); if (l <= m) sum += range_sum(l, r, cl, m, p * 2); if (r > m) sum += range_sum(l, r, m + 1, cr, p * 2 + 1); return sum; } void range_set(int l, int r, T val, int cl, int cr, int p) { if (l <= cl && cr <= r) { lazy[p] = val; tree[p] = (cr - cl + 1) * val; return; } int m = cl + (cr - cl) / 2; maintain(cl, cr, p); if (l <= m) range_set(l, r, val, cl, m, p * 2); if (r > m) range_set(l, r, val, m + 1, cr, p * 2 + 1); tree[p] = tree[p * 2] + tree[p * 2 + 1]; } void build(int s, int t, int p) { if (s == t) { tree[p] = (*arr)[s]; return; } int m = s + (t - s) / 2; build(s, m, p * 2); build(m + 1, t, p * 2 + 1); tree[p] = tree[p * 2] + tree[p * 2 + 1]; } public: explicit SegTreeLazyRangeSet
(vector
v) { n = v.size(); n4 = n * 4; tree = vector
(n4, 0); lazy = vector
(n4, 0); arr = &v; end = n - 1; root = 1; build(0, end, 1); arr = nullptr; } void show(int p, int depth = 0) { if (p > n4 || tree[p] == 0) return; show(p * 2, depth + 1); for (int i = 0; i < depth; ++i) putchar('\t'); printf("%d:%d\n", tree[p], lazy[p]); show(p * 2 + 1, depth + 1); } T range_sum(int l, int r) { return range_sum(l, r, 0, end, root); } void range_set(int l, int r, int val) { range_set(l, r, val, 0, end, root); } }; ``` --- ## 例题 [luogu P3372【模板】线段树 1](https://www.luogu.com.cn/problem/P3372) 已知一个数列,你需要进行下面两种操作: - 将某区间每一个数加上 $k$。 - 求出某区间每一个数的和。 参考代码 ```cpp #include
typedef long long LL; LL n, a[100005], d[270000], b[270000]; void build(LL l, LL r, LL p) { // l:区间左端点 r:区间右端点 p:节点标号 if (l == r) { d[p] = a[l]; // 将节点赋值 return; } LL m = l + ((r - l) >> 1); build(l, m, p << 1), build(m + 1, r, (p << 1) | 1); // 分别建立子树 d[p] = d[p << 1] + d[(p << 1) | 1]; } void update(LL l, LL r, LL c, LL s, LL t, LL p) { if (l <= s && t <= r) { d[p] += (t - s + 1) * c, b[p] += c; // 如果区间被包含了,直接得出答案 return; } LL m = s + ((t - s) >> 1); if (b[p]) d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m), b[p << 1] += b[p], b[(p << 1) | 1] += b[p]; b[p] = 0; if (l <= m) update(l, r, c, s, m, p << 1); // 本行和下面的一行用来更新p*2和p*2+1的节点 if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1); d[p] = d[p << 1] + d[(p << 1) | 1]; // 计算该节点区间和 } LL getsum(LL l, LL r, LL s, LL t, LL p) { if (l <= s && t <= r) return d[p]; LL m = s + ((t - s) >> 1); if (b[p]) d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m), b[p << 1] += b[p], b[(p << 1) | 1] += b[p]; b[p] = 0; LL sum = 0; if (l <= m) sum = getsum(l, r, s, m, p << 1); // 本行和下面的一行用来更新p*2和p*2+1的答案 if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1); return sum; } int main() { std::ios::sync_with_stdio(0); LL q, i1, i2, i3, i4; std::cin >> n >> q; for (LL i = 1; i <= n; i++) std::cin >> a[i]; build(1, n, 1); while (q--) { std::cin >> i1 >> i2 >> i3; if (i1 == 2) std::cout << getsum(i2, i3, 1, n, 1) << std::endl; // 直接调用操作函数 else std::cin >> i4, update(i2, i3, i4, 1, n, 1); } return 0; } ``` --- [luogu P3373【模板】线段树 2](https://www.luogu.com.cn/problem/P3373) 已知一个数列,你需要进行下面三种操作: - 将某区间每一个数乘上 $x$。 - 将某区间每一个数加上 $x$。 - 求出某区间每一个数的和。 参考代码 ```cpp #include
#define ll long long ll read() { ll w = 1, q = 0; char ch = ' '; while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar(); if (ch == '-') w = -1, ch = getchar(); while (ch >= '0' && ch <= '9') q = (ll)q * 10 + ch - '0', ch = getchar(); return (ll)w * q; } int n, m; ll mod; ll a[100005], sum[400005], mul[400005], laz[400005]; void up(int i) { sum[i] = (sum[(i << 1)] + sum[(i << 1) | 1]) % mod; } void pd(int i, int s, int t) { int l = (i << 1), r = (i << 1) | 1, mid = (s + t) >> 1; if (mul[i] != 1) { // 懒标记传递,两个懒标记 mul[l] *= mul[i]; mul[l] %= mod; mul[r] *= mul[i]; mul[r] %= mod; laz[l] *= mul[i]; laz[l] %= mod; laz[r] *= mul[i]; laz[r] %= mod; sum[l] *= mul[i]; sum[l] %= mod; sum[r] *= mul[i]; sum[r] %= mod; mul[i] = 1; } if (laz[i]) { // 懒标记传递 sum[l] += laz[i] * (mid - s + 1); sum[l] %= mod; sum[r] += laz[i] * (t - mid); sum[r] %= mod; laz[l] += laz[i]; laz[l] %= mod; laz[r] += laz[i]; laz[r] %= mod; laz[i] = 0; } return; } void build(int s, int t, int i) { mul[i] = 1; if (s == t) { sum[i] = a[s]; return; } int mid = s + ((t - s) >> 1); build(s, mid, i << 1); // 建树 build(mid + 1, t, (i << 1) | 1); up(i); } void chen(int l, int r, int s, int t, int i, ll z) { int mid = s + ((t - s) >> 1); if (l <= s && t <= r) { mul[i] *= z; mul[i] %= mod; // 这是取模的 laz[i] *= z; laz[i] %= mod; // 这是取模的 sum[i] *= z; sum[i] %= mod; // 这是取模的 return; } pd(i, s, t); if (mid >= l) chen(l, r, s, mid, (i << 1), z); if (mid + 1 <= r) chen(l, r, mid + 1, t, (i << 1) | 1, z); up(i); } void add(int l, int r, int s, int t, int i, ll z) { int mid = s + ((t - s) >> 1); if (l <= s && t <= r) { sum[i] += z * (t - s + 1); sum[i] %= mod; // 这是取模的 laz[i] += z; laz[i] %= mod; // 这是取模的 return; } pd(i, s, t); if (mid >= l) add(l, r, s, mid, (i << 1), z); if (mid + 1 <= r) add(l, r, mid + 1, t, (i << 1) | 1, z); up(i); } ll getans(int l, int r, int s, int t, int i) { // 得到答案,可以看下上面懒标记助于理解 int mid = s + ((t - s) >> 1); ll tot = 0; if (l <= s && t <= r) return sum[i]; pd(i, s, t); if (mid >= l) tot += getans(l, r, s, mid, (i << 1)); tot %= mod; if (mid + 1 <= r) tot += getans(l, r, mid + 1, t, (i << 1) | 1); return tot % mod; } int main() { // 读入 int i, j, x, y, bh; ll z; n = read(); m = read(); mod = read(); for (i = 1; i <= n; i++) a[i] = read(); build(1, n, 1); // 建树 for (i = 1; i <= m; i++) { bh = read(); if (bh == 1) { x = read(); y = read(); z = read(); chen(x, y, 1, n, 1, z); } else if (bh == 2) { x = read(); y = read(); z = read(); add(x, y, 1, n, 1, z); } else if (bh == 3) { x = read(); y = read(); printf("%lld\n", getans(x, y, 1, n, 1)); } } return 0; } ``` --- [HihoCoder 1078 线段树的区间修改](https://vjudge.net/problem/HihoCoder-1078) 假设货架上从左到右摆放了 $N$ 种商品,并且依次标号为 $1$ 到 $N$,其中标号为 $i$ 的商品的价格为 $Pi$。小 Hi 的每次操作分为两种可能,第一种是修改价格:小 Hi 给出一段区间 $[L, R]$ 和一个新的价格 $\textit{NewP}$,所有标号在这段区间中的商品的价格都变成 $\textit{NewP}$。第二种操作是询问:小 Hi 给出一段区间 $[L, R]$,而小 Ho 要做的便是计算出所有标号在这段区间中的商品的总价格,然后告诉小 Hi。 参考代码 ```cpp #include
int n, a[100005], d[270000], b[270000]; void build(int l, int r, int p) { // 建树 if (l == r) { d[p] = a[l]; return; } int m = l + ((r - l) >> 1); build(l, m, p << 1), build(m + 1, r, (p << 1) | 1); d[p] = d[p << 1] + d[(p << 1) | 1]; } void update(int l, int r, int c, int s, int t, int p) { // 更新,可以参考前面两个例题 if (l <= s && t <= r) { d[p] = (t - s + 1) * c, b[p] = c; return; } int m = s + ((t - s) >> 1); if (b[p]) { d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m); b[p << 1] = b[(p << 1) | 1] = b[p]; b[p] = 0; } if (l <= m) update(l, r, c, s, m, p << 1); if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1); d[p] = d[p << 1] + d[(p << 1) | 1]; } int getsum(int l, int r, int s, int t, int p) { // 取得答案,和前面一样 if (l <= s && t <= r) return d[p]; int m = s + ((t - s) >> 1); if (b[p]) { d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m); b[p << 1] = b[(p << 1) | 1] = b[p]; b[p] = 0; } int sum = 0; if (l <= m) sum = getsum(l, r, s, m, p << 1); if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1); return sum; } int main() { std::ios::sync_with_stdio(0); std::cin >> n; for (int i = 1; i <= n; i++) std::cin >> a[i]; build(1, n, 1); int q, i1, i2, i3, i4; std::cin >> q; while (q--) { std::cin >> i1 >> i2 >> i3; if (i1 == 0) std::cout << getsum(i2, i3, 1, n, 1) << std::endl; else std::cin >> i4, update(i2, i3, i4, 1, n, 1); } return 0; } ``` --- [2018 Multi-University Training Contest 5 Problem G. Glad You Came](https://acm.hdu.edu.cn/showproblem.php?pid=6356) "解题思路" 维护一下每个区间的永久标记就可以了,最后在线段树上跑一边 DFS 统计结果即可。注意打标记的时候加个剪枝优化,否则会 TLE。 --- ## 线段树合并 ### 过程 顾名思义,线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。它常常被用于维护树上或是图上的信息。 --- 显然,我们不可能真的每次建满一颗新的线段树,因此我们需要使用上文的动态开点线段树。 线段树合并的过程本质上相当暴力: --- 假设两颗线段树为 A 和 B,我们从 1 号节点开始递归合并。 递归到某个节点时,如果 A 树或者 B 树上的对应节点为空,直接返回另一个树上对应节点,这里运用了动态开点线段树的特性。 如果递归到叶子节点,我们合并两棵树上的对应节点。 最后,根据子节点更新当前节点并且返回。 --- #### 线段树合并的复杂度 显然,对于两颗满的线段树,合并操作的复杂度是 $O(n\log n)$ 的。但实际情况下使用的常常是权值线段树,总点数和 $n$ 的规模相差并不大。并且合并时一般不会重复地合并某个线段树,所以我们最终增加的点数大致是 $n\log n$ 级别的。这样,总的复杂度就是 $O(n\log n)$ 级别的。当然,在一些情况下,可并堆可能是更好的选择。 --- ### 实现 ```cpp int merge(int a, int b, int l, int r) { if (!a) return b; if (!b) return a; if (l == r) { // do something... return a; } int mid = (l + r) >> 1; tr[a].l = merge(tr[a].l, tr[b].l, l, mid); tr[a].r = merge(tr[a].r, tr[b].r, mid + 1, r); pushup(a); return a; } ``` --- ### 例题 [luogu P4556 \[Vani 有约会\] 雨天的尾巴/【模板】线段树合并](https://www.luogu.com.cn/problem/P4556) 解题思路 线段树合并模板题,用差分把树上修改转化为单点修改,然后向上 dfs 线段树合并统计答案即可。 --- 参考代码 ```cpp #include
using namespace std; int n, fa[100005][22], dep[100005], rt[100005]; int sum[5000005], cnt = 0, res[5000005], ls[5000005], rs[5000005]; int m, ans[100005]; vector
v[100005]; void update(int x) { if (sum[ls[x]] < sum[rs[x]]) { res[x] = res[rs[x]]; sum[x] = sum[rs[x]]; } else { res[x] = res[ls[x]]; sum[x] = sum[ls[x]]; } } int merge(int a, int b, int x, int y) { if (!a) return b; if (!b) return a; if (x == y) { sum[a] += sum[b]; return a; } int mid = (x + y) >> 1; ls[a] = merge(ls[a], ls[b], x, mid); rs[a] = merge(rs[a], rs[b], mid + 1, y); update(a); return a; } int add(int id, int x, int y, int co, int val) { if (!id) id = ++cnt; if (x == y) { sum[id] += val; res[id] = co; return id; } int mid = (x + y) >> 1; if (co <= mid) ls[id] = add(ls[id], x, mid, co, val); else rs[id] = add(rs[id], mid + 1, y, co, val); update(id); return id; } void initlca(int x) { for (int i = 0; i <= 20; i++) fa[x][i + 1] = fa[fa[x][i]][i]; for (int i : v[x]) { if (i == fa[x][0]) continue; dep[i] = dep[x] + 1; fa[i][0] = x; initlca(i); } } int lca(int x, int y) { if (dep[x] < dep[y]) swap(x, y); for (int d = dep[x] - dep[y], i = 0; d; d >>= 1, i++) if (d & 1) x = fa[x][i]; if (x == y) return x; for (int i = 20; i >= 0; i--) if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i]; return fa[x][0]; } void cacl(int x) { for (int i : v[x]) { if (i == fa[x][0]) continue; cacl(i); rt[x] = merge(rt[x], rt[i], 1, 100000); } ans[x] = res[rt[x]]; if (sum[rt[x]] == 0) ans[x] = 0; } int main() { ios::sync_with_stdio(0); cin >> n >> m; for (int i = 0; i < n - 1; i++) { int a, b; cin >> a >> b; v[a].push_back(b); v[b].push_back(a); } initlca(1); for (int i = 0; i < m; i++) { int a, b, c; cin >> a >> b >> c; rt[a] = add(rt[a], 1, 100000, c, 1); rt[b] = add(rt[b], 1, 100000, c, 1); int t = lca(a, b); rt[t] = add(rt[t], 1, 100000, c, -1); rt[fa[t][0]] = add(rt[fa[t][0]], 1, 100000, c, -1); } cacl(1); for (int i = 1; i <= n; i++) cout << ans[i] << endl; return 0; } ``` --- ## 线段树分裂 ### 过程 线段树分裂实质上是线段树合并的逆过程。线段树分裂只适用于有序的序列,无序的序列是没有意义的,常用在动态开点的权值线段树。 注意当分裂和合并都存在时,我们在合并的时候必须回收节点,以避免分裂时会可能出现节点重复占用的问题。 --- 从一颗区间为 $[1,N]$ 的线段树中分裂出 $[l,r]$,建一颗新的树: 从 1 号结点开始递归分裂,当节点不存在或者代表的区间 $[s,t]$ 与 $[l,r]$ 没有交集时直接回溯。 当 $[s,t]$ 与 $[l,r]$ 有交集时需要开一个新结点。 当 $[s,t]$ 包含于 $[l,r]$ 时,需要将当前结点直接接到新的树下面,并把旧边断开。 --- ### 线段树分裂的复杂度 可以发现被断开的边最多只会有 $\log n$ 条,所以最终每次分裂的时间复杂度就是 $O(\log n)$,相当于区间查询的复杂度。 --- ### 实现 ```cpp void split(int &p, int &q, int s, int t, int l, int r) { if (t < l || r < s) return; if (!p) return; if (l <= s && t <= r) { q = p; p = 0; return; } if (!q) q = New(); int m = s + t >> 1; if (l <= m) split(ls[p], ls[q], s, m, l, r); if (m < r) split(rs[p], rs[q], m + 1, t, l, r); push_up(p); push_up(q); } ```