2022-02-17 22:12阅读: 67评论: 0推荐: 0

线段树入门

线段树介绍

线段树是一种基于分治思想的二叉树结构,用于在区间上进行高效的信息统计。

如图是一般的线段树结构,我们可以发现:

  1. 线段树的每个节点都代表一个区间,且按照深度递增,代表的区间逐渐缩小。
  2. 线段树是单独的一棵树,具有唯一的根节点,它代表需要统计信息的整个区间。
  3. 线段树的每个叶子节点都代表一个长度为 1 的区间 [x,x]
  4. 对于每个非叶子结点 [l,r] ,它的左节点为 [l,mid] ,右节点为 [mid+1,r] ,其中 mid=(l+r)/2

如果去除最后一层,那么线段树是一棵完全二叉树,因此我们可以采用与二叉堆类似的存储形式:

  1. 根节点编号为 1
  2. 对于非叶子节点 p ,左节点为 p×2 ,右节点为 p×2+1

需要注意的是,最后一层是不满的,我们需要空出数组的位置来表示空节点。

除去最后一层,由于最后第二层最多有 n 个节点,因此满二叉树需要 n+n2+n4++1=2×n1 个节点。

最后一层需要开 n×2 个节点,因此我们需要至少 n×4 的空间存储,才能保证数组不会越界。


线段树的建树

线段树的基本用途是维护序列的某些属性,最基本的线段树具有查询和修改两个功能。给定长度为 [1,n] 的序列,我们可以按 [1,n] 的区间建一棵线段树。线段树基于分治思想,需要从上往下构建。递归完成后也可以方便地从下往上传递信息。

下面以维护区间最大值为例。

struct seg_tree
{
    #define lc(p) (p<<1)
    #define rc(p) (p<<1|1)
    int l, r; // 节点的信息,维护 [l, r] 区间内的信息
    int maxv; // 需要在区间上维护的信息
};

const int N = 100010; // 假设序列最长长度为 N
seg_tree t[N << 2];
int a[N]; // 原序列

void build (int p, int l, int r) // 当前构建的节点及其需要维护的区间
{
    t[p].l = l, t[p].r = r;
    if (l == r) { t[p].maxv = a[l]; return; } // 到达叶子节点,不需要再往下创建节点
    int mid = l + r >> 1; // 否则创建左节点 [l, mid] 和右节点 [mid+1, r]
    build(lc(p), l, mid), build(rc(p), mid+1, r);
    // 构建完后需要维护这个区间的信息,由于已经创建好左节点(左边一半区间)和右节点(右边一半区间),根据这两个节点来维护
    t[p].maxv = max(t[lc(p)].maxv, t[rc(p)].maxv);
}

build(1, 1, n); // 从根节点1开始,它维护的是整个区间[1, n]

在线段树的单点修改过程中,每一层只会被调用一次,而线段树的高度为 logn ,因此复杂度为 logn


线段树的单点修改

利用线段树递归从下往上传递信息的结构,我们也可以很方便地修改某一个元素。

叶子节点代表单个长度的区间,也就是具体的某一个元素。我们可以递归地找到需要修改的叶子节点,在回溯的过程维护它的父节点(代表的区间包括自己的区间,所以也要修改)。

void modify (int p, int x, int v) // 需要把x位置的元素改为v,目前为p节点
{
    if (t[p].l == t[p].r) { t[p].maxv = v; return; } // 已经找到
    int mid = t[p].l + t[p].r >> 1; // 否则判断去左区间找还是去右区间找
    if (x <= mid) modify(lc(p), x, v); // 左区间为[l, mid],在这个区间内
    else modify(rc(p), x, v); // 右区间为[mid+1, r]
    t[p].maxv = max(t[lc(p)].maxv, t[rc(p)].maxv); // 注意修改完子区间后,当前区间需要维护
}

modify(1, x, v); // 从根节点开始找

线段树区间查询

