【笔记】线段树

news2024/11/16 17:50:05

Start

【笔记】线段树 目录

      • 简介
      • 定义
      • 建树
      • 更新
      • 例题1: 单点修改,区间查询
        • 单点修改
        • 区间查询
        • 本题完整代码
      • 例题2: 区间修改,单点查询
        • 思路
        • 本题完整代码
      • 例题3: 区间修改,区间查询
        • 懒标记
          • 基本思想
          • 应用
        • 区间修改
        • 本题完整代码

简介

线段树是一棵二叉树。如果删去最后一层节点,它是一棵完全二叉树。

线段树是一种常用于处理区间问题的数据结构,分为 递归式线段树 和 非递归式线段树(又称zkw线段树)。

其时间复杂度一般为 O ( n log ⁡ n ) O(n \log n) O(nlogn),不过常数较大。如果追求最优解,建议使用树状数组。

线段树的常用操作共有 3 3 3 种,分别是:

  • 单点修改,区间查询。
  • 区间修改,单点查询。
  • 区间修改,区间查询。

下面的例题就以这三种情况和求和操作为例讲解。

定义

放个图:

线段树

我们考虑完全二叉树的性质:
若当前节点的编号为 x x x,则左儿子的编号为 2 x 2x 2x,右儿子的编号为 2 x + 1 2x+1 2x+1

由于线段树是完全二叉树,所以线段树的节点编号也遵循这个原则。

但是我们需要频繁用到 × 2 , + 1 \times2,+1 ×2,+1 等操作,怎么能让它效率高一点呢?

答案就是“位运算”。 x × 2 x \times 2 x×2 可以用 x << 1 替代, x × 2 + 1 x \times 2 + 1 x×2+1 可以用 x << 1 | 1 替代。

现在看我们需要维护什么。

对于每个节点,我们分别维护区间左端点 l l l,区间右端点 r r r,以及维护的区间值 x x x 和懒标记(如果你有的话)。

节点用结构体数组维护,下标遵循完全二叉树的性质。

上代码:

const int N = 100010;

struct Segment_Tree_Node
{
	int l, r;	// 区间左右端点
	int sum;	// 这里以区间和为例
	int lazy;	// 懒标记,只在有区间修改时用
}tr[N << 2];

有的同学就会问了:为什么要开 4 4 4 倍空间呢?

观察我们上面提到的:

线段树是一棵二叉树。如果删去最后一层节点,它是一棵完全二叉树。

假设我们维护的数列长度为 n n n

因为叶子节点的 l l l r r r 相等,所以叶子节点个数是 n n n

上面的所有节点共有大约 n − 1 n-1 n1 个,因为根据等比数列求和公式,

2 0 + 2 1 + ⋅ ⋅ ⋅ + 2 n − 1 = 2 n − 1 2^0 + 2^1 + ··· + 2^{n-1}=2^n-1 20+21+⋅⋅⋅+2n1=2n1

但是,叶子节点的下面可能还有一层节点,而这层节点的个数为 2 × n 2 \times n 2×n

所以保守起见,我们需要开 n + n + 2 × n = 4 × n n + n + 2 \times n = 4 \times n n+n+2×n=4×n 的空间。

建树

所谓建树,就是遍历所有节点,并初始化左右端点和维护的数值。

我们考虑递归遍历。

这个不难,直接放代码:

void build(int u, int l, int r) // 当前区间编号,区间左右端点
{
	if (l == r) tr[u] = {l, r, a[l], 0}; // 初始化叶子节点的左右端点和数值
	else
	{
		tr[u] = {l, r}; // 初始化非叶子节点的左右端点
		LL mid = l + r >> 1; // 以本区间中点向下取整作为左儿子的右端点
		build(u << 1, l, mid); // 建左儿子的树
		build(u << 1 | 1, mid + 1, r); // 建右儿子的树
		pushup(u); // 用子节点的数值更新父节点数值
	}
}

更新

我们发现建树用到了 pushup 操作,这个是用子节点的数值更新父节点数值。

很简单,放代码:

void pushup(LL u)
{
	tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
	// 用左右儿子的sum更新该节点的sum
}

例题1: 单点修改,区间查询

原题链接:P3374 【模板】树状数组 1

