线段树, 算法竞赛掌管区间的神
线段树, 算法竞赛掌管区间的神
什么是线段树
上回讲树状数组的时候说过, 是一种分治数据结构, 把区间从中间劈开, 通过左子区间和右子区间的合并得到大区间.
上回的树状数组及其各种扩展.
线段树长什么样
观察线段树.
再次思考, 把区间从中间劈开, 通过左子区间和右子区间的合并得到大区间.
两个子区间的答案可以变成大区间的答案, 大区间又作为子区间去维护更大区间的答案.
线段树的存储
假设根结点为 \(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; }
无聊的数列
差分, 单点加首项, 区间加公差, 在下一个减去最后一项, 区间查前缀和.
扶苏的问题
这个不难, 打两个标记即可.
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇
· 易语言 —— 开山篇