以维护区间最大值的线段树为例,我们查找区间 [l,r] 的最大值,需要从根节点开始,递归完成:

  1. 如果 [l,r] 区间完全覆盖了当前节点的范围,直接返回当前节点维护的信息。
  2. 如果左节点和 [l,r] 有交叉,那么递归查询左节点。
  3. 如果右节点和 [l,r] 有交叉,那么递归查询右节点。
int query (int p, int l, int r) // 需要查询[l, r]的最大值,当前节点为p
{
    if (l <= t[p].l && r >= t[p].r) return t[p].maxv; // 完全覆盖,节点区间内所有元素都是查询区间内的元素
    int mid = t[p].l + t[p].r >> 1; // 否则查询左节点和右节点
    int maxv = -2e9; // 设为负无穷
    if (l <= mid) maxv = max(maxv, query(lc(p), l, r)); // [l, r]与左节点有交叉
    if (r > mid) maxv = max(query(rc(p), l, r)); // [l, r]与右节点有交叉
    return maxv;
}

cout << query(1, l, r) << endl; // 从根节点开始查询

关于复杂度:

  1. 在任意一个节点,只查询它的左节点/右节点。

    显然复杂度是 O(logn) 的。

  2. 在某些节点,查询左右节点。

    在左节点,显然又有两种可能,如果只查询一遍,那么可以保证 O(logn) 的复杂度,如果查询左右两边,那么左边一定是被完全覆盖的,因此可以直接回溯。右节点同理。

    所以查询左右节点,本质只是比查询单边多了一次查询,因此复杂度为 O(2logn)=O(logn)


线段树的区间修改

假设现在要把 [l,r] 中所有元素加上 v ,一种显然的做法是对 [l,r] 中所有元素执行一次单点修改,但这样的复杂度为 O(len×logn) 的,最坏情况下修改所有元素,则需要修改整棵树,复杂度 O(n)

可以发现,如果我们对区间 [l,r] 中所有元素进行修改,但后面的查询中没有用到 [l,r] 的子区间 ,那么这个修改是无意义的。也就是说,最好的办法就是在查询时才更新当前的区间

类似于区间查找,当我们发现某一个节点维护的区间被需要修改的区间覆盖,那么我们修改完当前节点后,直接回溯,不用对其递归修改,比如修改区间 [1,10] ,当我们递归到节点 [1,5] 时,就可以修改并回溯。同时我们要给这个节点打上标记,表示当前节点已经修改,但其子区间未修改。

在之后的查询过程中,如果需要使用其子结点,则使用标记的信息更新两个子结点,同时为子结点打上标记,再清除当前节点的标记。

类似于区间查询,区间修改会将区间划分为 O(logn) 个小区间,将复杂度降为 O(logn)

下面以例题 [模板]线段树1 为例。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 以维护区间和为例
struct seg_tree
{
    #define lc(x) x<<1
    #define rc(x) x<<1|1
    int l, r;
    ll sum, add; // sum为[l, r]区间元素的和,add为懒标记,记录当前这个区间,每个元素加了多少
};

const int N = 100010;
seg_tree t[N << 2];
int a[N], n, m;

void pushup (int p) // pushup操作:自下而上维护每个节点的sum值
{
    t[p].sum = t[lc(p)].sum + t[rc(p)].sum;
}

void pushdown (int p) // pushdown操作:将懒标记下传,将子节点变为真实值
{
    if (!t[p].add) return ; // 如果当前位置没有懒标记,直接返回
    t[lc(p)].sum += t[p].add * (t[lc(p)].r - t[lc(p)].l + 1); // 左节点
    t[rc(p)].sum += t[p].add * (t[rc(p)].r - t[rc(p)].l + 1); // 右节点
    t[lc(p)].add += t[p].add; // 给左节点加懒标记,注意左节点可能已经加过懒标记
    t[rc(p)].add += t[p].add; // 给右节点加懒标记,注意右节点可能已经加过懒标记
    t[p].add = 0; // 清除p的懒标记
}

