算法提高篇(1)线段树(上)

1、引入

有这么一个问题:一共有n个数,q次操作,每次操作为询问区间[l, r]的和。

在算法基础篇中,我们知道了这种问题可以用前缀和来解决。

现在我们在原有问题的基础上进行如下进阶:问题1: 有n个数,q次操作,操作有两种:a、查询区间[l,r]的和;b、将第i个数修改成x。问题2: 有n个数,q次操作,操作有两种:a、查询区间[l,r]的和;b、将区间[l,r]的数全部修改成x。**问题3:**有n个数,q次操作,每次操作询问区间[l,r]的最大值或者最小值(这个其实是RMQ问题)。

这些问题虽然都是子问题的变形,但是你会发现前缀和一个都解决不了,用暴力解法又肯定会超时。因此,我们需要一个新的数据结构来帮助我们解决以上几类问题。

2、线段树的构建

线段树是基于分治思想的二叉树,树中每一个节点都会维护一段区间的信息。其中叶子节点存储元素本身,非叶子节点维护区间内元素的信息。

我们以数组a = [5,1,3,0,2,2,7,4,5,8]为例,如果要查询区间和,我们会先从下标中间位置将数组一分为二,直到区间长度为1。如下图所示:

而区间[1,1]的元素和就是数组中下标为1的元素,区间[2,2]的和就是数组下标为2的元素。此时我们就可以更新结果了:

根据上面的思路,就可以写出代码了:

cpp 复制代码
#define lc  p << 1
#define rc  p << 1 | 1

typedef long long LL;
const int N = 1e5 + 10;

int n, m;
LL a[N];

// 线段树
struct node
{
	LL l, r, sum;	
}tr[N << 2];

// 整合左右孩子的信息
void pushup(int p)
{
	tr[p].sum = tr[lc].sum + tr[rc].sum;
}

// 构建线段树
void build(int p, int l, int r)
{
	tr[p] = {l, r, 0};  // 初始化
	if(l == r)
	{
		tr[p].sum = a[l];
		return;
	}
	
	int mid = (l + r) >> 1;  // 一分为二
	build(lc, l, mid);  // 构建左子树
	build(rc, mid + 1, r);  // 构建右子树
	
	pushup(p);
}

时间复杂度:O(n)

3、区间查询

对于一个待查询的区间,用拆分+拼凑的思想,在线段树的节点中收集结果。具体流程:

1、从根节点出发,向下递归;

2、如果当前节点维护的区间信息包含在待查询的区间内,直接返回节点维护的信息(递归出口)

3、如果左区间有重叠,去左子树上找结果;

4、如果右区间有重叠,去右子树上找结果。

具体流程如下图所示:

代码实现:

cpp 复制代码
// 区间查询
LL query(int p, int x, int y)
{
	LL l = tr[p].l, r = tr[p].r;  // 当前节点维护的信息
	if(x <= l && r <= y)
		return tr[p].sum;
		
	LL sum = 0;
	LL mid = (l + r) >> 1;
	if(x <= mid)
		sum += query(lc, x, y);
	if(y >= mid + 1)
		sum += query(rc, x, y);
		
	return sum;
}

时间复杂度:O(logN)

4、单点修改

将下标为6的位置上的数加上3(如果是对单个位置上的数执行减、乘、除的操作都是这个思路)

1、递归找到叶子结点,并且维护修改后的信息;

2、然后一路向上回溯,修改所有路径上的节点信息,使得维护的信息为修改之后的信息。

流程图如下:

代码实现:

cpp 复制代码
// 整合左右孩子的信息
void pushup(int p)
{
	tr[p].sum = tr[lc].sum + tr[rc].sum; 
}

// 单点修改
void modify(int p, int x, LL k)
{
	int l = tr[p].l, r = tr[p].r;
	if(x == l && x == r)  // 叶子结点
	{
		tr[p].sum += k;
		return;
	}
	
	int mid = (l + r) >> 1;
	if(x <= mid)
		modify(lc, x, k);
	else
		modify(rc, x, k);
		
	pushup(p);
}

