高级数据结构 - 线段树(1)

本文详细介绍了线段树这种高级数据结构的应用,包括线段树的定义、建立过程、点状匹配、区间匹配、点状修改及区间更新等内容,并提供了具体的实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线段树是一种高级数据结构,用于解决区间的动态求最值与求和问题。举个例子,线段树能够解决的问题是像这样的:
给出一个序列A,序列A一开始有一个统一的初值S,现要求编写一个程序,能够维护这个序列已达成以下的操作:1、改变一段数,将从Begin到End的所有值全部修改成同一个数Bi;
2、求一段数的和,从Begin到End。
数据范围:序列的长度<=10^5,操作的数量<=10^6
这一道题如果用直接的模拟,代码不会很长,但是效率也是够低的。假设序列的长度为N,操作的数量为M,则时间复杂度平摊为O(N*M)。显然做不到这一道题目。所以我们就要引入线段树。

【线段树的定义】
线段树一般是一颗完全二叉树,用于存储长度为2^N的序列(当然如果不是的话,浪费一点空间也差不多)。它的定义是这样的:
高级数据结构 - 线段树 - 区间匹配 - wenjianwei1 - 算法的设计
 
我们可以看见,线段树是将一个区间二分,作为自己的两个子节点。线段树的根节点是整个区间,两个子节点分别是区间的前半部分和后半部分……这样就可以很容易地建立整棵线段树了。所以,就这样不断二分,就直接初始化了一颗线段树。但是如果我们直接将整个序列记录在线段树中,那样的话开销太大,所以说我们将其离散化,只记一个开头和一个结尾,就用来代表这整个区间。
基本结构:

const int maxN = 1000007; const int oo = 0x0fffffff; struct Tnode { int l, r; //区间的左右端点 int max; //线段树节点的保留信息,用于查询,这里是[l,r)区间中的最大值 int lc, rc; //leftchild 和 rightchild,结点的左右孩子 }; /*内存池及相关操作*/ Tnode f[maxN]; //静态内存池 int fp = 0; //内存池的最后空闲下标 int root; //根节点在内存池中的下标 /*内存池的申请(相当于new),不过相当于综合了构造函数*/ int getPoint(int l, int r, int max) { fp++; f[fp].l = l; f[fp].r = r; f[fp].max = max; return fp; }

建立:

/*开辟一棵线段树*/ int create(int l, int r) { int now = getPoint(l, r, -oo); if(l < r) { //递归,如果当前的区间合法就将孩子节点继续create() f[now].lc = create(l, (l + r) / 2); //左边 f[now].rc = create((l + r) / 2 + 1, r); //右边 } return now; }

另外还有一个问题,如果一开始有什么给定值该怎么办呢?很简单,首先建立最下面的一层依次赋值,接着从下往上递推即可。

【线段树的点状匹配】
所以说,线段树其实就是一些二分的区间,所以说这里的点状匹配,本质上就是二分查找。所以不难写出下面的伪代码:

1.如果当前区间是线段树中的叶节点则直接返回当前节点

2.如果当前点在左区间则返回往左区间递归的结果

3.如果当前点在右区间则返回往右区间递归的结果

所以点状的匹配无疑是最简单的。
关于具体实现,其实直接用区间匹配(下面会介绍),就直接找一个长度为1的区间即可。

【线段树的区间匹配】
既然点状匹配写出来了,而且点状匹配的时间复杂度无疑也是O(log2n),那区间匹配则么办呢?既然点状匹配是左边有就往左边找,右边有就往右边找,那换成区间,就有可能出现三种情况:只和左边的相交,只和右边的相交,和两边的都相交。所以相应的处理就是和左边相交就往左边递归,往右相交就往右递归。比如说区间的求和,就是将左右的递归结果相加返回即可。

1.如果当前查找的区间是线段树中的叶节点则直接返回当前节点的区间和

2.如果当前区间和左区间相交则向左区间递归

3.如果当前区间和右区间相交则向右区间递归

4.合并左右两个区间(如果有的话)的返回的解

如此这般,所以线段树的区间匹配也不算很难。理解了就好。
具体的实现如下:

int query(int root, int l, int r) { //剩下来的区间完全等同于当前的节点 if(l == f[root].l && r == f[root].r) return f[root].max; int mid = (f[root].l + f[root].r) / 2; int ans = -oo; //和左边相交 if(l <= mid) ans = max(ans, query(f[root].lc, l, min(mid, r))); //和右边相交 if(r >= mid + 1) ans = max(ans, query(f[root].rc, max(mid + 1, l), r)); return ans; }



【线段树的点状修改】
点状修改就不提了,几乎就是点状匹配,找到那个叶节点就直接修改吧。
实现如下:

/*点状更新,更新第p个为x*/ void update(int root , int p, int x) { int l = f[root].l; int r = f[root].r; if(p < l || r < p) return ; //如果溢出(输入不符合要求) 退出 if(l == r) { //因为p在当前节点的范围内,而当前节点是叶节点,那么就刚好找到了 f[root].max = x; //直接改,用max替代值 return ; } update(f[root].lc, p, x); //向左继续查询第p个节点 update(f[root].rc, p, x); //向右继续查询第p个节点 f[root].max = max(f [ f[root].lc ].max, f [ f[root].rc ].max); }


【线段树的区间更新】
最直观的思路,匹配到这整一个区间,再逐一修改所有的叶节点的值,但也是O(nlog2n),比原来的还慢。
但是,在这里,引入一个非常先进的思想——打标记。这个思想叫做lazy-tag,比如说我们现在查询到了一个区间,接着我们要修改这个区间里的所有元素(其实在实际应用的时候因为线段树的划分要修改多个区间,但是实际上也可以看作是多次的修改一个区间),我们在这上面打一个标记。然后我们将区间匹配的算法改一下,在最前面加一个如果当前的区间节点上带有标记,则将这个标记分拆,传递给两个孩子节点。
所以新的伪代码如下:

区间匹配:

1.如果当前区间上有标记,则分拆给可能的两个孩子节点

2.如果当前节点是叶子节点,如果有标记就直接更新值,如果没有标记就返回当前节点

3.其余情况,如果当前的查询区间与左孩子结点的区间相交则继续向左递归,和右孩子相交则向右递归

区间更新:

1.先匹配到相应的若干个区间

2.在上面打好标记即可返回


所以这就是线段树的基本内容。。就是这样简单。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值