浅谈线段树

列表

前置知识

线段树是什么?

线段树的思想

最初的最初( pushup \text{pushup} pushup

建树( build \text{build} build

查询( query \text{query} query

更新( update \text{update} update

优化 1 1 1 lazy-tag \text{lazy-tag} lazy-tag

优化 2 2 2(标记永久化)

最后的最后(空间提醒 & 时间复杂度说明)

代码

扩展

可持久化线段树/主席树

可持久化线段树有什么用

可持久化线段树的思想

建树( build \text{build} build

更新( update \text{update} update

查询( query \text{query} query

最后(空间复杂度分析)

代码

疑问 & 催更

前置知识

  1. 了解位运算(为线段树优化做铺垫)。
  2. 了解堆。
  3. 了解树。

如果您还没有学完,就可以不用看下面的内容了。

线段树是什么?

  • 线段树是一种可以解决区间问题的利器。

  • 线段树是一种高级数据结构。

  • 线段树是一种二叉搜索树(来源于网上)。

只不过以上这些都是介绍,其实真正的线段树并没有那么恐怖。

线段树的思想

线段树(segment tree),又名区间树。我们可以通过一个题目来直观的了解它:P3372。

我们按照常规思路想:

  1. 暴力,时间复杂度 O ( N 2 ) O(N^2) O(N2)

  2. 前缀和,由于有修改操作,所以时间复杂度为 O ( N 2 ) O(N^2) O(N2)

这个复杂度是我们不可接受的,所以我们用线段树来解决这个问题。

我们假设原来的序列为:

8
1 9 3 1 2 5 2 2

线段树,顾名思义,首先要建一棵二叉树:

一定要注意是二叉树。

这棵树按照堆式存储来编号,一共有 15 15 15 个节点。

然后我们赋予它每个节点一点值,并且把值记录为 t r e e tree tree

每个节点旁边的一个区间代表这个节点的值是这个区间的和,例如 t r e e 5 = 4 tree_5 = 4 tree5=4

那么区间又有什么特殊的性质呢?假设一个节点所代表的区间是 [ l , r ] [l, r] [l,r],那么它的两个子节点的区间就分别是 [ l , ( l + r > > 1 ) ] [l, (l + r >> 1)] [l,(l+r>>1)] [ ( l + r > > 1 ) , r ] [(l + r >> 1), r] [(l+r>>1),r]。就是分治的思想。

线段树的意义就讲完了,接下来将如何实现。

最初的最初( pushup \text{pushup} pushup

pushup \text{pushup} pushup 是一个函数,表示向上更新的意思,每次可以更新节点 c u r cur cur 的值。

pushup \text{pushup} pushup 代码:

void pushup(int cur)
{
   
    tree[cur] = tree[cur << 1] + tree[(cur << 1) + 1];   //更新
    return ;
}

其实就是一个回溯操作。

建树( build \text{build} build

这里非常简单,就是不断的分治/递归下去,最后再用 pushup \text{pushup} pushup 更新就可以了。

注意,到了叶子节点可以直接返回值。

build \text{build} build 代码:

void build(int cur, int lt, int rt) //cur 为树上节点,管辖 [lt, rt] 的数列区间
{
   
    if(lt == rt)   //递归到叶子节点
    {
   
        tree[cur] = a[lt];
        return ;
    }
    int mid = lt + rt >> 1;
    build(cur << 1, lt, mid);   //继续分治/递归
    build((cur << 1) + 1, mid + 1, rt);
    pushup(cur);  //向上传递结果
    return ;
}

这里时间复杂度为 O ( 4 N ) O(4N) O(4N)

查询( query \text{query} query

举个例子。

就比如说查询区间 [ 3 , 8 ] [3, 8] [3,8],我们首先把问题看到节点 1 1 1。节点 1 1 1 说我管得范围太大了,得让我的两个儿子来解决。然后节点 2 2 2 说,我也解决不了,又得给我的两个儿子解决。节点 4 4 4 说这事跟我没关系,但节点 5 5 5 说我正好就是问题中的一部分,可以解决。再来看节点 3 3 3,它说我也可以正好解决。所以询问的结果就是 t r e e 5 + t r e e 3 tree_5 + tree_3 tree5+tree3

从上述讲话中来看,其实就是不断的传儿子,只要在区间内就返回。

画图表示一下:

所以查询操作到的每个区间就只有三种选择:

  1. 询问的区间 [ x , y ] [x, y] [x,y] 和这个节点所覆盖的区间 [ l t , r t ] [lt, rt] [lt,rt] 根本没有关系。

图示如下:

  1. 询问的区间 [ x , y ] [x, y] [x,y] 和这个节点所覆盖的区间 [ l t , r t ] [lt, rt] [lt,rt] 是完全包含的关系。

图示如下:

  1. 问的区间 [ x , y ] [x, y] [x,y] 和这个节点所覆盖的区间 [ l t , r t ] [lt, rt] [lt,rt] 是部分包含的关系。

图示如下:

此时我们只有当区间是 3 3 3 的情形时才下传。

所以我们的代码就可以这么写:

int query(int cur, int lt, int rt, int x, int y) //cur 代表当前的节点,lt, rt 表示这个节点所管辖的区间,x, y 表示我要询问的区间
{
   
    if(x > rt || y < lt) //都已经没有关系了,直接返回 0
    {
   
        return 0;
    }
    if(x <= lt && rt <= y) //是 [x, y] 的一部分
    {
   
        return tree[cur];
    }
    int mid = lt + rt >> 1;   //向左右儿子请求答案
    return query(cur << 1, lt, mid, x, y) + query((cur << 1) + 1, mid + 1, rt, x, y);
}

这里时间复杂度是 O ( log ⁡ 2 N ) O(\log_2N) O(log2N) 的。

更新( update \text

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值