线段树的各种扩展
线段树的各种扩展
前情提要线段树, 算法竞赛掌管区间的神, 权值数据结构水各种题.
小技巧
动态开点
这篇博客所有的线段树扩展都基于动态开点, 所以先讲一下.
先申请一个很长的数组, 需要新结点就从数组里申请.
这是一种内存池思想, 可以避免内存的多次申请与释放 (更多的是可以避免指针), 在有文字记载的首任指针神教教主一扶苏一的代码中, 甚至会直接把这个很长的数组称为 \(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
是原数组去重的.