草庐IT

线段树的学习

UchihaTsuki 2023-05-05 原文

-1.算法简介

线段树(Segment Tree),几乎是算法竞赛最常用的数据结构,适用于一个序列的区间修改、区间询问交叉进行的问题。

-2.算法讲解

我们假设一个数列1 1 4 5 1 4 4 5,现在我们需要把它从第2个数到第4个数同时加上10,然后再让你求出从第3个数到第6个数中所有书加起来的和,让你用代码写出来,你会怎么办?有人会说:“哎呀,这太简单了吧,直接暴力枚举不就行了?那如果如果我们把数据调大一些呢?问你一百万次,每次让你同时让一个十万个数的区间同时加上一个数或者求出它们的和,那你的效率不就太慢了吗?线段树就能高效率地解决这样的问题。

线 段 树

我们先构造一棵二叉树

假设节点1表示第 \(1\) 个数到第 \(8\) 个数的和,节点 \(2\) 表示第 \(1\) 个数到第 \(4\) 个数的和,节点 \(3\) 表示第 \(5\) 个数到第 \(8\) 个数的和,节点 \(4\) 表示第 \(1\) 个数到第 \(2\) 个数的和...以此类推,那每一个节点都能表示一段区间的和,是这样的:


其中[ ]里面的数表示这个节点所表示的区间,sum表示这段区间的和。

我们就把这棵二叉树称为线段树

那如果我们需要找到一段区间的和,不就只需要找到哪些节点组合起来能够表示出这个区间,然后再把它加起来不就行了吗?修改也是一样的,只需要找到能够表示这个区间的所有叶子节点,然后加上/减去需要修改的数,然后再把它们的祖先节点都修改完,不就行了吗?这时候,有人可能就会问了:“为什么这个区间能够保证被一些节点表示出来呢?”因为每个点所表示的区间长度都是 \(2^n\),而任何一个正整数都能被若干个 \(2^n\) 相加表示出来,所以这个区间就一定能被表示出来。

代码实现

我们使用数组来存储这棵线段树。每个节点 \(cur\) 的左孩子是 \(2cur\) ,右孩子是 \(2cur+1\)\(lt\) 表示这个节点表示的区间的左端点,\(rt\) 表示右端点,设 \(mid=\frac{(lt+rt)}{2}\),那么 \(cur\) 的左孩子所表示的范围就是 \([lt,mid]\),右孩子所表示的范围就是 \([mid+1,rt]\)

1.建立线段树(\(\mathrm{build()}\) 函数)

我们只需一层一层地往下递归,直到叶子节点时把 \(tree[cur]\) 赋值为这个数,
否则 \(tree[cur]\) 就是它的左右两个子节点 \(tree[cur*2]\)\(tree[cur*2+1]\) 的和。

void pushup(int cur)
{
	tree[cur] = tree[cur * 2] + tree[cur * 2 + 1];//tree[cur]是它的左右两个子节点tree[cur * 2],tree[cur * 2 + 1]的和
	return;
}
void build(int cur, int lt, int rt)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点
{
	if(lt == rt)//如果是叶子节点(叶子节点只能表示一个点,也就是左右端点相等)
	{
		tree[cur] = a[lt];//那么叶子节点就是这个数
		return;
	}
	int mid = (lt + rt) >> 1;
	build(cur * 2, lt, mid);//递归左子树
	build(cur * 2 + 1, mid + 1, rt);//递归右子树
	pushup(cur);//给此节点赋值
	return;
}

2.设计询问函数(\(\mathrm{query()}\) 函数)

我们只需一层一层地往下递归,如果此节点覆盖的范围都在需要查询的范围内,那么就返回这个节点的值,否则就继续递归。

int query(int cur, int lt, int rt, int qx, int qy)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点,qx表示需要查询的区间的左端点,qy表示右端点
{
	if(qx > rt || qy < lt)//如果此节点覆盖的范围不在需要修改的范围内,停止递归,返回0
		return 0;
	if(qx <= lt && qy >= rt)//如果此节点覆盖的范围都在需要查询的范围内,返回这个节点的值
		return tree[cur];
	int mid = (lt + rt) >> 1;
	return query(cur * 2, lt, mid, qx, qy) + query(cur * 2 + 1, mid + 1, rt, qx, qy);//继续往下递归,返回左右两个子节点的和
}

