线段树讲解

news2025/1/17 0:21:28

0、引入

假设给定一个长度为 1001 的数组,即下标 0 到 1000。

现在需要完成 3 个功能:

add(1, 200, 6); //给下标 1 到 200 的每个数都加 6;
update(7, 375, 4); //下标 7 到 375 的数全部修改为 4
query(3, 999); //下标 3 到 999 所有数的累加和

如果暴力解,则上述的每个功能的时间复杂度都为 O ( N ) O(N) O(N);而如果是使用线段树,则可以做到时间复杂度 O ( l o g N ) O(logN) O(logN)

1、简介

为了简单起见,涉及到线段树的数组下标从 1 开始。

线段树是一种支持范围整体修改范围整体查询的数据结构。

首先,希望将一个数组分成树状
请添加图片描述
怎么做到呢?申请一个数组,只要满足下标 i i i 的父节点是 i / 2 i/2 i/2 ,左孩子为 i ∗ 2 i * 2 i2,右孩子为 i ∗ 2 + 1 i * 2 + 1 i2+1 就能转换为一个树状。

请添加图片描述
那么 申请的数组到底要准备多长呢? 如果原数组长度为 N N N,则准备 4 N 4N 4N 长度是够用的,包括一些用不上的位置。

这个结论怎么来的呢?

首先,当数组长度为 2 x 2^x 2x 次方时最省空间,因为没有废的空间。例长度为 4 时,申请 7 长度的的数组(0位置不用,下标1到7)即可, 就算包含弃而不用的 0 位置,也只需要 8 长度的空间。

当数组长度为 2 x + 1 2^x+1 2x+1 时,最费空间,但是也不会超过 4 N 4N 4N 长度。

2、懒更新:add

例如要给 3 ~ 874 下标范围的每个值都 +5,懒更新就是当树状数组中的某个范围包含在 3 ~ 874 时,就不再往下下发任务,而是就地处理。即只要树状中包含的数组范围没有完全包含在3 ~ 874中,就下发任务。
请添加图片描述
可以认为当一个任务到来时,卡着左边界和右边界各下放一次,中间该懒的就懒了(不往下发),时间复杂度 O ( l o g n ) O(logn) O(logn) 级别,非常快。


具体来说,就是一开始准备一个累加和数组(即 4 N 4N 4N长度)和一个懒更新数组,初始时:
请添加图片描述
如果来了一个任务 add(1,4,3),即1~4下标范围的每个数都+3,对应到树状中,树根的范围已经完全包含在这个任务要求的范围中了,于是不再下发任务,而是就地处理。于是,懒更新数组更新为:
请添加图片描述
如果接着又来了一个任务 add(1,2,4)。首先,先看是否有懒信息,当前的情况是有,于是将该信息下发一层(下发到1-2 和 3-4),然后清空本层的懒信息:
在这里插入图片描述
然后执行任务 add(1,2,4),这个任务只需要分给 1-2,于是1-2 这个格子就得到了每个数+4的任务,先检查之前1-2 是否有懒信息,发现有,于是将当前的懒信息往下下发一层(1-1 和 2-2),将自己清空,发现任务1-2 包含了当前的1-2,于是拦住这个任务,1-2 的懒信息更新为4:
在这里插入图片描述
在这里插入图片描述
也就是如果当前层有懒信息,需要先将任务往下发一层然后再执行新任务

3、懒更新:update

change 数组和 lazy 数组、sum 数组一样,都是原数组长度的 4 倍,只是用于记录原数组的不同信息。

为什么需要一个布尔类型的数组 update 呢?

例如原数组下标为1 ~ 500:

  • sum[1] = 100 万 表示1 ~ 500 的累加和为 100万,而 sum[1] = 0 表示累加和为0,没有歧义;
  • add[1] = 7 表示 1 ~ 500 每个数加7,而 add[1] = 0 无论是每个数都加0 还是 表示没有懒信息 都是可以的;
  • change[1] = 7 表示1 ~ 500 的每个数都修改为7;但是 change[1] = 0 就有歧义,不知道是1 ~ 500 每个数都变成 0 还是 1 ~ 500 的每个数没有update信息,造成了歧义。

