线段树与树状数组
线段树是一种支持修改、用于维护区间信息的数据结构,可以在
静态区间和可以使用前缀和优化,但如果有修改操作呢?当你更新一个点
基本结构
假设现在有一个大小为
大致思想:最初有一个区间
区间肯定不能
空间分析
现在有一个问题:编号最大为多少?
通过观察,容易发现线段树深度为
详细证明请右转 OI-wiki。
Code
int n, a[N], tr[4 * N]; // Make Tree 建树 void MT (int id, int l, int r) { // 当前节点编号为 id,区间范围 [l, r] if (l == r) { tr[id] = a[l]; return ; } int mid = (l + r) >> 1; MT(id * 2, l, mid), MT(id * 2 + 1, mid + 1, r); tr[id] = tr[id * 2] + tr[id * 2 + 1]; }
复杂度
区间查询
对于上面的例子,我们现在需要查询某些区间的和。
如果是查询
既然
大致做法
- 如果当前遍历到的区间
被查询区间完全包含,那么可以直接计算当前区间对答案的贡献,即 。 - 如果当前遍历到的区间
与查询区间无交集,直接return ;
- 否则,将其分为左右两个区间进行查询,即查询
和 。
时间复杂度分析
做法了解了,接下来就是分析时间复杂度了。
我们可以把
由于查询是一段连续区间,所以当你把所有可以合并的区间都合并之后,线段树每层最多只会有两个区间,时间复杂度为
Code
int qry; // Query 查询 void Query (int id, int l, int r, int x, int y) { // 查询区间 [x, y] if (l >= x && r <= y) { // 当前区间被查询区间完全包含 qry += tr[id]; return ; } if (l > y || r < x) { // 当前区间与查询区间无交集 return ; } int mid = (l + r) >> 1; Query(id * 2, l, mid, x, y), Query(id * 2 + 1, mid + 1, r, x, y); }
单点修改
继续使用上面的例子,如果我们要修改
如图,当你找到区间
如何寻找区间对应节点呢?当我们考虑到一个包含修改目标的区间
总时间复杂度为
Code
// modify 单点修改 void modify (int id, int l, int r, int x, int y) { // 将 a[x] 修改为 y if (l == r) { // 找到修改目标 tr[id] = y; // 直接修改 return ; } int mid = (l + r) >> 1; if (mid >= x) { // 修改目标在左半区间 modify(id * 2, l, mid, x, y); } else { // 修改目标在右半区间 modify(id * 2 + 1, mid + 1, r, x, y); } tr[id] = tr[id * 2] + tr[id * 2 + 1]; // 重新更新 }
区间修改与懒标记
单点修改解决,然后就是区间修改。如果对于区间内每个元素都进行一次单点修改,时间复杂度无法接受,需要使用懒标记。
懒标记 lazy tag
懒标记,顾名思义就是一种十分懒惰的标记,用于临时记录区间操作对于当前节点对应范围造成的影响,当它要访问它的左右儿子时,需要将懒标记下传并更新左右儿子的节点信息,懒标记初始为
引入懒标记,初始状态:
懒标记下传 Code
int lzy[4 * N]; // 将 id[l, r] 的懒标记下传 void pushdown (int id, int l, int r) { int mid = (l + r) >> 1; tr[id * 2] += lzy[id] * (mid - l + 1); // 左区间 tr 更新 tr[id * 2 + 1] += lzy[id] * (r - mid); // 右区间 tr 更新 lzy[id * 2] += lzy[id]; // 左区间 lazy tag 更新 lzy[id * 2 + 1] += lzy[id]; // 右区间 lazy tag 更新 lzy[id] = 0; // 清空当前节点 lazy tag }
有了懒标记,我们就可以实现
操作类似区间查询(将区间
- 如果当前遍历到的区间
被修改区间完全包含,则更新当前节点的懒标记( )和答案( )。 - 如果当前遍历到的区间
与修改区间无交集,直接return ;
- 否则,将其分为左右两个区间各自修改,即修改
和 。
区间修改(加) Code
// modify 区间修改-加 void modify (int id, int l, int r, int x, int y, int z) { // 将 a[x ~ y] 每个元素加 z if (l >= x && r <= y) { // 当前区间被修改区间完全包含 lzy[id] += z, tr[id] += (r - l + 1) * z; return ; } if (l > y || r < x) { // 当前区间与修改区间无交集 return ; } int mid = (l + r) >> 1; pushdown(id, l, r); // 懒标记下传 modify(id * 2, l, mid, x, y, z), modify(id * 2 + 1, mid + 1, r, x, y, z); tr[id] = tr[id * 2] + tr[id * 2 + 1]; // 重新更新当前节点 }
区间修改(赋值) Code
容易发现,一个数只与最后一次对它的赋值操作有关。所以懒标记下传与更新需要一点点的更改。
// 将 id[l, r] 的懒标记下传 void pushdown (int id, int l, int r) { int mid = (l + r) >> 1; tr[id * 2] = lzy[id] * (mid - l + 1); // 左区间 tr 更新 tr[id * 2 + 1] = lzy[id] * (r - mid); // 右区间 tr 更新 lzy[id * 2] = lzy[id]; // 左区间 lazy tag 更新 lzy[id * 2 + 1] = lzy[id]; // 右区间 lazy tag 更新 lzy[id] = 0; // 清空当前节点 lazy tag } // modify 区间修改-赋值 void modify (int id, int l, int r, int x, int y, int z) { // 将 a[x ~ y] 每个元素赋值为 z if (l >= x && r <= y) { // 当前区间被修改区间完全包含 lzy[id] = z, tr[id] = (r - l + 1) * z; return ; } if (l > y || r < x) { // 当前区间与修改区间无交集 return ; } int mid = (l + r) >> 1; pushdown(id, l, r); // 懒标记下传 modify(id * 2, l, mid, x, y, z), modify(id * 2 + 1, mid + 1, r, x, y, z); tr[id] = tr[id * 2] + tr[id * 2 + 1]; // 重新更新当前节点 }
老师做的视频
推销一下:
优化
- 叶子节点没有儿子,所以懒标记不用下传到叶子节点。
- 根据儿子更新当前节点的操作可以写一个函数
pushup
,增加代码可读性。 - 标记永久化:在确定懒标记不会发生溢出的情况下,可以选择不清空懒标记,只计算对答案的贡献(用处不大,主要用于可持久化数据结构)。
- 动态开点:左右儿子不设为
和 ,而是设为线段树中没有出现的最小 。线段树第一层节点数最多为 ,第二层最多为 ,第三层最多为 ,依次类推,可以计算出节点数量最多为 ,可以节省空间。
树状数组,是一种用于维护单点修改和区间查询的数据结构。
一些要求
普通树状数组要求维护的信息和运算满足结合律且可差分(具有逆运算),包括加法、乘法、异或等。
注意:
- 模意义下乘法若要可差分,需要保证每个元素都存在逆元。
- 区间极值、最大公因数等无法用普通树状数组维护(但有办法解决,详见 OI-wiki)。
初步感知
假设现在有一个数组
这就是树状数组,可以在预处理出一些区间的和之后,把一段前缀化为不超过
上图中的
区间管辖范围
树状数组规定:右端点为
令 lowbit(i)
为 lowbit(x) = x & (-x)
,这个东西和原、反、补码有关,这里就不详说了,具体可以看 OI-wiki。
构建树状数组
强调:必须确保维护的信息是可差分的,例如求区间和,而区间极值则不可以用树状数组进行维护。
现在给定一个大小为
可以想到一种比较简单的方法:对于每个
通过找规律,可以发现这个的时间复杂度在
验证 Code
#include <iostream> using namespace std; int lowbit (int x) { return x & -x; } int n, ans; int main () { ios::sync_with_stdio(0), cin.tie(0); cin >> n; for (int i = 1; i <= n; i++) { ans += lowbit(i); } cout << ans; return 0; }
Code
void MT () { for (int i = 1; i <= n; i++) { for (int j = i - lowbit(i) + 1; j <= i; j++) { // 统计范围内的整数和 tr[i] += a[j]; } } }
当然如果你提前用前缀和进行预处理的话,可以直接
区间查询
举个例子,现在要求区间
根据区间管辖范围,我们可以轻松地推出前缀和的求法:函数 Query(x)
求解的是前缀 Query(x - lowbit(x))
,如果现在 x = 0
,即已将前缀完全求解,那么直接返回
Code
inline int lowbit (int x) { return x & -x; // 位运算求 lowbit } int n, tr[N]; // tr 就是 c 数组 int Query (int x) { return (x ? tr[x] + Query(x - lowbit(x)) : 0); // 分类讨论 }
单点修改
树状数组の一些性质
令
- 对于任意
,要么 ,要么 。 - 对于任意
,有 。 - 对于任意
,有 。
详细证明见 OI-wiki。
假设现在要将
为了快速更新
根据如上几个性质,可以推出更新方法:当
Code
void modify (int x, int y) { // 单点修改 if (x > n) { // 更新完毕 return ; // 返回 } tr[x] += y, modify(x + lowbit(x), y); // 更新 }
复杂度
区间修改
开两个树状数组,利用差分维护即可。
本文作者:wnsyou の blog
本文链接:https://www.cnblogs.com/wnsyou-blog/p/seg_and_fenwick.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步