(未分类)【总结】线段树

线段树是一种高效解决区间查询与更新问题的数据结构。本文介绍了线段树的基本概念,包括如何建立线段树,如何进行单点更新和区间查询。线段树的每个节点代表一个区间,节点信息通过递归更新。单点更新和区间查询的时间复杂度均为O(logn)。
摘要由CSDN通过智能技术生成

线段树是什么?

有一类区间问题可以抽象成如下模型。

给定包含 n n n 个数的数组 a 1 , a 2 , ⋯ a_1, a_2, \cdots a1,a2,。有两种操作

查询区间 [ l , r ] [l, r] [l,r] 最小的数。

修改第 a i a_i ai x x x

这里,为了解决这个问题,我们介绍一种灵活的数据结构——线段树。


我们用一棵二叉树来表示线段树,线段树中的每个结点都表示一个区间。每个非叶子结点都有左右两棵子树,分别对应区间的 “左半” 和 “右半”。为了方便起见,我们给根结点编号为 1 1 1。对于每个结点,其左结点的编号为 2 i 2i 2i,其右结点的编号为 2 i + 1 2i+1 2i+1

对于一个结点,如果其表示的区间为 [ l , r ] [l, r] [l,r]。分情况如果 l = r l = r l=r,那么这个是一个叶子结点。否则令 m i d = ⌊ l + r 2 ⌋ mid = \lfloor \frac{l+r}{2} \rfloor mid=2l+r,左儿子对应的区间为 [ l , m i d ] [l, mid] [l,mid],右儿子对应的区间为 [ m i d + 1 , r ] [mid + 1, r] [mid+1,r],这一思想有点类似二分。下面就是 n = 10 n = 10 n=10 的时候的线段树。

在这里插入图片描述
↑点击图片观看

假定根结点表示长度为 2 h 2^h 2h的区间,不难发现,树的第 i i i 层有 2 i 2^i 2i个结点,每个结点对应一个长度为 2 h − i 2^{h-i} 2hi的区间。最大层的编号为 h h h,结点总数为 1 + 2 + 4 + 8 + ⋯ + 2 h = 2 h + 1 − 1 1+2+4+8+\cdots + 2^h=2^{h+1}-1 1+2+4+8++2h=2h+11,略小于区间长度的两倍。而当整个区间长度不是 2 2 2 的整数幂时,虽然叶子结点不在同一层,但树的最大层编号和结点总数仍满足上述结论。

有一个需要特别注意的地方,虽然线段树中总的结点数是区间长度的两倍,但是实际上,我们结点的编号不一定是连续的,所以需要开更多的内存。即 4 4 4

为什么是4倍,我们来大致的理解一下

在这里插入图片描述
首先,如果区间长度为2的正整数幂,我们会发现这棵树是一棵满二叉树,他的节点个数为 2 n − 1 2n-1 2n1

但是当我们的n为2的幂数+1时,我们的树就成了这样:

在这里插入图片描述

我们发现层数直接多了一层,这样一来我们就需要31个节点,但是这一层的使用率却很低,现在我们来想想到底应该开多大(比较严谨下)

首先,若n为2的整数次幂,最后一层空间可以刚好用完,只需 2 n − 1 2n-1 2n1个节点

但是,当n不为2的整数次幂,我们会凭空多出一层,对于这一层,如果我们全部利用上,不就相当又一个满二叉树了嘛,那么这时候对应的的n也就是最小的且大于之前n的2的整数次幂1

而这个数一定小于等于 2 n 2n 2n,所以我们把n乘个2再开空间一定没问题

那就是2n了吗?

别急,现在我们假设2n已经是2的整数次幂了,那么节点数为 2 ( 2 n ) − 1 = 4 n − 1 2(2n)-1=4n-1 2(2n)1=4n1

所以我们开4倍空间一定够啦

注意,4倍空间是线段树一个很重要的特点,在程序阅读题中看到4倍空间就要想到线段树,在用线段树做题的时候也一定要记住

OK,让我们来看一些基本操作

建立

再使用之前,我们先要对线段树建立,得到一个初始的树,这是我们才可以对他进行各种操作