所以再增加一个布尔类型的 update 数组,如果 update[1] = 1, change[1] = 0 就表示 1 ~ 500 的每个数变成0;而update = 0, change[1] = 0 表示没有关于 update 的懒信息,原数组该什么样就什么样。

4、懒更新:query

当需要查询区间的累加和时,要先将区间的其他任务(更新、累加)进行下发,然后执行查询任务。

5、解决的问题

线段树是用于解决区间增加、区间更新和区间查询的问题。

6、线段树代码实现

public class SegmentTree {

	public static class SegmentTree {
		private int MAXN;
		private int[] arr; //缓存数组,原序列的信息从0开始,但在arr里是从1开始的,0位置不用
		private int[] sum; //模拟线段树维护区间和,原数组arr长度扩4倍
		private int[] lazy; //为累加和懒惰标记,原数组arr长度扩4倍
		private int[] change; //范围上所有的值被更新成的值
		private boolean[] update; //update信息的慵懒标记,是否有修改信息

		public SegmentTree(int[] origin) {
			MAXN = origin.length + 1;
			arr = new int[MAXN]; // arr[0]不用,从1开始使用
			for (int i = 1; i < MAXN; i++) {
				arr[i] = origin[i - 1];
			}
			sum = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围的累加和信息
			lazy = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務
			change = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围有没有更新操作的任务
			update = new boolean[MAXN << 2]; // 用来支持脑补概念中,某一个范围更新任务,更新成了什么
		}

		private void pushUp(int rt) { //树状中根的累加和 = 左孩子的累加和 + 右孩子的累加和
			sum[rt] = sum[rt << 1] + sum[rt << 1 | 1]; //sum[i] = sum[2*i] + sum[2*i+1]
		}

		// 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围
		// 分发策略是什么
		// rt表示父节点的下标,ln表示左子树元素结点个数,rn表示右子树结点个数
		private void pushDown(int rt, int ln, int rn) {
			//先检查“更新”信息,再检查“累加”信息
			//之所以这样,是因为在update()函数操作的时候会将lazy信息清空
			//如果既有update信息又有lazy信息,则说明最近一次更新到目前为止又攒了几个累加信息
			//所以在分发任务的时候要先检查“更新”,再检查“累加”,因为两者同时存在时,更新是早的,累加是后攒起来的
			
			if (update[rt]) { //如果父节点有“更新”信息,则下发一级
				//左右孩子都有更新信息
				update[rt << 1] = true;
				update[rt << 1 | 1] = true;
				//左右孩子都改成父节点的change信息
				change[rt << 1] = change[rt];
				change[rt << 1 | 1] = change[rt];
				//左右孩子的累加和(lazy)信息都清空,被update信息覆盖掉
				lazy[rt << 1] = 0;
				lazy[rt << 1 | 1] = 0;
				//左右孩子的累加和重新计算
				sum[rt << 1] = change[rt] * ln;
				sum[rt << 1 | 1] = change[rt] * rn;
				//父节点的“更新”信息清空
				update[rt] = false;
			}
			if (lazy[rt] != 0) { //有懒信息,向下分发一级:更新左右孩子的累加和信息和和懒信息
				lazy[rt << 1] += lazy[rt]; //左孩子的懒信息加上父节点的懒信息
				sum[rt << 1] += lazy[rt] * ln; //左孩子的累加和更新加上左边节点个数 * 懒信息
				lazy[rt << 1 | 1] += lazy[rt]; //右孩子的懒信息加上父节点的懒信息
				sum[rt << 1 | 1] += lazy[rt] * rn; //右孩子的累加和更新
				lazy[rt] = 0; //孩子承接了懒任务,所以父节点的懒信息清零
			}
		}
		
