首页 > 编程语言 >算法 - 线段树学习笔记

算法 - 线段树学习笔记

时间:2022-10-20 02:11:04浏览次数:69  
标签:node int 线段 tree 笔记 start 算法 区间 节点

前言:

此文章为线段树基础知识可供学习参考


咳咳,进入正题:

我们在做题的时候可能会遇到 给定一个数组 同时给出一个值进行修改 或是 区间性的操作

这里以单点修改和区间查询为例子:

我们每次枚举一遍答案可能会非常的慢,时间分别可能为 \(O(1)\), \(O(n)\)

或许会有人说 用前缀和 区间求和就会降为 \(O(1)\) 但是区间求改 变为了\(O(n)\) 那有没有可能平均一下 将两个情况都成 \(O(\log n)\)

答案肯定是有的 : 那就是线段树———— Segment tree

线段树学习分为 \(3\) 个阶段 --------- 自我总结 \(4\) 个知识点如下:
(我会根据以下的顺序进行续写)

1.1: 建树;

2.1: 单点修改;

 2.1.1:  单点增值

3.1: 区间修改;

 3.1.1:  区间求和;
 3.1.2:  区间增值;
 3.1.3:  区间查询

4.1 : lazy标记 *

1.1: 建树:

对于线段树的编写 第一步的重点就是建树 此后所有进行的更改均需第一步建树

顾名思义: 建树从字面意义上来讲就是 数组以树的方式存储下来。

举个例子:

令 \(a_{n} = \left\{1,3,5,7,9,11\right\}\)

同时再开一个数组 \(tree[4 \cdot n]\) (这里对于线段树来说一般需要开 \(4 \cdot n\) 的空间 \(n\) 为原数组大小)

核心思想:每次进行对数组二分 分为 L R 两个分叉 每个叶子的父节点为其子节点的和

下面我将以图的形式把 \(a\) 数组所对应的 \(tree\) 展现出来 方便理解;

P.s.图为b站某up主的,仅为参考

此图即为 \(a\) 数组所对应的 \(tree\) 在这颗树上每个父节点即为其右上角所对应的区间和

相应代码如下:

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\) 数组 的 组:

    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

(正常来说可能会出现非满二叉树的情况我们只需要 其余空节点记为 \(0\) 即可方便查询)

以上为建树的所有内容。


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) 的情况 不过出题人不会那么良心的。

因此 我们需要加速 便可以用线段树来做。

大体的思路 : 从最高父节点遍历 通过给定的下标确定数所在的区间 进行深层搜索 直到找到为止)。

我们还是以图的形式论述,更加直观:

对于 \(3\) 这个下标 首先找到对应区间 发现 :

\(2 < 3 < 5\) 。

所以 区间便到了 \([ 3 \sim 5 ]\)。

此时我们可以发现 继续往下搜索 通过比对 :

$ 3 < 4 < 5$。

因此 区间便到了 \([ 3 \sim 4 ]\)。

发现依然没有找到答案 所以继续往下;直到节点 \([ 3 ]\) 停止搜索 将其数值更新 。

可是更新之后 此棵树的左半部分所有父节点收到了影响 违背了线段树的原理:

每个叶子的父节点为其子节点的和。

所有 最后我们还需自下而上进行更新节点操作。

代码如下:

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 单点增值

这里 单点增值 指的是给定一个下标 使该下标上的数增加 \(n\)。

其实原理单点修改完全一样 不多论述,将其代码改一处地方即可:

if (start == end)
    {
        arr[X] += K;
        tree[node] += K;
    }

P.s: 每一次增值后需将 \(lazy\) 下放 ( \(lazy\) 数组具体后面说

2.1 整节 结束


3.1 区间性问题


3.1.1 : 区间求和

大意为 :给定一个左端点 给定一个右端点 求得左右端点之间所有数的和。

继续接着线段树的思路说 :

在建完树之后 我们可以得知 父节点即为 --- 左儿子 \(+\) 右儿子

因此只需要判断一下 \(\text{L}\), \(\text{R}\) 所包含的区间在哪即可 从最底层遍历 直到 $start = L $, \(end = R\)

代码如下:

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: 区间增值 ( 雾

意义: 给定 \(\text{L}\), \(\text{R}\) 将其所有值相加。

如果按照线段树进行之间修改 时间复杂度会飞起QAQ

所以,这里提前引入一个新东西 : lazy标记。

\(lazy\) 标记顾名思义:懒标记..

思路 : 在每次遍历子树时 若找到 \(\text{L}\), \(\text{R}\) 所在的区间 则在当前父节点打上标记 将其值更新为:$val + val \cdot up $ 即为更新后的值。 每次遍历时将标记下放 , 取消先前标记 以后遍历时标记过的点直接跳过即可 直至更新的最深层节点。

这样子操作之后 大大降低了时间复杂度。

代码如下:

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;
    }
}

