线段树详解

线段树详解 (原理,实现与应用) 线段树详解 By 岩之痕 一:综述 线段树是一种可以快速进行区间修改和区间查询的数据结构。点修改,区间修改和区间查询的复杂度都是O(log2(n)) 并且,线段树可以维护很多种类的信息。说到线段树就不得不提一下树状数组,树状数组维护的信息必须满足区间减法,因为树状数组只是维护1到X的信息,通过相减来得到区间信息,代码简单而且速度快,占用空间又少于线段树,所以一般情况下可以用树状数组的地方就 阅读详情

致歉:嗯... 由于数学竞赛原因,本人很久没有更新了,望洛谷的粉丝朋友们多多谅解。这一期就不做漫画了,我们出个博文讲讲线段树。感谢粉丝们对我长期以往的支持与鼓励,谢谢!

OK,我们言归正传,来讲一讲线段树吧!

首先,看一道例题,洛谷的 P3372,【线段树1】。主要看一下精华部分。

如题,已知一个数列,你需要维护下面两种操作:

  1.  将某区间每一个数加上 k 。
  2.  求出某区间每一个数的和 。

题目要求我们维护这两种操作。我们首先可以用暴力的思路解决。

暴力解法:

      解决操作1,很简单,开一个数组,对于给出的区间,for 一下,直接加上 k 就完事了。              解决操作2,更简单,要么暴力累加,要么来个前缀和,稍微优化一下。

所以,用暴力解决很简单,下面看一下代码。

//P3372 【模板】线段树1  暴力解法
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1e5 + 5;
int n, m;
int number[MAXN];

int main () {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0); //以上两行是关闭同步,让速度更快,大家要是不知道,不用没关系

	cin >> n >> m;
	for (int i = 1;i <= n; ++i) 
		cin >> number[i];
	for (int i = 1;i <= m; ++i) {
		int opt; cin >> opt; //这里的 opt 指的是操作前的提示符 (1, 2)
		if (opt == 1) {
			int x, y, k; cin >> x >> y >> k;
			for (int j = x;j <= y; ++j) {
				number[j] += k; //实现1操作
			}
		}
		else if (opt == 2) {
			int x, y; cin >> x >> y;
			int sum = 0;
			for (int j = x;j <= y; ++j) {
				sum += number[j]; //实现2操作
			}
			cout << sum << '\n'; //使用关闭同步,就不能用 endl 了
		}
	}
	return 0;
}

洛谷测评机:您老真是勇气可嘉 (回去挨打吧你)

TLE 的原因就不用说了。 O(n^2) 的时间复杂度,配上 1e5 的数据,100% 超时。 

重点来了,线段树的基本想法:

        我们刚刚已经公开处刑了这位用暴力求解的孩子,没错,爆时间了。那有没有一种方法,能够在短时间内维护这两种操作?有,线段树!

        线段树本身不难理解。正所谓 “线段树”,它是一颗树。而树上的节点呢,都是一段区间。就以这道题为例,构建一个“线段树”。我们假设原数组num有八个格,数值分别为[5, 9, 4, 1, 8, 8, 8, 8] 。 观察上面的树。图中绿色的中括号,是它的节点所代表的区间。图中节点上的数字,就是它所代表的的区间的和。这么说我怕有些人会看不懂,那么举个例子。看到图中左上角的节点,它节点上写的数字是19,这个19 表达的就是从数组 num[1]  到  num[4] 的和 (5 + 9 + 4 + 1 = 19)。

线段树的应用:

我们现在清楚了线段树的每个部分都在表达什么,进一步,我们去探究线段树如何去应用在这道题目上。

首先看第 2 种操作,注意是第二种 :比如说我们想知道 [1, 5] 这个区间的和,那我们无需用 for 循环一遍一遍地做,直接把代表区间 [1, 4] 和 [5] 的节点加起来,结果就是 19 + 8 = 27 。这样,就可以把时间复杂度直接减到 O (log~n),岂不美哉!

接着看第 1 种操作:看到上图。比如,我们想把 [1, 4] 这个区间的数值都加上 1,我们先把图中的 [1], [2], [3], [4] 都加上1,紧接着,[1], [2], [3], [4] 再分别把自己的父节点加上1:[1, 2] 加上2 ;    [3 ,4] 加上2 ;然后,[1, 4] 加上4,[1, 8] 加上4。这就完整地完成了一次 1 操作。大家如果不太理解,可以联系线段树的基本想法部分手绘理解一下。