不要说我用树状数组的题练习线段树,我找不到线段树的模板题才用的这个

单点修改

如果递归到这个点所在的叶子节点就直接修改,否则判断它在这个区间的左儿子还是右儿子并递归。

代码:

void modify(int u, int x, int d) // 当前节点编号、修改的节点编号、加的数值
{
    if (tr[u].l == x && tr[u].r == x) // 如果当前的节点和要修改的节点相同就直接修改
        tr[u].sum += d;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1; // 否则取当前区间的中点
        if (x <= mid) modify(u << 1, x, d); // 如果在左儿子就递归到左儿子
        else modify(u << 1 | 1, x, d); // 如果在右儿子就递归到右儿子 
        pushup(u); // 因为修改了,所以更新一下当前节点的值
    }
}

区间查询

结合代码理解:

int query(int u, int l, int r) // 当前节点编号、查询区间的左右端点
{
    if (tr[u].l >= l && tr[u].r <= r) // 如果当前节点完全被查询就返回这个区间的值
        return tr[u].sum;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1; // 否则取当前区间的中点
        int res = 0;
        if (l <= mid) res += query(u << 1, l, r); // 涉及到左儿子
        if (r > mid) res += query(u << 1 | 1, l, r); // 涉及到右儿子
        return res; // 返回
    }
}

本题完整代码

#include <iostream>

using namespace std;

const int N = 500010;

struct Segment_Tree_Node
{
	int l, r;
	int sum;
}tr[N * 4];

int n, m;
int a[N];

void pushup(int u)
{
	tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void build(int u, int l, int r)
{
	if (l == r) tr[u] = {l, r, a[l]};
	else
	{
		tr[u] = {l, r};
		int mid = l + r >> 1;
		build(u << 1, l, mid);
		build(u << 1 | 1, mid + 1, r);
		pushup(u);
	}
}

void modify(int u, int x, int d)
{
    if (tr[u].l == x && tr[u].r == x)
        tr[u].sum += d;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, d);
        else modify(u << 1 | 1, x, d);
        pushup(u);
    }
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r)
        return tr[u].sum;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        int res = 0;
        if (l <= mid) res += query(u << 1, l, r);
        if (r > mid) res += query(u << 1 | 1, l, r);
        return res;
    }
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ )
		scanf("%d", &a[i]);

	build(1, 1, n);

	int op, l, r, d;
	while (m -- )
	{
		scanf("%d%d%d", &op, &l, &r);
		if (op == 1) modify(1, l, r);
		else printf("%d\n", query(1, l, r));
	}

	return 0;
}

例题2: 区间修改,单点查询

原题链接:P3368 【模板】树状数组 2

思路

相信大家都学过差分,它可以 O ( 1 ) O(1) O(1) 的时间复杂度进行区间修改。

所以我们直接把刚才的数组进行差分,再建树。

注意:由于差分涉及在第 n + 1 n+1 n+1 个节点中进行操作,所以建树时要到 n + 1 n+1 n+1

本题完整代码

#include <iostream>

using namespace std;

const int N = 500010;

struct Segment_Tree_Node
{
	int l, r;
	int sum;
}tr[N * 4];

int n, m;
int a[N], b[N];

void pushup(int u)
{
	tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void build(int u, int l, int r)
{
	if (l == r) tr[u] = {l, r, b[l]};
	else
	{
		tr[u] = {l, r};
		int mid = l + r >> 1;
		build(u << 1, l, mid);
		build(u << 1 | 1, mid + 1, r);
		pushup(u);
	}
}

void modify(int u, int x, int d)
{
    if (tr[u].l == x && tr[u].r == x)
        tr[u].sum += d;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, d);
        else modify(u << 1 | 1, x, d);
        pushup(u);
    }
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r)
        return tr[u].sum;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        int res = 0;
        if (l <= mid) res += query(u << 1, l, r);
        if (r > mid) res += query(u << 1 | 1, l, r);
        return res;
    }
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ )
		scanf("%d", &a[i]), b[i] = a[i] - a[i - 1]; // 差分

	build(1, 1, n + 1); // 建树到 n + 1

	int op, l, r, d;
	while (m -- )
	{
		scanf("%d%d", &op, &l);
		if (op == 1)
		{
		    scanf("%d%d", &r, &d);
		    modify(1, l, d), modify(1, r + 1, -d); // 区间修改时修改区间的端点
		}
		else printf("%d\n", query(1, 1, l)); // 查询当前节点的前缀和
	}

	return 0;
}