这里 \(add\) 为 \(lazy\) 数组 这里包含了下放操作 由此可知没此更新 \(lazy\) 数组之后 重新赋值为 \(0\) (删除标记)

$ m $ 为此修改区间长度


3.1.2: 区间增值

讲完了 \(lazy\) 数组之后其实区间增值就很好理解了。

和区间求和一样 分左右子树根据区间进行遍历 每一次递归后再更新其父节点的值

代码如下:

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];//更新节点
}

这里代码给出的是 将 \(L \sim R\) 这个区间所有数加一 ( 非常容易理解是不是qwq


3.1.3: 区间查询

我想 看到这里 大家对线段树已经 有了很深的理解了 区间查询我就不多说了 和所有区间操作一样。

口糊一下思路: 遍历子树 直到相对应的节点 输出即可 若直接有树上修改操作 则每次遍历需 pushdown


好啦 线段树的学习就到这了 以上所有的乘除操作均可以换成位运算加快效率

最后给出一个洛谷官方的 线段树练习题单:

戳这里哦

标签:node,int,线段,tree,笔记,start,算法,区间,节点
From: https://www.cnblogs.com/S1gMa/p/16808365.html

相关文章

  • MySQL多表&事务课堂笔记
    今日内容1.多表查询2.事务3.DCL多表查询:*查询语法: select 列名列表 from 表名列表 where....*准备sql #创建部门表 CREATETABLEdept( idINT......
  • MySQL基础课堂笔记
    今日内容数据库的基本概念MySQL数据库软件安装卸载配置SQL数据库的基本概念1.数据库的英文单词:DataBase简称:DB2.什么数据库? *用于存储和管理数......
  • MySQL约束课堂笔记
    今日内容1.DQL:查询语句 1.排序查询 2.聚合函数 3.分组查询 4.分页查询2.约束3.多表之间的关系4.范式5.数据库的备份和还原DQL:查询语句1.排序查询......
  • 排序算法
    排序算法一、简介sort排序是计算机中重要的一种操作,作用是讲一个数据元素重新排列成一个有序的序列。排序算法在数据结构中相当重要。二、算法复杂度1.时间复杂度时间......
  • guli笔记
    模块一(讲师管理模块)后端(准备工作)准备工作搭建数据库数据库设计搭建搭建项目项目结构的搭建创建父工程pom类型(父工程)依赖管理版本和公共依赖搭建子模块......
  • 优化算法|MOAVOA:一种新的多目标人工秃鹰优化算法(Matlab代码实现)
    ......
  • 超几何函数学习笔记
    参考:具体数学1.超几何函数HYPERGEOMETPICFUNCTIONS定义:\[F\left(\begin{gathered}a_1,\cdots,a_m\\b_1,\cdots,b_n\end{gathered}\middle|z\right)=F(a_1,\cdots,a_m......
  • Linux实战笔记_CentOS 7中格式化磁盘
    fdisk-l#检查是否添加成功(添加一块磁盘并重启计算机后)fdisk/dev/sdb#格式化磁盘mount/dev/sdb1/opt#挂载到/opt目录df-h......
  • Linux实战笔记_ 如何远程访问Kali?
    注:基于2018年安装的kali版本。启动ssh服务/etc/init.d/sshstart或servicesshstart#启动ssh服务/etc/init.d/sshstatus或者servicesshstatus#查看ssh服......
  • 《计算机组成与设计:硬件/软件接口》第三章 计算机的算术运算笔记
    title:第三章计算机的算术运算笔记date:2022-10-18周二摘要:本文是《计算机组成与设计:硬件/软件接口》第三章的学习记录,其中辅以cs61c以及csapp的部分内容。mindm......