重头戏——线段树的实现:


我们首先看到操作 2 的实现。

         注意是操作 2!我们要实现的是一个区间求和操作。但是,我们在线段树里,就可以理解为一个“查询”操作。为啥呢?还是上文那个例子,我们要求区间 [1, 5] 的和。就要先知道 [1, 5] 是由 [1, 4] 和 [5] 组合而成的,才能把两个区间加起来得到答案。那,怎么知道 [1, 5] 是谁和谁组合而成的呢?请看下图。

 图片中的字有些小,请各位放大使用。上图所示,便是操作 2 的流程。我们注意到,图中有两种行走方式:向左下走和向右下走。那么,我们为了方便说明,就定义往左下走,就是去往 left_son ,往右下走,就是去往 right_son 。我们宏定义一下:

#define mid ( (l + r) / 2 )
#define left_son u * 2, l, mid
#define right_son u * 2 + 1, mid + 1, r

有些同学可能会抱怨:唉,又开始看不懂了。没关系,这段代码现在看不懂没事,待会放到代码里,你就知道起什么作用了。 


我们再看到操作 1 的实现。

        操作 1 放到后面讲可能好一点,并不是它难。我们实现的是修改操作。根据上文“线段树的应用”,我们得知,操作 1 其实就像条链子一样,一直不断往上加。如果不太理解,我们看图。

        同上文,假设我们要把区间 [1, 4] 加上 1。

字比较小,咱还是放大食用一下。


好,我们现在就到了代码实现的部分了。

我们先实现两个函数,叫做 push_down 和 push_up 。

push_down:这个函数可以理解为是向左儿子和右儿子去走,并且在走完的地方打上标记。

push_up:这个函数可以理解为向上回溯,那么有些代码和思路中是不需要他的。但是我们这个思路就需要。

我们先把它们实现了再说!

/*                            push_down 的模板代码                        */
void push_down (int u, int l, int r) { //u是本节点的下标。l和r分别是查询的左区间和右区间
    tree[u] += vis[u] * (r - l + 1);
    vis[u * 2] += vis[u]; //向左儿子走
    vis[u * 2 + 1] += vis[u]; //向右儿子走
    vis[u] = 0; //清空标记
}
/*                            push_up 的模板代码                        */
void push_up (int u, int l, int r) {
    push_down (u, l, r);
    push_down (left_son); //去往左儿子
    push_down (right_son); //去往右儿子
    tree[u] = tree[u * 2] + tree[u * 2 + 1]; //一步步往上更新加和
}

我们看到代码,应该不难理解。tree 是构建树的数组这些都好理解。但是我相信大家一定对这个叫做 vis 的数组抱有疑问。下面,我们就来讲讲 vis 数组。

       vis 数组的作用:我们的 vis 数组是一个懒标记  (Lazy~ Tag) 数组,也有的地方翻译成“延迟标记”(听起来高端,实际上啥也不是)。好了,回顾一下整个过程:区间更新时,我们递归查找目标区间的子区间,过程中不断维护当前节点的区间和,每找到一个目标区间的子区间(有可能只有1个点)被完全覆盖到时停止往下递归。最妙的地方在于我们每次不必要把懒标记下传到底,只需传到管理目标区间的子区间的节点就行,下次再有更新的时候,我们再随着更新递归传递标记就好了。这样,时间复杂度会大大提升有了上文这些准备,主要的代码来了!

对于操作 2,区间查询。 实现一个 query查找函数。我们需要几个参数。分别是 u,~ l,~ r,~ L,~ R ,分别表示本节点的下标、当前节点的左区间、当前节点的右区间、查询的左区间、查询的右区间。

query Code

long long query (int u, int l, int r, int L, int R) { //上文所提到的参数
	push_down (u, l, r); //向下查询
	if (L <= l and r <= R) { //如果本区间完全属于要查询的区间
		return tree[u]; //直接返回答案
	}
	long long answer = 0; //定义输出答案
	if (L <= mid) { //判断左区间是否合法
		answer += query (left_son, L, R); //去右儿子
	}
	if (R > mid) { //判断右区间是否合法
		answer += query (right_son, L, R); //去左儿子 
	}
	return answer; //返回最终的答案
}