3.设计修改函数(\(\mathrm{update()}\) 函数)

我们也只需一层一层地往下递归,如果是叶子节点,就加上需要修改的值,否则就就将此节点修改为它的左右两个子节点的和。

void update(int cur, int lt, int rt, int qx, int qy, int val)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点,qx表示需要修改的区间的左端点,qy表示右端点,val表示需要修改的数
{
	if(qx > rt || qy < lt)//如果此节点覆盖的范围不在需要修改的范围内,停止递归
		return;
	if(lt == rt)//如果是叶子节点
	{
		tree[cur] += val;//加上要修改的值
		return;//停止递归
	}
	int mid = (lt + rt) >> 1;
	update(cur * 2, lt, mid, qx, qy, val);//递归左子树
	update(cur * 2 + 1, mid + 1, rt, qx, qy, val);//递归右子树
	pushup(cur);//因为它的左右两个子节点修改了,所以此节点需要修改为左右两个子节点的和
	return;
}

现在,我们这份代码的雏形就完成了!只需要加上主函数的一些输入和调用就行了。不过为什么我不把主函数也写完呢?因为它太慢了!!!

在修改函数中,每一次都需要递归到叶子节点,如果需要修改每一个数的话,那就需要走完线段树中的每一个点,也就是 \(2^n-1\) 个点,那么修改函数的时间复杂度就变成了 \(O(2^n)\),比暴力修改还慢,于是这份代码就需要优化。

懒 标 记(Lazy-Tag)

我们想一想,修改是干什么的呢?为什么要修改呢?因为要查询啊修改是为查询服务的啊!你不查询,修改它有屁用啊?

我们举个例子:老师布置了一堆作业,但是不查,谁会做啊?

于是,只要它不查询,我们就把这个修改给存起来,等它查询的时候,我们再一个一个往下传,修改,不就行了吗?我们把这个操作称为懒标记

我们使用 \(tag[cur]\) 表示 \(cur\) 这个点的懒标记,只要 \(cur\) 这个点所覆盖的范围都需要修改,我们就把它用 \(tag[cur]\) 存起来,先不修改,等到需要查询的时候,我们再修改。

查询时,我们只需要把这个点的懒标记传给它的两个子节点,让它们加上标记,下一次查询时,照样也被修改了,我们把这个操作叫作标记下传

1.设计标记下传函数(\(\mathrm{pushdown()}\) 函数)

\(tag[cur]\) 打给左右子节点,且清空 \(tag[cur]\),如果不清空的话只要重复下传就会重复加上标记,标记就错误了

void addtag(int cur, int lt, int rt, int val)//加上标记,cur表示节点编号,lt表示这个节点覆盖的区间的左端点,rt表示右端点
{
	tag[cur] += val;//此节点的标记加上它的父节点的标记
	tree[cur] += (rt - lt + 1) * val;//此节点的值加上它的父节点的值乘上它覆盖的节点数
	return;
}
void pushdown(int cur, int lt, int rt)//标记下传,cur表示节点编号,lt表示这个节点覆盖的区间的左端点,rt表示右端点
{
	if(tag[cur] == 0)//如果标记本来就是0,那么return
		return;
	int mid = (lt + rt) >> 1;
	addtag(cur * 2, lt, mid, tag[cur]);//给左孩子加上标记
	addtag(cur * 2 + 1, mid + 1, rt, tag[cur]);//给右孩子加上标记
	tag[cur] = 0;//清空标记
	return;
}

2.修改 \(\mathrm{update()}\) 函数

我们只需要把修改叶子节点的那一段删掉,只要 \(cur\) 这个点所覆盖的范围都需要修改,我们就把它用 \(tag[cur]\) 存起来,不进行修改(\(\mathrm{addtag()}\) 函数),否则就标记下传

void update(int cur, int lt, int rt, int qx, int qy, int val)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点,qx表示需要修改的区间的左端点,qy表示右端点,val表示需要修改的数
{
	if(qx > rt || qy < lt)//如果此节点覆盖的范围不在需要修改的范围内,停止递归
		return;
	if(qx <= lt && qy >= rt)//如果此节点覆盖的范围都在需要修改的范围内,增加标记
	{
		addtag(cur, lt, rt, val);
		return;
	}
	pushdown(cur, lt, rt);//标记下传
	int mid = (lt + rt) >> 1;
	update(cur * 2, lt, mid, qx, qy, val);//递归左子树
	update(cur * 2 + 1, mid + 1, rt, qx, qy, val);//递归右子树
	pushup(cur);//因为它的左右两个子节点修改了,所以此节点需要修改为左右两个子节点的和
	return;
}