		// 该函数就是将原数组建立成一个树状结构,最大范围一定在 1 这个下标上
		// 在初始化阶段,先把sum数组,填好
		// 在arr[l~r]范围上,去build,1~N,
		// rt : 这个范围在sum中的下标
		public void build(int l, int r, int rt) { //l~r这个范围在sum数组中对应的下标为rt
			if (l == r) { //叶节点,只有一个数,将原数组中的l范围的值填到sum[rt]即可
				sum[rt] = arr[l];
				return;
			}
			int mid = (l + r) >> 1; //求出中点
			build(l, mid, rt << 1); //左孩子
			build(mid + 1, r, rt << 1 | 1); //右孩子
			pushUp(rt); //计算出左孩子和右孩子后,将左右孩子相加得到根节点的值
		}

		
		//任务:将L到R范围上的数都修改为C
		//目前到了rt这个下标的节点,它代表的是l到r区间
		public void update(int L, int R, int C, int l, int r, int rt) {
			//任务把此时的范围全部包括了
			if (L <= l && r <= R) {
				update[rt] = true; //设置“更新”操作的懒信息
				change[rt] = C; //“更新”操作的懒信息
				sum[rt] = C * (r - l + 1); //直接设置累加和
				lazy[rt] = 0; //因为已经进行了“更新”操作,所以之前累加和的懒信息全部清空
				return;
			}
			// 当前任务躲不掉,无法懒更新,要往下发
			int mid = (l + r) >> 1;
			pushDown(rt, mid - l + 1, r - mid); //检查是否有老任务要先下发
			if (L <= mid) {
				update(L, R, C, l, mid, rt << 1);
			}
			if (R > mid) {
				update(L, R, C, mid + 1, r, rt << 1 | 1);
			}
			pushUp(rt);
		}

		// L到R范围上的每个数都加C (即任务),是固定的
		// 当前来到的格子是rt,表示的范围是l到r
		public void add(int L, int R, int C, int l, int r, int rt) {
			// 任务如果把此时的范围全包了!
			//如任务为[3,874]范围的每个数都加5,而当前来到的是树状的3位置的[251,500]区间
			//因为[251,500] 包含在[3,874],则懒更新,即直接在这里处理任务,而不是下发任务
			if (L <= l && r <= R) {
				sum[rt] += C * (r - l + 1); //l到r范围共r-l+1个数,则累加和变大 C*(r-l+1)
				lazy[rt] += C; //懒更新信息
				return;
			}
			// 任务没有把你全包!
			// l  r  mid = (l+r)/2
			int mid = (l + r) >> 1;
			//先处理老任务
			pushDown(rt, mid - l + 1, r - mid); //如果有懒信息先下发一级再处理当前的任务
			//然后处理新任务
			if (L <= mid) { //任务需要发给左边
				add(L, R, C, l, mid, rt << 1);
			}
			if (R > mid) { //任务需要发给右边
				add(L, R, C, mid + 1, r, rt << 1 | 1);
			}
			pushUp(rt); //调整自己的累加和
		}

		// 任务:查询L到R范围上的累加和
		// 目前到了下标为rt的节点,代表的区间是l到r
		public long query(int L, int R, int l, int r, int rt) {
			if (L <= l && r <= R) {
				return sum[rt];
			}
			int mid = (l + r) >> 1;
			//将之前缓存的任务先下发在处理查询
			pushDown(rt, mid - l + 1, r - mid);
			long ans = 0;
			if (L <= mid) { //任务需要分给左边,左边的查询结果
				ans += query(L, R, l, mid, rt << 1);
			}
			if (R > mid) { //任务需要分给右边,右边的查询结果
				ans += query(L, R, mid + 1, r, rt << 1 | 1);
			}
			return ans;
		}

	}

	//纯暴力
	public static class Right {
		public int[] arr;

		public Right(int[] origin) {
			arr = new int[origin.length + 1];
			for (int i = 0; i < origin.length; i++) {
				arr[i + 1] = origin[i];
			}
		}