以上的代码应该很好理解,我们主要来看下面的 update 函数。

update 函数主要实现的就是加一个数字 k。我们需要的参数有 u,~l,~r,~L,~,R~,~value ,其中的 value 就是输入的 k 。其他的上文说过不过多解释,详见代码注释。

void update (int u, int l, int r, int L, int R, int value) { //上文所述的一些系数
    push_down (u, l, r); //往下走
    if (L <= l and r <= R) { //跟上个函数一样,先判断是否全部属于
        vis[u] += value; //更新懒标记
        return ;
    }
    if (L <= mid) {
        update (left_son, L, R, value);
    }
    if (R > mid) {
        update (right_son, L, R, value);
    } //同上
    push_up (u, l, r); //可以理解为回溯
    return ;
}

我们实现了这两个函数之后,就把他们融入我们的大程序吧!

我觉得我的代码还算好懂吧...

#include <iostream>
#include <cstdio>
#define MAXN 100010
#define mid ((l + r) / 2)
#define left_son u * 2, l, mid //宏定义我们的左儿子和右儿子
#define right_son u * 2 + 1, mid + 1, r
using namespace std;
int n, m;
long long tree[MAXN * 8], vis[MAXN * 8];


void push_down (int u, int l, int r) { //u是本节点的下标。l和r分别是查询的左区间和右区间
    tree[u] += vis[u] * (r - l + 1);
    vis[u * 2] += vis[u]; //向左儿子走
    vis[u * 2 + 1] += vis[u]; //向右儿子走
    vis[u] = 0; //清空标记
}

void push_up (int u, int l, int r) {
    push_down (u, l, r);
    push_down (left_son); //去往左儿子
    push_down (right_son); //去往右儿子
    tree[u] = tree[u * 2] + tree[u * 2 + 1]; //一步步往上更新加和
}

long long query (int u, int l, int r, int L, int R) { //上文所提到的参数
	push_down (u, l, r); //向下查询
	if (L <= l and r <= R) { //如果本区间完全属于要查询的区间
		return tree[u]; //直接返回答案
	}
	long long answer = 0; //定义输出答案
	if (L <= mid) { //判断左区间是否合法
		answer += query (left_son, L, R); //去右儿子
	}
	if (R > mid) { //判断右区间是否合法
		answer += query (right_son, L, R); //去左儿子 
	}
	return answer; //返回最终的答案
}

void update (int u, int l, int r, int L, int R, int vlaue) { //上文所述的一些系数
    push_down (u, l, r); //往下走
    if (L <= l and r <= R) { //跟上个函数一样,先判断是否全部属于
        vis[u] += value; //更新懒标记
        return ;
    }
    if (L <= mid) {
        update (left_son, L, R, value);
    }
    if (R > mid) {
        update (right_son, L, R, value);
    } //同上
    push_up (u, l, r); //可以理解为回溯
    return ;
}

int main () {
	cin >> n >> m;
	for (int i = 1;i <= n; ++i) {
		long long number;
		cin >> number;
		update (1, 1, n, i, i, number); //最初输入数据的时候,也相当于在更新。我们就用更新函数建一下树。
	}
	for (int i = 1;i <= m; ++i) {
		int a; cin >> a;
		if (a == 1) {
			long long x, y, k;
			cin >> x >> y >> k;
			update (1, 1, n ,x, y, k); //对于操作1,调用update。
		}
		else {
			int x, y;
			cin >> x >> y;
			cout << query (1, 1, n, x, y) << endl; //对于操作2,调用query,并输出。
		}
	}
	return 0;
}

贴一下题目【模板】线段树1https://www.luogu.com.cn/problem/P3372嗯...这次讲解差不多就到这了。goodbye~

大家对我文章的意见可以直接在下方留言,或者在洛谷的帖子上说。

我还发起一个投票:下期是继续讲线段树的深入还是画漫画?大家可以投个票哈~

感谢观看~

