算法 - 线段树学习笔记
前言
此文章为线段树基础知识可供学习参考
咳咳,进入正题:
我们在做题的时候可能会遇到 给定一个数组 同时给出一个值进行修改 或是 区间性的操作
这里以单点修改和区间查询为例子:
我们每次枚举一遍答案可能会非常的慢,时间分别可能为
或许会有人说 用前缀和 区间求和就会降为
答案肯定是有的 : 那就是线段树———— Segment tree
。
线段树学习分为
1.1: 建树:
对于线段树的编写 第一步的重点就是建树 此后所有进行的更改均需第一步建树
顾名思义: 建树从字面意义上来讲就是 数组以树的方式存储下来。
举个例子:
令
同时再开一个数组
核心思想:每次进行对数组二分 分为 L R 两个分叉 每个叶子的父节点为其子节点的和
下面我将以图的形式把
P.s.图为b站某up主的,仅为参考
此图即为
相应代码如下:
void build_tree(int node, int start, int end)//node为本节点
{
if (start == end)//start为区间的左端 , end即为右端
tree[node] = arr[start];//如果start == end 说明已经遍历到本棵树的最深节点 我们就将其存储下来
else
{
int mid = (start + end) / 2;//每次将其二分
int left_node = 2 * node + 1;//这里对应着其节点的左儿子
int right_node = 2 * node + 2;//则这里为右儿子
build_tree(left_node, start, mid);
build_tree(right_node, mid + 1, end);//递归进行求解
tree[node] = tree[left_node] + tree[right_node];//这里说明本node的节点为其左右儿子的和
}
}
最后,我们只需按照 层序遍历 将其输出, 答案即为
a数组 1 3 5 7 9 11
对应下标: 0 1 2 3 4 5
tree数组 36 9 27 4 5 16 11 1 3 0 0 7 9 0 0
对应下标 : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
(正常来说可能会出现非满二叉树的情况我们只需要 其余空节点记为
以上为建树的所有内容
2.1 单点修改
单点修改分为:
1.单点更改。
2.单点增值。
什么是单点更改呢? 举个栗子:
给出一组数 1 3 5 7 9 11
下标 0 1 2 3 4 5
请将下标为 3 的数 变为 9
这里输出的是: 1 3 5 9 9 11
根据常理来说 从头开始遍历 一个一个枚举 时间复杂度为 O(n)。
若有时可以为 O(1) 的情况 不过出题人不会那么良心的。
因此 我们需要加速 便可以用线段树来做。
大体的思路 : 从最高父节点遍历 通过给定的下标确定数所在的区间 进行深层搜索 直到找到为止
我们还是以图的形式论述,更加直观:
对于
所以 区间便到了
此时我们可以发现 继续往下搜索 通过比对 :
因此 区间便到了
发现依然没有找到答案 所以继续往下;直到节点
可是更新之后 此棵树的左半部分所有父节点收到了影响 违背了线段树的原理:
每个叶子的父节点为其子节点的和。
所有 最后我们还需自下而上进行更新节点操作。
代码如下:
void arr_tree(int node, int start, int end, int X, int K)//将 X 的值变为 K
{
if (start == end)//如果找到
{
arr[X] = K;//更新原数组
tree[node] = arr[X];同时更新树
}
else
{
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
if (X >= start && X <= mid)//如果 X 比中值小则遍历左区间
arr_tree(left_node, start, mid, X, K);
else //反之则遍历右区间
arr_tree(right_node, mid + 1, end, X, K);
tree[node] = tree[left_node] + tree[right_node];//更新每个更改后的父节点
}
}
以上则为单点修改所有内容。
2.1.1 单点增值
这里 单点增值 指的是给定一个下标 使该下标上的数增加
其实原理单点修改完全一样 不多论述,将其代码改一处地方即可:
if (start == end)
{
arr[X] += K;
tree[node] += K;
}
P.s: 每一次增值后需将
2.1 整节 结束
3.1 区间性问题
3.1.1 : 区间求和
大意为 :给定一个左端点 给定一个右端点 求得左右端点之间所有数的和。
继续接着线段树的思路说 :
在建完树之后 我们可以得知 父节点即为 --- 左儿子
因此只需要判断一下
代码如下:
int query_tree(int node, int start, int end, int L, int R)
{
if (R < start || L > end)//若区间不覆盖此数组之间返回0
return 0;
else if (L <= start && end <= R)//剪枝
return tree[node];
else if (start == end)
return tree[node];//若遍历到最深节同样返回值
else
{
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
int sum_left = query_tree(left_node, start, mid, L, R);
int sum_right = query_tree(right_node, mid + 1, end, L, R);//分别遍历左右子树 进行求和
return sum_left + sum_right;//返回左右子树相加结果
}
}
3.1.2: 区间增值 ( 雾
意义: 给定
如果按照线段树进行之间修改 时间复杂度会飞起QAQ
所以,这里提前引入一个新东西 : lazy标记
思路 : 在每次遍历子树时 若找到
这样子操作之后 大大降低了时间复杂度。
代码如下:
void push_down(int node, int m)
{
if (add[node])
{
add[2 * node + 1] += add[node];
add[2 * node + 2] += add[node];
tree[2 * node + 1] += (m - (m >> 1)) * add[node];
tree[2 * node + 2] += (m >> 1) * add[node];
add[node] = 0;
}
}
这里
3.1.2: 区间增值
讲完了
和区间求和一样 分左右子树根据区间进行遍历 每一次递归后再更新其父节点的值
代码如下:
void arr_tree(int node, int start, int end, int L, int R)
{
if (L <= start && end <= R)
{
add[node] += 1;
tree[node] = end - start + 1 - tree[node];
return;
}
push_down(node, end - start + 1);
//本节点进行标记 并且做下放操作 长度即为 end - start + 1
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
if (L <= mid)
arr_tree(left_node, start, mid, L, R);
if (R > mid)
arr_tree(right_node, mid + 1, end, L, R);
tree[node] = tree[left_node] + tree[right_node];//更新节点
}
这里代码给出的是 将
3.1.3: 区间查询
我想 看到这里 大家对线段树已经 有了很深的理解了 区间查询我就不多说了 和所有区间操作一样。
口糊一下思路: 遍历子树 直到相对应的节点 输出即可 若直接有树上修改操作 则每次遍历需 pushdown 。
结语
好啦 线段树的学习就到这了 以上所有的乘除操作均可以换成位运算加快效率
最后给出一个洛谷官方的 线段树练习题单:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通