前面构建的线段树,只是展示了线段树中各结点所对应的区间,但是对于用到线段树的大部分题目来说,这些线段所拥有的附加信息才是重头戏。比如要维护区间最小值问题,我们用一个额外的数组minv记录每个结点对应的区间的最小值。

对于叶子结点,最小值就是一个数。而对于非叶子结点,区间的最小值就是左儿子的最小值和右儿子最小值中的最小值。

比如 n = 10 n = 10 n=10 a = 1 , 3 , 5 , 7 , 9 , 10 , 2 , 4 , 8 , 6 a = 1, 3, 5, 7, 9, 10, 2, 4, 8, 6 a=1,3,5,7,9,10,2,4,8,6 的时候,对应的线段树如下

在这里插入图片描述
↑点击图片观看
可以发现这个构建过程是一个递归的过程,父节点的信息需要用子节点去更新,所以我们需要先递归的构建好左右子树。见下面代码。

void build(int id,int l,int r){
    if(l==r){
        minv[id]=a[l];
        return;
    }
    int mid=(l+r)>>1;
    build(id<<1,l,mid);
    build(id<<1|1,mid+1,r);
    minv[id]=min(minv[id<<1],minv[id<<1|1]);
}

所以,整个建树的过程,时间复杂度是 O ( n ) \mathcal{O}(n) O(n)

单点更新

接着我们看如何进行单点更新。如果是把整棵树推倒了全部重建,那未免也太不划算了。仔细分析,一个点的修改,只会影响到包含这个点的区间,而包含这个点的区间在树上实际上是一条链,比如我们更新 a 6 = 1 a_6 = 1 a6=1,那么我们更新的方式如下

在这里插入图片描述
↑点击图片观看
也就是先递归找到这个单点对应的子节点,然后在回溯的过程中顺带更新一波,还是比较好理解

一般,我们我们可以认为线段树的最大深度为 log ⁡ n \log n logn,所以这条链的最大长度也是 log ⁡ n \log n logn,所以一次更新时间复杂度是 O ( log ⁡ n ) \mathcal{O}(\log n) O(logn)

代码如下:

void update(int id,int l,int r,int x,int v){
    if(l==r){
        minv[id]=v;
        return;
    }
    int mid=(l+r)>>1;
    if(x<=mid){
        update(id<<1,l,mid,x,v);
    }
    else{
        update(id<<1|1,mid+1,r,x,v);
    }
    minv[id]=min(minv[id<<1],minv[id<<1|1]);
}

区间查询

对于查询的区间 [ x , y ] [x, y] [x,y] 我们可以划分为线段树上的结点,这些结点的区间合并起来就可以得到所需信息。

比如查询区间 [ 3 , 6 ] [3, 6] [3,6] 就是由下面这些红点合并起来的,其中绿点表示与被查询的区间 [ x , y ] [x, y] [x,y] 有交集的结点。

在这里插入图片描述
↑点击图片观看

我们发现,查询到一个红色的结点,可以直接返回,因为此时,区间 [ x , y ] [x, y] [x,y] 是完全覆盖红点所在的区间的,而一个绿色的结点我们还需要继续递归;

不过我们可以发现,每一层最多只会有两个绿点,最左边一个和最右边一个,这两个绿点中间的点都能被区间 [ x , y ] [x, y] [x,y] 完全覆盖,所以最多只会有 2 log ⁡ n 2\log n 2logn 个绿点,所以一次区间查询的复杂度也是 log ⁡ n \log n logn

代码:

int query(int id,int l,int r,int x,int y){
    if(x<=l && r<=y){//如果完全包含,则直接返回
        return minv[id];
    }
    int mid=(l+r)>>1;
    int ans=inf;
    if(x<=mid){
        ans=min(ans,query(id<<1,l,mid,x,y));
    }
    if(y>mid){//注意这里不是else if,因为我们有可能两边都要往下走
        ans=min(ans,query(id<<1|1,mid+1,r,x,y));
    }
    return ans;
}

  1. eg最小的且大于3的2的整数次幂是4
    最小的且大于9的2的整数次幂是16 ↩︎

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值