3.修改 \(\mathrm{query()}\) 函数

我们只需要在查询左右子节点前标记下传,使得其被修改就行了

int query(int cur, int lt, int rt, int qx, int qy)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点,qx表示需要查询的区间的左端点,qy表示右端点
{
	if(qx > rt || qy < lt)//如果此节点覆盖的范围不在需要修改的范围内,停止递归,返回0
		return 0;
	if(qx <= lt && qy >= rt)//如果此节点覆盖的范围都在需要查询的范围内,返回这个节点的值
		return tree[cur];
	pushdown(cur, lt, rt);//标记下传
	int mid = (lt + rt) >> 1;
	return query(cur * 2, lt, mid, qx, qy) + query(cur * 2 + 1, mid + 1, rt, qx, qy);//继续往下递归,返回左右两个子节点的和
}

至此,基本的线段树我们就已经学完了,下面附上模板题的完整代码(P3372)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N], tree[4 * N], tag[4 * N];
void pushup(int cur)
{
	tree[cur] = tree[cur * 2] + tree[cur * 2 + 1];//tree[cur]是它的左右两个子节点tree[cur * 2],tree[cur * 2 + 1]的和
	return;
}
void build(int cur, int lt, int rt)//cur表示节点编号,lt表示这个节点覆盖的区间的左端点,rt表示右端点
{
	if(lt == rt)//如果是叶子节点(叶子节点只能表示一个点,也就是左右端点相等)
	{
		tree[cur] = a[lt];//那么叶子节点就是这个数
		return;
	}
	int mid = (lt + rt) >> 1;
	build(cur * 2, lt, mid);//递归左子树
	build(cur * 2 + 1, mid + 1, rt);//递归右子树
	pushup(cur);//给此节点赋值
	return;
}
void addtag(int cur, int lt, int rt, int val)//加上标记,cur表示节点编号,lt表示这个节点覆盖的区间的左端点,rt表示右端点
{
	tag[cur] += val;//此节点的标记加上它的父节点的标记
	tree[cur] += (rt - lt + 1) * val;//此节点的值加上它的父节点的值乘上它覆盖的节点数
	return;
}
void pushdown(int cur, int lt, int rt)//标记下传,cur表示节点编号,lt表示这个节点覆盖的区间的左端点,rt表示右端点
{
	if(tag[cur] == 0)//如果标记本来就是0,那么return
		return;
	int mid = (lt + rt) >> 1;
	addtag(cur * 2, lt, mid, tag[cur]);//给左孩子加上标记
	addtag(cur * 2 + 1, mid + 1, rt, tag[cur]);//给右孩子加上标记
	tag[cur] = 0;//清空标记
	return;
}
int query(int cur, int lt, int rt, int qx, int qy)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点,qx表示需要查询的区间的左端点,qy表示右端点
{
	if(qx > rt || qy < lt)//如果此节点覆盖的范围不在需要修改的范围内,停止递归,返回0
		return 0;
	if(qx <= lt && qy >= rt)//如果此节点覆盖的范围都在需要查询的范围内,返回这个节点的值
		return tree[cur];
	pushdown(cur, lt, rt);//标记下传
	int mid = (lt + rt) >> 1;
	return query(cur * 2, lt, mid, qx, qy) + query(cur * 2 + 1, mid + 1, rt, qx, qy);//继续往下递归,返回左右两个子节点的和
}
void update(int cur, int lt, int rt, int qx, int qy, int val)//cur表示节点编号,lt表示这个节点表示的区间的左端点,rt表示右端点,qx表示需要修改的区间的左端点,qy表示右端点,val表示需要修改的数
{
	if(qx > rt || qy < lt)//如果此节点覆盖的范围不在需要修改的范围内,停止递归
		return;
	if(qx <= lt && qy >= rt)//如果此节点覆盖的范围都在需要修改的范围内,增加标记
	{
		addtag(cur, lt, rt, val);
		return;
	}
	pushdown(cur, lt, rt);//标记下传
	int mid = (lt + rt) >> 1;
	update(cur * 2, lt, mid, qx, qy, val);//递归左子树
	update(cur * 2 + 1, mid + 1, rt, qx, qy, val);//递归右子树
	pushup(cur);//因为它的左右两个子节点修改了,所以此节点需要修改为左右两个子节点的和
	return;
}
signed main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		cin >> a[i];
	build(1, 1, n);
	for(int i = 1; i <= m; i++)
	{
		int opt, x, y, val;
		cin >> pt >> x >> y;
		if(opt == 1)
		{
			cin >> val;
			update(1, 1, n, x, y, val);
		}
		else
			cout << query(1, 1, n, x, y) << "\n";
	}
	return 0;
}

