线段树, 算法竞赛掌管区间的神
线段树, 算法竞赛掌管区间的神
什么是线段树
上回讲树状数组的时候说过, 是一种分治数据结构, 把区间从中间劈开, 通过左子区间和右子区间的合并得到大区间.
上回的树状数组及其各种扩展.
线段树长什么样
观察线段树.
再次思考, 把区间从中间劈开, 通过左子区间和右子区间的合并得到大区间.
两个子区间的答案可以变成大区间的答案, 大区间又作为子区间去维护更大区间的答案.
线段树的存储
假设根结点为 \(1\), 按照BFS宽搜遍历树.
可以发现, \(x\) 的左儿子是 \(2x\), 右儿子是 \(2x + 1\).
然后又可以发现, 结点 \(1\) 到 \(15\) 再加上 \(0\) 号 (虽然很少有人用), \(16\) 个结点恰好等于 \(2 * 8\) 也就是 \(2N\).
但是考虑 \(N \notin \{2^{x} | x \in \N\}\) (这个表达式是说 \(N\) 不是 \(2\) 的自然数幂), 下面会多一排, 而多的一排的长度是 \(2N\) (这是二叉树的性质), 所以线段树的空间是 \(2N + 2N = 4N\).
所以我们可以申请一个数组存储结点值.
ll val[NN << 2]; // NN << 2 = NN * 4
单点修改
首先, 不想讲建树是因为Defad不怎么喜欢建树, Defad喜欢 \(N\) 次单点修改当做建树, 都是 \(N\log{N}\), 还能少写一个函数.
现在我们要修改 \(4\), 观察线段树, 哪些结点和 \(4\) 有关?
可以发现, 首先 \(4\) 在根的左儿子, 然后到右儿子, 再到右儿子.
那么就可以通过区间进行递归修改.
void chg(int x, int l, int r, int I, ll k) {
if (l == r) { // 区间 l 和 r 相等说明是叶子了
val[x] += k;
} else {
int m(l + r >> 1); // 左右分割点, (l + r) / 2
if (I <= m) // I 在左子区间
chg(x << 1, l, m, I, k); // 左儿子 2x
else // I 在右子区间
chg(x << 1 | 1, m + 1, r, I, k); // 右儿子 2x + 1
val[x] = val[x << 1] + val[x << 1 | 1]; // 合并答案
}
}
区间修改
很容易想到, 把区间递归成单点进行修改, 但是这样的效率太慢了.
void chg(int x, int l, int r, int L, int R, ll k) {
if (l == r) {
val[x] += k;
} else {
int m(l + r >> 1);
if (L <= m) // 包含在左子区间
chg(x << 1, l, m, L, R, k);
if (m + 1 <= R) // 包含右子区间
chg(x << 1 | 1, m + 1, r, L, R, k);
val[x] = val[x << 1] + val[x << 1 | 1]; // 合并答案
}
}
观察线段树 (这次观察上次树状数组的博客里的图).
要修改 \(1\) 到 \(7\), 哪些结点完全包含 \(1\) 到 \(7\), 不需要往下递归了呢?
可以发现, 递归到 \([1, 4]\) 和 \([5, 6]\) 和 \([7, 7]\) 就不需要往下递归了.
但是此时没有递归到的在区间内的结点的值是错误的.
我们引入一个懒标记.
ll tag[NN << 2]; // 叶子其实无需懒标记, 但是那样还要特判
可以想到懒标记在操作子区间时需要下传.
inline
void pushdown(int x, int l, int r, int m) { // 传入 m 就不用再算了
val[x << 1] += tag[x] * (m - l + 1); // 左子区间是 l 到 m
val[x << 1 | 1] += tag[x] * (r - m); // 右子区间是 m + 1 到 r
tag[x << 1] += tag[x]; // 标记下传
tag[x << 1 | 1] += tag[x]; // 标记下传
tag[x] = 0LL; // 清空标记
}
所以区间修改的代码就写出来了.
void chg(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; // 打标记
} else {
int m(l + r >> 1);
pushdown(x, l, r, m); // 下传标记
if (L <= m)
chg(x << 1, l, m, L, R, k);
if (m + 1 <= R)
chg(x << 1 | 1, m + 1, r, L, R, k);
val[x] = val[x << 1] + val[x << 1 | 1];
}
}
区间查询
学会了修改就很容易想到查询, 合并答案即可.
ll qry(int x, int l, int r, int L, int R) {
if (L <= l && r <= R) {
return val[x];
} else {
int m(l + r >> 1); l s(0LL);
pushdown(x, l, r, m);
if (L <= m)
s += qry(x << 1, l, m, L, R);
if (m + 1 <= R)
s += qry(x << 1 | 1, m + 1, r, L, R);
return s;
}
}
例题
A Simple Problem with Integers
板子.
线段树2
这个不算难, 就是先下放乘标记再下放加标记, 加标记在乘时也要乘.
给一个pushdown(x, l, r, m)
吧.
inline
void addmul(int x, int l, int r, ll f, ll g) {
if (g ^ 1LL) {
val[x] = val[x] * g % Mod;
add[x] = add[x] * g % Mod;
mul[x] = mul[x] * g % Mod;
}
if (f ^ 0LL) {
val[x] = (val[x] + f * (r - l + 1)) % Mod;
add[x] = (add[x] + f * (r - l + 1)) % Mod;
}
}
inline
void pushdown(int x, int l, int r, int m) {
addmul(x << 1, l, m, add[x], mul[x]);
addmul(x << 1 | 1, m + 1, r, add[x], mul[x]);
add[x] = 0LL;
mul[x] = 1LL;
}
无聊的数列
差分, 单点加首项, 区间加公差, 在下一个减去最后一项, 区间查前缀和.
扶苏的问题
这个不难, 打两个标记即可.