例题3: 区间修改,区间查询

原题链接:P3372 【模板】线段树 1

懒标记

基本思想

当进行区间修改时,只修改当前区间,而它的子节点的修改先欠着,等用到了子节点的时候再往下传。

应用

将懒标记下传的操作:pushdown 函数

如果这个节点有懒标记,就把懒标记加到子节点的懒标记中,并把当前节点的懒标记清空。

代码:

void pushdown(LL u)
{
	Segment_Tree_Node &U = tr[u], &L = tr[u << 1], &R = tr[u << 1 | 1];
	if (tr[u].lazy) // 如果当前节点有懒标记
	{
		L.lazy += U.lazy, L.sum += (L.r - L.l + 1) * U.lazy; // 左儿子懒标记和区间和
		R.lazy += U.lazy, R.sum += (R.r - R.l + 1) * U.lazy; // 右儿子懒标记和区间和
		U.lazy = 0; // 清空当前节点懒标记
		// 显然,区间和在加的时候应该加上懒标记和区间长度的乘积
	}
}

区间修改

这里和 pushdown 差不多,修改懒标记和区间和。

代码:

void modify(LL u, LL l, LL r, LL d)
{
	if (tr[u].l >= l && tr[u].r <= r) // 如果这个区间被完全包含
	{
		tr[u].lazy += d; // 修改懒标记 
		tr[u].sum += (tr[u].r - tr[u].l + 1) * d; // 区间和 + d * 区间长度
	}
	else
	{
		pushdown(u);
		LL mid = tr[u].l + tr[u].r >> 1;
		if (l <= mid) modify(u << 1, l, r, d);
		if (r > mid) modify(u << 1 | 1, l, r, d);
		pushup(u);
	}
}

本题完整代码

#include <iostream>

using namespace std;

typedef long long LL;

const LL N = 100010;

struct S_Tree
{
	LL l, r;
	LL sum, lazy;
}tr[N * 4];

LL n, m;
LL a[N];