线段树--概念+模板+入门题 引子 子问题–1 给出n个数,m次问询,每次问询给出一个范围l、r,求区间l–r的总和 很容易想到用前缀和可以解决 O(n) 子问题–2 给出n个数,m次修改,每次给出一个范围l、r和一个值k,将区间l—r的数全部+k,m次修改后,再问询1次求区间l–r的总和 也可以想到这用差分就能解决 O(n) 最后将这两个问题结合起来,给出n个数,m次操作,操作有两种一个是将区间l–r的值+k,一个是询问区间l–r的总和。 这个问题与之前不同的地方是,之前想要求一个范围只需处理一次数据算前缀和,然后数据不变,可以一 阅读详情

相关推荐

线段树详解(包含加法线段树、乘法线段树及区间根号线段树,简单易懂)

线段树及简单应用。

qi_programmer的博客 2582

【学习笔记】详解线段树(浅显易懂,匠心之作,图文并茂)

超详解线段树(浅显易懂,几乎涵盖所有线段树类型讲解,匠心之作,图文并茂)

weq2011的博客 3万+

线段树合并与线段树分裂

线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。线段树分裂实质上是线段树合并的逆过程。线段树分裂只适用于有序的序列,无序的序列是没有意义的,常用在动态开点的权值线段树。线段树分裂与线段树合并密不可分,一般来说没有只分裂不合并的题。的救灾粮,问所有操作结束后每个节点类型最多的救灾粮是那种。一棵树,点有点权,问每个节点的子树内有多少节点点权比它大。从这棵树的根节点开始递归分裂,当节点不存在时,递归结束。显然,对于两颗满的线段树,合并操作的复杂度是。

conti123的博客 934

算法数据结构——线段树(Segment Tree)精讲及python实现

线段树(Segment Tree):一种基于分治思想的二叉树,用于在区间上进行信息统计。它的每一个节点都对应一个区间leftright通常是整数。每一个叶子节点表示了一个单位区间(长度为1),叶子节点对应区间上。每一个非叶子节点的左子节点表示的区间都为,右子节点表示的的区间都为。线段树是一棵平衡二叉树,树上的每个节点维护一个区间。根节点维护的是整个区间,每个节点维护的是父亲节点的区间二等分之后的其中一个子区间。当有n。

白话机器学习 5170

阿里云Ubuntu部署Steam饥荒联机版服务器:从零到多人联机的完整指南

本文详细介绍了在阿里云Ubuntu系统上部署Steam饥荒联机版服务器的完整流程,包括环境准备、SteamCMD安装、服务端配置、Mod管理及后台运行技巧。通过2核4GB的ECS实例配置和端口设置,实现5人流畅联机体验,适合游戏爱好者快速搭建专属服务器。

weixin_29229261的博客 1023

颠覆性创新的测量框架与跨领域实证分析

颠覆性创新作为科技发展的重要驱动力,其核心在于实现知识体系的范式转换。从技术原理看,这类创新通常具备知识替代、网络重构和跨领域渗透三大特征,通过引用网络分析和文本挖掘等方法可量化评估其影响力。在工程实践中,CD指数、DI指数等测量工具帮助研究者识别具有突破潜力的成果,而深度学习等新兴技术进一步提升了评估精度。当前AI、半导体等领域的热门技术发展表明,模块化架构和产业协同能有效促进颠覆性创新。面对全球范围内创新颠覆性下降的趋势,优化科研评价体系、构建跨学科协作网络成为关键应对策略。

weixin_29735807的博客 321

数据结构 线段树--权值线段树 详解

一、权值线段树 简介 1.线段树 线段树是一种用于维护区间信息的高效数据结构,可以在 O(log⁡N)O(\log N)O(logN) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。 线段树维护的信息必须满足可加性,即能以可以接受的速度合并信息和修改信息,包括在使用Lazy标记时,标记也要满足可加性 具体有关线段树的的讲解请参考博客:线段树 详解与模板

HeartFireY的博客 1万+

【入门篇】第五章 第一次与 AI Agent 交互

本文介绍了OpenClaw中最基本的用户与AI Agent交互方式,剖析了Agent和Gateway两个核心组件的作用,通过简单示例展示了从用户请求到结果返回的完整执行流程,说明了最小交互过程对验证系统可用性的重要意义,为后续学习复杂任务执行机制奠定基础。