void build (int p, int l, int r)
{
    t[p].l = l; t[p].r = r;
    if (l == r) { t[p].sum = a[l]; return; } // 到达叶子节点,此时区间长度为1,sum即为此位置的值
    int mid = l + r >> 1;
    build(lc(p), l, mid); build(rc(p), mid+1, r);
    pushup(p); // 使用pushup自下往上维护信息
}

void modify (int p, int l, int r, int v) // 为[l, r]区间所有数字增加v
{
    if (t[p].l >= l && t[p].r <= r) // 当前节点被[l, r]区间覆盖
    {
        // 修改当前区间,打上标记并回溯
        t[p].sum += (ll)v * (t[p].r - t[p].l + 1);
        t[p].add += v;
        return ;
    }
    pushdown(p); // 此时这个点的子节点需要使用,将懒标记下传
    int mid = t[p].l + t[p].r >> 1;
    if (l <= mid) modify(lc(p), l, r, v); // 左节点[t[p].l, mid]有部分被覆盖
    if (r > mid) modify(rc(p), l, r, v); // 右节点[mid+1, t[p].r]有部分被覆盖
    pushup(p); // 子结点被修改,记得维护当前节点
}

ll query (int p, int l, int r)
{
    if (t[p].l >= l && t[p].r <= r) return t[p].sum;
    pushdown(p); // 子节点需要使用,记得下传懒标记
    int mid = t[p].l + t[p].r >> 1;
    ll ret = 0; // 当前节点被[l, r]覆盖的元素的和
    if (l <= mid) ret += query(lc(p), l, r); // 左节点[t[p].l, mid]有部分被覆盖
    if (r > mid) ret += query(rc(p), l, r); // 右节点[mid+1, t[p].r]有部分被覆盖
    return ret;
}

int main ()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];
    build(1, 1, n); // 在区间[1, n]上建立线段树
    while(m -- )
    {
        int op, l, r, v;
        cin >> op >> l >> r;
        if (op == 1) cin >> v, modify(1, l, r, v);
        else cout << query(1, l, r) << endl;
    }
    return 0;
}

例题

  1. 一个简单的整数问题 可以使用树状数组

  2. 一个简单的整数问题2 区间加,区间和

  3. 进制 维护所有进制

  4. 你能回答这些问题吗 比较难的题目,需要维护较多信息

  5. 区间最大公约数 更损相减法

本文作者:Horb7

本文链接:https://www.cnblogs.com/Horb7/p/15906661.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Horb7  阅读(67)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.

作曲 : Reol

作词 : Reol

fade away...do over again...

fade away...do over again...

歌い始めの一文字目 いつも迷ってる

歌い始めの一文字目 いつも迷ってる

どうせとりとめのないことだけど

伝わらなきゃもっと意味がない

どうしたってこんなに複雑なのに

どうしたってこんなに複雑なのに

噛み砕いてやらなきゃ伝わらない

ほら結局歌詞なんかどうだっていい

僕の音楽なんかこの世になくたっていいんだよ

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

僕は気にしない 君は気付かない

何処にももういないいない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

忘れていく 忘れられていく

We don't know,We don't know.

目の前 広がる現実世界がまた歪んだ

目の前 広がる現実世界がまた歪んだ

何度リセットしても

僕は僕以外の誰かには生まれ変われない

「そんなの知ってるよ」

気になるあの子の噂話も

シニカル標的は次の速報

麻痺しちゃってるこっからエスケープ

麻痺しちゃってるこっからエスケープ

遠く遠くまで行けるよ

安定なんてない 不安定な世界

安定なんてない 不安定な世界

安定なんてない きっと明日には忘れるよ

fade away...do over again...

fade away...do over again...

そうだ世界はどこかがいつも嘘くさい

そうだ世界はどこかがいつも嘘くさい

綺麗事だけじゃ大事な人たちすら守れない

くだらない 僕らみんなどこか狂ってるみたい

本当のことなんか全部神様も知らない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

僕は気にしない 君は気付かない

何処にももういないいない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

忘れていく 忘れられていく

We don't know,We don't know.