线段树的各种扩展
线段树的各种扩展
前情提要线段树, 算法竞赛掌管区间的神, 权值数据结构水各种题.
小技巧
动态开点
这篇博客所有的线段树扩展都基于动态开点, 所以先讲一下.
先申请一个很长的数组, 需要新结点就从数组里申请.
这是一种内存池思想, 可以避免内存的多次申请与释放 (更多的是可以避免指针), 在有文字记载的首任指针神教教主一扶苏一的代码中, 甚至会直接把这个很长的数组称为 \(pool\).
int rt = 0, cntt = 0; int ls[NN << 5], rs[NN << 5]; T val[NN << 5]; // 这个按照需要来写类型, 下面所有T都是同样 inline // 这个函数是申请结点用的, 一般直接写++cntt, 不写函数 int newnode() { return ++cntt; }
为什么是 \(32\) 倍空间呢?
每次新建都需要从根到叶子建一串结点, 就是 \(\log V\) 个, 所以开\(32\) (int
的最大长度) 倍空间即可 (当然 long long
是 \(64\)).
很显然修改可能要新建, 但是询问和线段树上二分是不需要新建的.
放一下代码, 单点修改区间查询和线段树上二分 (这篇博客介绍的东西都是只需要单点修改的, 涉及区间修改也只需要打上标记即可).
传入指针是因为C语言没有引用这种东西, 有条件建议是传引用.
传指针是因为不想传引用, 不代表是指针线段树. --指针神教教主Defad
void update(int *x, int l, int r, int I, T k) { if (!*x) *x = ++cntt; if (l == r) { val[*x] += k; return; } else { int m = l + r >> 1; if (I <= m) update(ls + *x, l, m, I, k); else update(rs + *x, m + 1, r, I, k); return pushup(*x); } }
T query(int x, int l, int r, int L, int R) { if (L <= l && r <= R) { return val[x]; } else { int m = l + r >> 1; T s = 0; // 置空 if (L <= m) s += query(ls[x], l, m, L, R); if (m + 1 <= R) // 3F曾经某次At-ABC因为这里写成 s == query 导致罚时, 希望同学们引以为戒 s += query(rs[x], m + 1, r, L, R); return s; }
int kth(int x, int l, int r, T k) { if (l == r) { return l; } else { int m = l + r >> 1; if (k <= val[ls[x]]) return kth(ls[x], l, m, k); else if (k <= val[x]) return kth(rs[x], m + 1, r, k - val[ls[x]]); else // 这个根据题目来, 有时是 -1, 有时是 r 或 r + 1 return -1; } }
不知有没有同学想过为什么这里访问了 \(ls\), 但是有可能 \(ls\) 没有用过就是 \(0\), 反正Defad是想过的.
因为 \(0\) 是始终都不会被修改的, 所以一直有 \(ls_{0} = 0, rs_{0} = 0, val_{0} = 0\), 所以不会出问题.
但是如果用指针线段树就要注意了, 没有初始化的是 NULL
, 访问空指针就是RE, 直接挂大分.
标记永久化
线段树上的一种技巧, 不需要进行 pushdown
就能快很多, 最重要的是在标记难以下传时可以直接维护标记.
首先区间修改的时候打标记, 这里用普通线段树举例.
inline void pushup(int x, int l, int r) { val[x] = val[x << 1] + val[x << 1 | 1] + tag[x] * (r - l + 1); }
void update(int x, int l, int r, int L, int R, ll k) { if (L <= l && r <= R) { val[x] += k * (r - l + 1); tag[x] += k; return; } else { int m = l + r >> 1; if (L <= m) update(x << 1, l, m, L, R, k); if (m + 1 <= R) update(x << 1 | 1, m + 1, r, L, R, k); return pushup(x, l, r); } }
然后查询时带着标记查询.
ll query(int x, int l, int r, int L, int R, ll k) { if (L <= l && r <= R) { return val[x] + k * (r - l + 1); } else { int m = l + r >> 1; ll s = 0LL; if (L <= m) s += query(x << 1, l, m, L, R, k + tag[x]); if (m + 1 <= R) s += query(x << 1 | 1, m + 1, r, L, R, k + tag[x]); return s; } }
动态开点权值线段树
好消息是不需要离散化, 坏消息是容易写挂.
以上次的平衡树举例.
直接把区间长度开到 \([-1 * 10^{7}, 1 * 10^{7}]\), 然后把上次的代码的离散化删掉, 提交.
线段树分裂合并
一般用于集合的分裂合并, 所以多数是权值线段树.
线段树合并
合并两个可重集, 这两个可重集分别是用权值线段树维护的.
首先, 肯定要递归整个线段树.
其次, 如果当前可重集 \(x\) 或 \(y\) 的这个结点为空, 就返回非空的.
void merge(int x, int y, int l, int r) { if (!x || !y) return x ^ y; // 返回非空的 if (l == r) { val[x] += val[y]; return x; } else { int m = l + r >> 1; ls[x] = merge(ls[x], ls[y], l, m); rs[x] = merge(rs[x], rs[y], m + 1, r); pushup(x); return x; } }
线段树分裂
把可重集 \(y\) 的所有在 \([L, R]\) 区间内的数放到新的可重集 \(x\), 并在 \(y\) 中删掉这些数.
首先肯定要申请 \(x\) 的结点.
然后重要的, 在区间内就 \(ls, rs, val\) 都要传递, 清空可以偷懒直接给这个结点的父结点的这个儿子变成 \(0\).
int split(int *y, int l, int r, int L, int R) { int x = ++cntt; if (L <= l && r <= R) { ls[x] = ls[*y]; rs[x] = rs[*y]; val[x] = val[*y]; *y = 0; return x; } else { int m = l + r >> 1; if (L <= m) ls[x] = split(ls + *y, l, m, L, R); if (m + 1 <= R) rs[x] = split(rs + *y, m + 1, r, L, R); pushup(*y); pushup(x); return x; } }
板子
需要记录多个根, 使用一个 \(cntr\) 记录当前有多少个根, 分裂出来直接 \(cntr := cntr + 1\) 然后给到 \(rt_{cntr}\) 即可.
持久化线段树
本期唯一不涉及权值的线段树, 只需要在历史版本上单点修改单点查询.
首先这次如果有初始化就必须建树了, 不然就不是版本 \(0\) 了.
inline void pushup(int x) { val[x] = val[ls[x]] + val[rs[x]]; }
void build(int *x, int l, int r) { if (!*x) *x = ++cntt; if (l == r) { val[*x] = a[l]; return; } else { int m = l + r >> 1; build(ls + *x, l, m); build(rs + *x, m + 1, r); return pushup(*x); } }
然后考虑修改.
A. 把原来的线段树复制一遍.
B. 只把要修改的一串修改, 其他的用原来的.
显然, 前者极易MLE, 而后者较省空间.
void update(int *x, int y, int l, int r, int I, ll k) { if (!*x) *x = ++cntt; if (l == r) { val[*x] = k; return; } else { int m = l + r >> 1; if (I <= m) { rs[*x] = rs[y]; update(ls + *x, ls[y], l, m, I, k); } else { ls[*x] = ls[y]; update(rs + *x, rs[y], m + 1, r, I, k); } return pushup(*x); } }
查询如果要新建相同版本, 可以传递一下 \(rt_{h}\) 到 \(rt_{i}\), 把历史版本的根直接给到当前版本.
普通的动态开点线段树单点查询.
ll query(int x, int l, int r, int I) { if (l == r) { return val[x]; } else { int m = l + r >> 1; if (I <= m) return query(ls[x], l, m, I); else return query(rs[x], m + 1, r, I); } }
主席树
动态开点权值线段树, 并不是持久化的, 因为持久化需要支持修改操作, 而主席树的修改操作并不是对于值进行修改, 而是维护数组的.
第一步, 离散化.
第二步, 建树.
这个建树和普通线段树的建树不一样, 要建立 \(N\) 个权值线段树 (显然要动态开点并且每次只新建一串), 第 \(i\) 个权值线段树维护数组 \(a\) 的 \(1\) 到 \(i\) 中出现的所有的数.
指针神教上一任教主Wild-Donkey形容主席树为"批判的继承".
inline void pushup(int x) { val[x] = val[ls[x]] + val[rs[x]]; }
void update(int *x, int y, int l, int r, int I, int k) { if (!*x) *x = ++cntt; if (l == r) { val[*x] = val[y] + k; return; } else { int m = l + r >> 1; if (I <= m) { rs[*x] = rs[y]; update(ls + *x, ls[y], l, m, I, k); } else { ls[*x] = ls[y]; update(rs + *x, rs[y], m + 1, r, I, k); } return pushup(*x); } }
f1 (i, 1, S, 1) { // 这是main里的离散化后的建树部分 update(rt + i, rt[i - 1], 1, N, a[i], 1); }
查询.
线段树上二分, 注意要二分的是 \(x - 1\) 和 \(y\) , 至于为什么可以想一想前缀和.
int kth(int x, int y, int l, int r, int k) { if (l == r) { return l; } else { int m = l + r >> 1; if (k <= val[ls[y]] - val[ls[x]]) return kth(ls[x], ls[y], l, m, k); else if (k <= val[y] - val[x]) return kth(rs[x], rs[y], m + 1, r, k - (val[ls[y]] - val[ls[x]])); else return -1; } }
最后输出的是?
A. a[query(rt[x - 1], rt[y], 1, N, k)]
B. b[query(rt[x - 1], rt[y], 1, N, k)]
显然要选B, 但是Defad在SPOJ上交主席树时在这里也出过逝, 所以也要引以为戒, a
是离散化后的, b
是原数组去重的.
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇
· 易语言 —— 开山篇