下转线段树习题可能码风和习惯会有些不同,因为不是我写的,是我同学写的,请见谅!

有关线段树的学习的更多相关文章

  1. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  2. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  3. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

  4. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  5. ruby - 我如何学习 ruby​​ 的正则表达式? - 2

    如何学习ruby​​的正则表达式?(对于假人) 最佳答案 http://www.rubular.com/在Ruby中使用正则表达式时是一个很棒的工具,因为它可以立即将结果可视化。 关于ruby-我如何学习ruby​​的正则表达式?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/1881231/

  6. ruby-on-rails - 在 Rails 中需要整个目录树的好方法是什么? - 2

    我正在使用Rails3.2.2并希望递归加载某个目录中的所有代码。例如:[Railsroot]/lib/my_lib/my_lib.rb[Railsroot]/lib/my_lib/subdir/support_file_00.rb[Railsroot]/lib/my_lib/subdir/support_file_01.rb...基于谷歌搜索,我试过:config.autoload_paths+=["#{Rails.root.to_s}/lib/my_lib/**"]config.autoload_paths+=["#{Rails.root.to_s}/lib/my_lib/**/"

  7. 深度学习12. CNN经典网络 VGG16 - 2

    深度学习12.CNN经典网络VGG16一、简介1.VGG来源2.VGG分类3.不同模型的参数数量4.3x3卷积核的好处5.关于学习率调度6.批归一化二、VGG16层分析1.层划分2.参数展开过程图解3.参数传递示例4.VGG16各层参数数量三、代码分析1.VGG16模型定义2.训练3.测试一、简介1.VGG来源VGG(VisualGeometryGroup)是一个视觉几何组在2014年提出的深度卷积神经网络架构。VGG在2014年ImageNet图像分类竞赛亚军,定位竞赛冠军;VGG网络采用连续的小卷积核(3x3)和池化层构建深度神经网络,网络深度可以达到16层或19层,其中VGG16和VGG

  8. 机器学习——时间序列ARIMA模型(四):自相关函数ACF和偏自相关函数PACF用于判断ARIMA模型中p、q参数取值 - 2

    文章目录1、自相关函数ACF2、偏自相关函数PACF3、ARIMA(p,d,q)的阶数判断4、代码实现1、引入所需依赖2、数据读取与处理3、一阶差分与绘图4、ACF5、PACF1、自相关函数ACF自相关函数反映了同一序列在不同时序的取值之间的相关性。公式:ACF(k)=ρk=Cov(yt,yt−k)Var(yt)ACF(k)=\rho_{k}=\frac{Cov(y_{t},y_{t-k})}{Var(y_{t})}ACF(k)=ρk​=Var(yt​)Cov(yt​,yt−k​)​其中分子用于求协方差矩阵,分母用于计算样本方差。求出的ACF值为[-1,1]。但对于一个平稳的AR模型,求出其滞

  9. Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板 - 2

    写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c

  10. ruby-on-rails - 这个 C 和 PHP 程序员如何学习 Ruby 和 Rails? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我来自C、php和bash背景,很容易学习,因为它们都有相同的C结构,我可以将其与我已经知道的联系起来。然后2年前我学了Python并且学得很好,Python对我来说比Ruby更容易学。然后从去年开始,我一直在尝试学习Ruby,然后是Rails,我承认,直到现在我还是学不会,讽刺的是那些打着简单易学的烙印,但是对于我这样一个老练的程序员来说,我只是无法将它

随机推荐