void pushup(LL u)
{
	tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

void pushdown(LL u)
{
	S_Tree &U = tr[u], &L = tr[u << 1], &R = tr[u << 1 | 1];
	if (tr[u].lazy)
	{
		L.lazy += U.lazy, L.sum += (L.r - L.l + 1) * U.lazy;
		R.lazy += U.lazy, R.sum += (R.r - R.l + 1) * U.lazy;
		U.lazy = 0;
	}
}

void build(LL u, LL l, LL r)
{
	if (l == r) tr[u] = {l, r, a[l], 0};
	else
	{
		tr[u] = {l, r};
		LL mid = l + r >> 1;
		build(u << 1, l, mid);
		build(u << 1 | 1, mid + 1, r);
		pushup(u);
	}
}

void modify(LL u, LL l, LL r, LL d)
{
	if (tr[u].l >= l && tr[u].r <= r)
	{
		tr[u].lazy += d;
		tr[u].sum += (tr[u].r - tr[u].l + 1) * d;
	}
	else
	{
		pushdown(u);
		LL mid = tr[u].l + tr[u].r >> 1;
		if (l <= mid) modify(u << 1, l, r, d);
		if (r > mid) modify(u << 1 | 1, l, r, d);
		pushup(u);
	}
}

LL query(LL u, LL l, LL r)
{
	if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
	else
	{
		pushdown(u);
		LL mid = tr[u].l + tr[u].r >> 1;
		LL res = 0;
		if (l <= mid) res += query(u << 1, l, r);
		if (r > mid) res += query(u << 1 | 1, l, r);
		return res;
	}
}

int main()
{
	scanf("%lld%lld", &n, &m);
	for (LL i = 1; i <= n; i ++ )
		scanf("%lld", &a[i]);

	build(1, 1, n);

	LL op, l, r, d;
	while (m -- )
	{
		scanf("%lld%lld%lld", &op, &l, &r);
		if (op == 1)
		{
			scanf("%lld", &d);
			modify(1, l, r, d);
		}
		else
		{
			LL t = query(1, l, r);
			printf("%lld\n", t);
		}
	}

	return 0;
}

End

最后,如果觉得对您有帮助的话,点个赞再走吧!

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

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

相关文章

带团队后的日常思考

一、日常问题 1&#xff09;补充产品文档 最近版本迭代&#xff0c;其中涉及一块举报流程的优化&#xff0c;其实会涉及管理后台发送站内信。 刚开始&#xff0c;大家都没怎么在意这个需求&#xff0c;但是在执行时才发现有许多细节没有考虑到。 管理后台的举报页面是在 2018 …

【C++技能树】一文看懂模板匹配

Halo&#xff0c;这里是Ppeua。平时主要更新C&#xff0c;数据结构算法&#xff0c;Linux与ROS…感兴趣就关注我bua&#xff01; 文章目录 0.泛型编程1.模板2 函数模板:2.1函数模板的特化 3. 类模板3.1 非类型模板参数3.2 类的模板刻画3.2.1 全特化3.2.2 偏特化 0.泛型编程 假…

在Microsoft SQL Server 2008中,语法生成错误“并行数据仓库(PDW)功能未启用“

案例&#xff1a; 原表有两列&#xff0c;分别为月份、月份销售额&#xff0c;而需要一条 SQL 语句实现统计出每个月份以及当前月以前月份销售额和 sql 测试数据准备&#xff1a; DECLARE Temp Table ( monthNo INT, --- 月份 MoneyData Float --- 金额 ) insert INTO TEM…

利用AIGC,零成本靠谱副业,每天半小时,挣个生活费不成问题,亲测,省时省力攻略

文章目录 背景为什么是写作如何操作申请各大创作平台的账号吸引人关注&#xff0c;增加粉丝利用AIGC来创作 看看其他好友分享的收入情况 背景 最近&#xff0c;尝试利用AIGC在头条号、微信公众号上开始写文章&#xff0c;并且通过这个机会赚取了一笔的外快。这个经历让我深刻认…

最强整理,HttpRunner接口自动化框架-hook机制实战,一篇上高速...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 对于使用 Python结…

谷歌广告(Google ads)如何投放?新手必看的超全教程

Google是公认的全球最大的搜索引擎&#xff0c;同时&#xff0c;Google还通过旗下的 YouTube、Gmail、Google Play、Android等产品&#xff0c;汇集了海量的海外用户。对于跨境出海商家来说&#xff0c;谷歌广告是提高销售额、提高产品流量、拓展全球市场的重要推广渠道。 那么…

从0开始搭建一个Monorepo模版,基于Turborepo+pnpm+changesets+dumi

Monorepo 前言开始一、使用turborepo初始化项目二、调整目录结构及文件1. 调整package.json文件2. 调整app目录3. 调整eslint包4. 调整ui包5. 调整eslint配置6. 调整.npmrc7. 使用commitizen规范代码提交8. 使用commitlinthusky进行 commit提交信息校验9. 使用husky进行commit前…

vue3 - 使用reactive定义响应式数据进行列表赋值时,视图没有更新的解决方案

文章目录 1&#xff0c;问题2&#xff0c;原因3&#xff0c;解决方案一、再封装一层数据&#xff0c;即定义属性名&#xff0c;在后期赋值的时候&#xff0c;对此属性进行直接赋值三、使用数组的splice来直接更改原数组三、使用 ref 来定义数据 1&#xff0c;问题 在Vue 3.0 中…

pythonocc进阶学习:投影projection

1.点 到 线, 直线,曲线,等上的投影 staticmethod # 点到Lin的投影 def Project_Pnt_To_Lin(p: gp_Pnt, lin: gp_Lin):Edge BRepBuilderAPI_MakeEdge(lin).Edge()curve BRep_Tool.Curve(Edge)proPnt GeomAPI_ProjectPointOnCurve(p, curve[0])NearestPoint proPnt.Nearest…

JVM虚拟机篇

JVM组成 面试题1&#xff1a;什么是程序计数器&#xff1f; 面试题2&#xff1a;你能给我详细的介绍Java堆吗? 面试题3&#xff1a;什么是虚拟机栈&#xff1f; 面试题4&#xff1a;垃圾回收是否涉及栈内存&#xff1f; 垃圾回收主要指就是堆内存&#xff0c;当栈帧弹栈以后…

全开源国外购物商城手机APP页面装修功能开发

搭建一个全开源国外购物商城手机APP页面装修功能开发需要以下步骤&#xff1a; 1. 确定需求&#xff1a;首先&#xff0c;需要明确页面装修功能的具体需求。例如&#xff0c;是否需要支持自定义布局、颜色、字体等&#xff1b;是否需要支持多种商品展示方式&#xff08;列表、…

misc学习(1)Bugku-社工-进阶收集

新手一枚&#xff0c;参考文献如下&#xff1a; Bugku&#xff1a;社工-进阶收集_bugku 社工 进阶收集_FW_ENJOEY的博客-CSDN博客 照片如图所示&#xff1a; 线索线&#xff1a; 1.百度识图 发现是大雁塔 2.主角家距离大雁塔一共有七站地铁&#xff0c;其中要进行中转。 同时…

噼里啪啦 图像分类篇

1. LeNet初始化权重的问题 由于我使用的是torch 1.10.0的版本&#xff0c;其Conv2d的init是使用asqrt(5) 我将这里的torch默认初始化改为a1之后&#xff0c;acc的对比如下&#xff1a;可以看出&#xff1a;更改初始化之后&#xff0c;5个epoch&#xff0c;acc提高了3个点。改为…

数组相关练习

数组练习 将数组转化成字符串数组拷贝求数组元素的平均值查找数组中指定元素(顺序查找)二分查找冒泡排序数组逆序 将数组转化成字符串 import java.util.Arrays;public class Text1 {public static void main(String[] args) {int[] arr {5, 6, 4, 2};System.out.println(Arr…

机器学习中训练数据的重要性

人工智能技术发展至今&#xff0c;训练数据的重要性已经是我们老生常谈的问题。在重声训练数据为什么重要之前&#xff0c;我们先重新回顾下AI技术大爆炸的三大初始概念&#xff1a;机器学习是什么&#xff1f;人工智能是什么&#xff1f;训练数据又是什么&#xff1f; 机器学…

《Kali渗透基础》14. 无线渗透(四)

kali渗透 1&#xff1a;相关工具1.1&#xff1a;Aircrack-ng1.1.1&#xff1a;airmon-ng1.1.2&#xff1a;airodump-ng1.1.3&#xff1a;aireplay-ng1.1.4&#xff1a;airolib-ng1.1.5&#xff1a;bessid-ng 1.2&#xff1a;JTR1.3&#xff1a;Evil Twin Attacker1.4&#xff1…

Vue 中使用 WebWorker

目录 安装 loader 应用场景 打包时错误处理 安装 loader npm install worker-loader -D 如果直接把worker.js放到public目录下&#xff0c;则不需要安装loader vue.config.js const { defineConfig } require(vue/cli-service)module.exports defineConfig({transpileDe…

JS逆向系列之猿人学爬虫第14题-备而后动-勿使有变

文章目录 题目地址参数分析参考jspython 调用往期逆向文章推荐题目地址 https://match.yuanrenxue.cn/match/14题目难度标的是困难,主要难在js混淆部分。 参数分析 初始抓包有无限debugger反调试,可以直接hook 函数构造器过掉无限debugger Function.prototype.__construc…

C++异常体系

文章目录 一.C对运行时错误的处理方式函数调用链中的异常机制 二.异常的使用规范三.C异常体系C标准库中的异常体系 四.关于C异常的注意事项 一.C对运行时错误的处理方式 传统的C语言处理运行时错误采用的是assert或者错误码的方式,这种异常处理机制对错误信息的定位和描述能力…

ArcGIS Pro技术应用(暨基础入门、制图、空间分析、影像分析、三维建模、空间统计分析与建模、python融合)

GIS是利用电子计算机及其外部设备&#xff0c;采集、存储、分析和描述整个或部分地球表面与空间信息系统。简单地讲&#xff0c;它是在一定的地域内&#xff0c;将地理空间信息和 一些与该地域地理信息相关的属性信息结合起来&#xff0c;达到对地理和属性信息的综合管理。GIS的…