lanyv的博客 512

详解线段树

但是在做题的时候还是会对模板进行修改,修改的点主要在区间val的意义上,我们这里模板里面val代表的是区间和,但后面题目里面可能是 区间最大值 、区间最小值…,但是万变不离其宗,线段树的结构是不会变的。

潜心钻研技术 1353

在Qt中使用EtherCAT-SOEM的初始化中,状态机卡在PreOp->Save-Op过程中,出现State=0x12, Error=0x001e错误的解决办法

1.在调试EtherCAT软件过程中,一直出现配置从站进不了Save_op状态,通过下函数检查从站状态: //使用的从站编号为1 只有1个从站 ec_slave[1].state = EC_STATE_SAFE_OP; ec_writestate(1); //设置到安全模式 ec_statecheck(1, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE * 4); printf("Slave %d State=0x%2.2x StatusCode=0x%4

zeal_for_rov的博客 6826

直流并联电压源的下垂控制与模拟光伏板特性的方法与仿真

并联直流电源下垂控制方法,下垂特性可以让并联直流电源输出功率/电流自动均衡,直流电源输出特性模拟光伏板的输出特性,具有自动限制输出最大功率的功能

1197

二维线段树详解

如题

INF_512的博客 2098

Angular 相关的知识点和项目汇总

Angular 知识点和项目汇总

weixin_45428910的博客 577

线段树 详解

线段树

wuyixiang___的博客 1941

基于DFT的滤波系列:加窗滤波的 Matlab 实现

在实际应用中,你可以尝试不同类型的窗函数(如汉宁窗、汉明窗等)以及不同的滤波参数来获得所需的滤波效果。此外,还可以进一步了解 DFT 和窗函数的原理,以及其他数字信号处理技术的应用。加窗滤波是一种常用的数字信号处理技术,它通过在频域上应用窗函数来改善信号的频谱特性。我们通过定义输入信号、应用窗函数、进行 DFT 和逆 DFT,实现了滤波过程,并通过绘图进行了可视化。在频域上进行加窗滤波的关键是将窗函数应用于 DFT 的结果。首先,我们需要定义一个输入信号,这里我们使用一个简单的正弦波作为示例。

m0_47037246的博客 462

YOLOv12中的A2C2f模块详解,涉及区域注意力模块(A2)和 R-ELAN 结构详解

区域注意力模块(Area Attention, A2)的设计出发点是为了解决传统注意力机制在计算复杂度和内存访问效率上的问题。传统的自注意力机制(Self-Attention)具有二次方的计算复杂度(O(n²)),尤其是在处理高分辨率图像时,计算成本非常高。此外,注意力机制的内存访问模式不如卷积神经网络高效,导致在实际应用中难以满足实时性要求。区域注意力模块通过将特征图划分为多个区域,并在每个区域内进行注意力计算,从而显著降低了计算复杂度。

Limiiiing的博客 5513

详解动态开点线段树

本文介绍了动态开点线段树及其在解决CF915E问题中的应用。由于数据范围过大(n≤1e9),传统线段树无法直接使用。动态开点线段树的核心思想是只创建操作中实际需要的节点,而非预先构建完整结构。文章通过图解展示了动态开点的过程,并提供了关键代码实现,包括修改操作和pushdown处理。最后给出了完整代码,并指出动态开点技术在可持久化线段树中的重要性。这种方法有效解决了大区间范围下的线段树实现问题。

Benny_250的博客 936

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

线段树及线段树模板

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为Olog⁡NO(\log N)OlogN。而未优化的空间复杂度为2N2N2N,实际应用时一般还要开空间为4N4N4N的数组以免越界,因此有时需要离散化让空间压缩。线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

AllenAC的博客 1234

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

线段树入门详解

目录 线段树入门详解 本文适用人群 线段树大致思路 本人代码习惯 单点修改 单点查询 区间查询 区间加减 Last 线段树入门详解 本文适用人群 不算特别入门啦...

dieloupi0136的博客 321

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

上一篇: 洛谷题目全解(P1000 超级玛丽游戏)
下一篇: 数据结构——vector 清晰讲解
VINTAGE_vector
博客等级 码龄5年 213粉丝 8原创
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值