		public void update(int L, int R, int C) {
			for (int i = L; i <= R; i++) {
				arr[i] = C;
			}
		}

		public void add(int L, int R, int C) {
			for (int i = L; i <= R; i++) {
				arr[i] += C;
			}
		}

		public long query(int L, int R) {
			long ans = 0;
			for (int i = L; i <= R; i++) {
				ans += arr[i];
			}
			return ans;
		}

	}

	public static int[] genarateRandomArray(int len, int max) {
		int size = (int) (Math.random() * len) + 1;
		int[] origin = new int[size];
		for (int i = 0; i < size; i++) {
			origin[i] = (int) (Math.random() * max) - (int) (Math.random() * max);
		}
		return origin;
	}

	public static boolean test() {
		int len = 100;
		int max = 1000;
		int testTimes = 5000;
		int addOrUpdateTimes = 1000;
		int queryTimes = 500;
		for (int i = 0; i < testTimes; i++) {
			int[] origin = genarateRandomArray(len, max);
			SegmentTree seg = new SegmentTree(origin);
			int S = 1;
			int N = origin.length;
			int root = 1;
			seg.build(S, N, root);
			Right rig = new Right(origin);
			for (int j = 0; j < addOrUpdateTimes; j++) {
				int num1 = (int) (Math.random() * N) + 1;
				int num2 = (int) (Math.random() * N) + 1;
				int L = Math.min(num1, num2);
				int R = Math.max(num1, num2);
				int C = (int) (Math.random() * max) - (int) (Math.random() * max);
				if (Math.random() < 0.5) { //0.5的概率执行add
					seg.add(L, R, C, S, N, root);
					rig.add(L, R, C);
				} else { //0.5的概率执行update
					seg.update(L, R, C, S, N, root);
					rig.update(L, R, C);
				}
			}
			for (int k = 0; k < queryTimes; k++) {
				int num1 = (int) (Math.random() * N) + 1;
				int num2 = (int) (Math.random() * N) + 1;
				int L = Math.min(num1, num2);
				int R = Math.max(num1, num2);
				long ans1 = seg.query(L, R, S, N, root);
				long ans2 = rig.query(L, R);
				if (ans1 != ans2) {
					return false;
				}
			}
		}
		return true;
	}