5、区间修改

例如,对区间[4, 9]上每个元素增加2。如果按照单点修改的方式把该区间内所有的点全部修改,时间复杂度是O(n)。

但是,如果某个节点维护的区间[l, r]被要修改的区间[x, y]完全覆盖时,它的左右子树可以没必要修改,等到下次遇到时再去处理。

借助这一思想,我们会在每个节点中维护一个额外的懒标记

1、如果当前区间[l, r]被待查询区间[x, y]完全覆盖时,停止递归,根据区间长度维护出增加元素之后的和,打上一个懒标记,不去处理左右孩子。

2.等到下次修改或者查询操作,遇到该节点时,再把懒标记下放给左右孩子。

以数组a = [5,1,3,0,2,2,7,4,5,8]为例,如果对区间[4,9]上每个元素加2,维护信息如下:

再执行查询操作:查询区间[5,7]上所有元素的和,维护信息如下:

代码实现:

cpp 复制代码
struct node
{
	int l, r;
	LL sum, add;	
}tr[N * 4];

void pushup(int p)
{
	tr[p].sum = tr[lc].sum + tr[rc].sum;
}

void build(int p, int l, int r)
{
	tr[p] = {l, r, 0, 0};
	if(l == r)
	{
		tr[p].sum = a[l];
		return;
	}
	
	int mid = (l + r) / 2;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	pushup(p);
}

// 接收到修改任务,修改完毕之后,把信息懒下来
void lazy(int p, LL add)
{
	int l = tr[p].l, r = tr[p].r;
	tr[p].sum += (r - l + 1) * add;
	tr[p].add += add;
}

void pushdown(int p)
{
	lazy(lc, tr[p].add);  // 懒标记分配给左孩子
	lazy(rc, tr[p].add);  // 懒标记分配给右孩子
	tr[p].add = 0;
}

void modify(int p, int x, int y, LL k)
{
	int l = tr[p].l, r = tr[p].r;
	if(x <= l && r <= y)
	{
        // 修改之后打上标记
		tr[p].sum += (r - l + 1) * k;
		tr[p].add += k;
		return;
	}
	
	pushdown(p);  // 懒标记下放
	
	int mid = (l + r) / 2;
	if(x <= mid)
		modify(lc, x, y, k);
	if(y >= mid + 1)
		modify(rc, x, y, k);
		
	pushup(p);  // 更新父节点
}

LL query(int p, int x, int y)
{
	int l = tr[p].l, r = tr[p].r;
	LL sum = 0;
	if(x <= l && r <= y)
	{
		return tr[p].sum;
	}
	
	pushdown(p);
	
	int mid = (l + r) / 2;
	if(x <= mid)
		sum += query(lc, x, y);
	if(y >= mid + 1)
		sum += query(rc, x, y);
	
	return sum;
}

由于加上了懒标记,所有的操作与区间查询过程一致,所以整体的时间复杂度为O(logN)。

相关推荐
py有趣1 小时前
力扣热门100题之单词拆分
算法·leetcode
杨凯凡2 小时前
【012】图与最短路径:了解即可
java·数据结构
j_xxx404_2 小时前
C++算法:哈希表(简介|两数之和|判断是否互为字符重排)
数据结构·c++·算法·leetcode·蓝桥杯·力扣·散列表
Aaron15883 小时前
RFSOC+VU13P+RK3588的核心优势与应用场景分析
嵌入式硬件·算法·matlab·fpga开发·信息与通信·信号处理·基带工程
优家数科3 小时前
精准预测:基于多维用水量的滤芯寿命预警算法
算法
脱氧核糖核酸__3 小时前
LeetCode热题100——189.轮转数组(题解+答案+要点)
数据结构·c++·算法·leetcode
贾斯汀玛尔斯3 小时前
每天学一个算法-快速排序(Quick Sort)
数据结构·算法
炽烈小老头3 小时前
【每天学习一点算法 2026/04/16】逆波兰表达式求值
学习·算法
优家数科3 小时前
水质监测不准?解密云端 TDS 数据建模纠偏算法
算法