	public static void main(String[] args) {
		int[] origin = { 2, 1, 1, 2, 3, 4, 5 };
		SegmentTree seg = new SegmentTree(origin);
		int S = 1; // 整个区间的开始位置,规定从1开始,不从0开始 -> 固定
		int N = origin.length; // 整个区间的结束位置,规定能到N,不是N-1 -> 固定
		int root = 1; // 整棵树的头节点位置,规定是1,不是0 -> 固定
		int L = 2; // 操作区间的开始位置 -> 可变
		int R = 5; // 操作区间的结束位置 -> 可变
		int C = 4; // 要加的数字或者要更新的数字 -> 可变
		// 区间生成,必须在[S,N]整个范围上build
		seg.build(S, N, root);
		// 区间修改,可以改变L、R和C的值,其他值不可改变
		seg.add(L, R, C, S, N, root);
		// 区间更新,可以改变L、R和C的值,其他值不可改变
		seg.update(L, R, C, S, N, root);
		// 区间查询,可以改变L和R的值,其他值不可改变
		long sum = seg.query(L, R, S, N, root);
		System.out.println(sum);

		System.out.println("对数器测试开始...");
		System.out.println("测试结果 : " + (test() ? "通过" : "未通过"));

	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/145069.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解如何利用PWM驱动舵机:ESP32驱动DS1115舵机

深入理解如何利用PWM驱动舵机&#xff1a;ESP32驱动DS1115舵机DS1115舵机技术规格举例说明之前做了一个项目&#xff0c;关于ESP32驱动DS1115舵机&#xff0c;但是在项目运行的过程中由于学艺不精&#xff0c;导致电机抽搐 &#x1f635;‍&#x1f4ab;&#xff0c;所以特意拜…

声纹识别可靠评测

分享嘉宾 | 李蓝天 文稿整理 | William 1 Introduction 声纹识别的发展&#xff0c;非常迅猛&#xff0c;在一些基准上取得了不错的效果&#xff0c;但如果将其部署到一个实际的应用系统里面&#xff0c; 从应用方的反馈来看&#xff0c;纹识别在很多场景里的鲁棒性并不理想。…

聚观早报 | 亚马逊将裁员17000人;苹果砍单MacBook等产品线架构

今日要闻&#xff1a;亚马逊将裁员17000人&#xff1b;苹果砍单MacBook等产品线&#xff1b;京东科技调整组织架构&#xff1b;小米x徕卡团队获技术大奖&#xff1b;必应搜索或将纳入ChatGPT亚马逊将裁员17000人 1 月 5 日消息&#xff0c;知情人士称&#xff0c;亚马逊新一轮裁…

正版授权|FastStone Capture 专业屏幕截图录屏工具软件 商业版,支持商业用途。

现在截图对每个人来说都是一个必不可少的功能。QQ软件截图、360游览器截图等都是相对简单快速的途径。但是如果你对截图有更多的要求&#xff0c;那么这里推荐一款截图软件&#xff0c;它就是FastStone Capture。这个对于商城老用户来说&#xff0c;几乎是接近人手一份。强大的…

【VUE3】保姆级基础讲解(六)Axios库

目录 Axios介绍与原生的差异 发送常见的请求和配置选项 1、发送request请求 baseURL &#xff1a; 2、发送get请求 3、发送post请求 axios.all Axios创建新的实例 请求和响应拦截 请求拦截 响应拦截 Axios介绍与原生的差异 Axios其实就是一个网络请求库 与原生的差异&…

勇夺中国市场豪华品牌第一名后,特斯拉S3XY全系售价调整

比你优秀的人比你更努力&#xff0c;用这句话形容特斯拉最贴切不过。 刚刚过去的2022年&#xff0c;特斯拉在海内外市场交出了亮眼答卷&#xff1a;全球共计交付产品超131万辆&#xff0c;同比增长40%&#xff1b;乘联会给出的数据显示&#xff0c;上海超级工厂全年交付71.1万辆…

不止IVAS,微软Azure也在布局这些军事模拟场景

一提起微软在军事领域的应用&#xff0c;我们第一印象可能是美军以220亿美元采购HoloLens 2 AR头显的项目&#xff0c;这个项目后期由于AR光学和设计方面受限&#xff0c;正式应用的日期一直再推迟。实际上&#xff0c;微软除了向美军提供HoloLens外&#xff0c;还提供了基于云…

Unity 3D GUI 简介||OnGUI Button 控件

游戏开发过程中&#xff0c;开发人员往往会通过制作大量的图形用户界面&#xff08; Graphical User Interface&#xff0c;GUI &#xff09;来增强游戏与玩家的交互性。 Unity 3D 中的图形系统分为 OnGUI、NGUI、UGUI等&#xff0c;这些类型的图形系统内容十分丰富&#xff0…

第05章 数组、排序和查找

数组 基本介绍 数组可以存放多个同一类型的数据&#xff0c;数组也是一种数据类型&#xff0c;是引用类型。 即&#xff1a;数组就是一组数据。 数组的使用 1、数组的定义 方法一&#xff1a; 数据类型[] 数组名 new 数据类型[大小] 说明&#xff1a;int[] a new int[5…

【C++ Primer】阅读笔记(5):vector|迭代器|数组

目录 简介参考结语简介 Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~ ଘ(੭ˊᵕˋ)੭ 昵称:海轰 标签:程序猿|C++选手|学生 简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金,有幸在竞赛中拿过一些国奖、省奖…已保研 学习经验:…

数图互通高校房产管理——CAD图形管理

数图互通房产管理系统在这方面做得比较全面&#xff1b; 支持通过建筑物的楼层CAD图查看房间属性和使用信息&#xff0c;实现图数结合、以图管房、图数互查、数图互通、图文一体化。 1.1支持客户端和AutoCAD无缝集成 支持客户端和AutoCAD无缝集成&#xff0c;实现在客户端/Aut…

Acwing---796.子矩阵的和

子矩阵的和1.题目2.基本思想3.代码实现1.题目 输入一个n行m列的整数矩阵&#xff0c;再输入q个询问&#xff0c;每个询问包含四个整数1&#xff0c;y1&#xff0c;2&#xff0c;y2&#xff0c;表示一个子矩阵的左上角坐标和右下角坐标。 对于每个询问输出子矩阵中所有数的和。…

质性分析软件nvivo的学习(三)

0、前言&#xff1a; 这部分内容是&#xff0c;质性分析软件nvivo的学习&#xff08;二&#xff09;的衔接内容&#xff0c;建议看完&#xff1a;质性分析软件nvivo的学习&#xff08;一&#xff09;&#xff08;二&#xff09;再看这部分内容。这里的笔记都是以nvivo12作为学…

高成长、高潜力、高社区影响,达坦科技入选 2022 中国新锐技术先锋企业

2023 年 1 月 4日&#xff0c;中国技术先锋年度评选 | 2022 中国新锐技术先锋企业榜单正式发布。作为中国领先的新一代开发者社区&#xff0c;SegmentFault 思否依托数百万开发者的用户数据分析&#xff0c;各科技企业在国内技术领域的行为及影响力指标&#xff0c;最终评选出 …

【学习】网络压缩:知识蒸馏、参数量化、动态计算,PPO

文章目录一、知识蒸馏Knowledge Distillation二、参数量化结构设计:深度方向可分卷积Depthwise Separable Convolution1、Depthwise Convolution三、动态计算Dynamic Computation四、From on-policy to off-policy&#xff08;PPO&#xff09;一、知识蒸馏Knowledge Distillati…

Python蓝桥杯训练:数组和字符串 Ⅳ

Python蓝桥杯训练&#xff1a;数组和字符串 Ⅳ 文章目录Python蓝桥杯训练&#xff1a;数组和字符串 Ⅳ一、买卖股票的最佳时机二、删除排序数组中的重复项三、找出字符串中第一个匹配项的下标四、将整数转换为两个无零整数的和一、买卖股票的最佳时机 给定一个数组 prices &…

k8s 实战1:WordPress搭建

文章目录第一步&#xff1a;部署MariaDB第二步&#xff1a;部署WordPress第三步&#xff1a;映射WordPress Pod 端口号&#xff0c;让它在集群外可见第四步&#xff1a;创建反向代理的 Nginx&#xff0c;让我们的网站对外提供服务WordPress架构图第一步&#xff1a;部署MariaDB…

如何使用LightningChart JS创建高性能可视化的HTML图表?

LightningChart JS是一款高性能的JavaScript图标库&#xff0c;专注于实时数据可视化&#xff0c;以“快如闪电”享誉全球&#xff0c;是Microsoft Visual Studio数据展示速度最快的2D和3D图表制图组件&#xff0c;可实时呈现超过10亿数据点的海量数据。 LightningChart .JS |…

Redis基础篇——Redis常见命令及数据类型详解

文章目录1. Redis常见命令2. Redis数据结构介绍3. 通用命令KEYSDELEXISTSEXPIRETTL4. Redis 命令类型4.1 String 类型String 类型常见命令key的层级格式4.2 Hash 类型Hash 类型常用命令4.3 List 类型List 类型的常见命令4.4 Set 类型Set 类型的常见命令4.5 SortSet 类型SortedS…

全局描述符表

文章目录段描述符全局描述符表GDT段选择子进入保护模式步骤在开始介绍全局描述符之前&#xff0c;先了解一下段描述符。 段描述符 内存段是一片内存区域&#xff0c;访问内存就要提供段基址&#xff08;段基址属性&#xff09;以及段界限属性&#xff08;约束段大小&#xff…