线段树初步

本文初步介绍了线段树 (Segment Tree),提供了一种 C++ 模板代码,并介绍了一些常用操作的实现方法。

线段树是 OI 中一种非常强大且泛用的数据结构,其目的在于在 $ O(\log n) $ 的时间内修改一个序列,在 $ O(\log n) $ 的时间内查找序列中的一些信息。

什么是线段树

基本性质

线段树,Segment Tree,顾名思义,即为多条线段组成的树。

线段树一般用于维护一段序列,并且支持高效地修改和查询某些信息,例如区间和、最值、最大子段和、方差等。

线段树本质是一棵二叉树,并且是一棵平衡二叉树。作为一棵二叉树,线段树上每个节点存储的都是一条线段(端点可以相等)的某些信息。一般地,使用存储完全二叉树的方式存储一棵线段树。

上文说过线段树每个节点维护的都是一段线段,假设有一个长度为 $ n $ 的 $ 1 $ 到 $ n $ 的序列,则线段树的根节点维护整段序列。对于任意一个维护线段长度为 $ x $ 的非叶子节点,其左儿子维护线段长度为 $ \lfloor \frac{x}{2} \rfloor $,右儿子维护线段长度为 $ x - \lfloor \frac{x}{2} \rfloor $,且左儿子维护其父节点内部下标为 $ \lfloor \frac{x}{2} \rfloor $ 的元素及其左侧部分。

根据对于线段树结构的描述,容易发现,对于线段树上任意一个节点,其儿子个数为 $ 0 $ 或 $ 2 $。

因为存储线段树时是以存储完全二叉树的方式存储,所以对于一个有 $ n $ 个元素的序列,用线段树维护这段序列时需要开 $ 4n $ 个线段树节点。

用法

对于一棵维护某些信息的线段树,可以从修改和查询两方面考虑如何维护。

修改

假设已经拥有一棵维护好的线段树,其维护一个长度为 $ n $ 的序列,下标 $ 1 $ 到 $ n $。

假设此时需要修改 $ [L, R] $ 范围内的信息,则从根节点出发,执行:

  1. 检查当前线段树节点是否被 $ [L, R] $ 完全包含,若是,修改之并返回;否,执行 $ 2 $ 。
  2. 计算当前节点中点。
  3. 分别检查当前节点左、右儿子维护的线段是否与 $ [L, R ] $ 相交,若是,对其执行 $ 1 $;否,返回。
  4. 利用左右儿子信息重新计算当前节点信息。

根据上述方法修改时,因为从根节点出发,所以一定能保证访问的每个线段树节点都与 $ [L,R] $ 有相交部分,并且线段树所有叶子节点维护的一定是退化成点的线段,所以上述方法一定可以保证所有修改的线段完整覆盖 $ [L, R] $。

显然这样修改的时间复杂度为 $ O(\log n) $。

查询

与修改时自顶而下的方式不同,查询要从具体的节点出发,自然是自下而上的结构。我们在线段树上选一些节点,利用选择节点的线段完整且不多余地拼成所要查询的线段,拼接时利用某种方式合并,最终得到所要查询线段的信息。

但显然我们的代码不能从最底层开始依次查询。所以在实际写代码时,从根节点出发,与查询类似地检查左右儿子是否与查询区间有交,利用递归地查询左右儿子的信息,并合并为当前节点的信息。

需要注意,合并的方法可能根据不同题目不同场景而有不同。但因为一定要进行合并操作,所以线段树强制要求维护的操作必须满足结合律(其实不满足结合律的操作也不多)

懒标记

相信聪明的读者已经发现,上文讲述的修改操作,在当前线段树节点被修改区间完整包含时就会停止继续访问儿子,但修改的处理逻辑并不保证被修改区间完整覆盖的线段树节点一定是叶子节点。换言之,修改的信息在线段树节点被完整覆盖时就会停止传递。

显然这样是错误的,当后续再次用到下面的节点时,节点内部的信息将会是没有修改过的错误信息。但如果修改时完整访问下面的所有节点,当下面节点利用率很低时时间开销又过大。懒标记的引入便是为了解决这一问题。

懒标记的核心思想是:某些信息在不用时就不维护,只是将其标记为“待维护”状态,在需要使用时再完整维护(下放)。

所以在查询或修改时,在访问一个线段树节点的儿子节点前,必须先对懒标记进行下放,以保证儿子节点信息的正确性。


至于建树,本质只是 init + 多次修改,很容易实现。

实例 & 模板

上述描述显然过于抽象,所以让我们用一个模板题来具体使用线段树。

例题 1

P3372 【模板】线段树 1 - 洛谷

题意

维护一个数列,支持区间加 $ k $、区间求和。

实现

模板题。题目要求维护区间和,并支持区间加,因为加法的一些优美性质,查询时的合并非常简单,只需要维护区间和,合并时相加即可。

建树

上文说过,线段树采用类似完全二叉树的存储方式,而每个节点存储的又是一条线段的信息,包括线段的左右端点,所以一般用结构体存储线段树节点。

struct node{
    int l, r;    // 线段端点
    int sum;     // 所维护的信息,此处为区间和
    int lazy;    // 懒标记
} tree[N * 4];

建树时,对于每个节点初始化,并存储初始区间和即可。

void build(int l, int r, int u) {
    // l, r 为当前线段的左右端点,u 为当前节点的编号
    tree[u].l = l;
    tree[u].r = r;
    tree[u].lazy = 0;
    if (l == r) {
        tree[u].sum = a[l];
        // a 为原序列
        // 对于一个点,其区间和即为本身
        return;
    }
    
    int mid = (l + r) >> 1;
    build(l, mid, u * 2);
    build(mid + 1, r, u * 2 + 1);
    tree[u].sum = tree[u * 2].sum + tree[u * 2 + 1].sum;
    // 利用左右儿子区间和合并求得当前节点区间和
    
    return;
}

修改

因为区间所加的数是固定的,我们可以轻易算出任意一个区间总增加的数值。假设区间长度为 $ n $,增加 $ k $,则该区间总和增加 $ nk $。那么修改的实现便非常容易了,计算区间长度乘增加的数值即可。

void update(int l, int r, int k, int u) {
    if (tree[u].l >= l && tree[u].r <= r) {    // 被完全覆盖
        tree[u].sum += (tree[u].r - tree[u].l + 1) * k;
        tree[u].lazy += k;
        
        return;
    }
    
    push_down(u);    // 访问儿子前下放懒标记
    
    int mid = (tree[u].l + tree[u].r) >> 1;
    if (mid >= l) update(l, r, k, u * 2);
    if (mid < r) update(l, r, k, u * 2 + 1);
    tree[u].sum = tree[u * 2].sum + tree[u * 2 + 1].sum;    // 更新当前节点信息,push_up
    
    return;
}

查询

同样的,因为加法的优秀性质,直接求和即可。

int query(int l, int r, int u) {
    // l, r 为目标查询区间
    if (tree[u].l >= l && tree[u].r <= r) {
        return tree[u].sum;
    }
    
    push_down(u);
    
    int mid = (tree[u].l + tree[u].r) >> 1, sum = 0;
    if (mid >= l) sum += query(l, r, u * 2);
    if (mid < r) sum += query(l, r, u * 2 + 1);
    
    return sum;
}

push down

懒标记下放。与修改类似,根据所加的值和区间长度,乘算总增量,加和计算即可。注意检查一下懒标记是否存在可能能优化常数。

void push_down(int u) {
    if (tree[u].lazy) {
        int llen = tree[u * 2].r - tree[u * 2].l + 1;
        int rlen = tree[u * 2 + 1].r - tree[u * 2 + 1].l + 1;
        
        tree[u * 2].sum += llen * tree[u].lazy;
        tree[u * 2 + 1].sum += rlen * tree[u].lazy;
        
        tree[u * 2].lazy += tree[u].lazy;
        tree[u * 2 + 1].lazy += tree[u].lazy;
        
        tree[u].lazy = 0;
    }
    
    return;
}

AC Code

#include <iostream>
#include <cstring>
#include <algorithm>

#define int long long 

using namespace std;

const int N = 1e5 + 10;

struct node{
    int l, r, sum, lazy;
} tree[N * 4];

int n, q, a[N];

void build(int l, int r, int u) {
    tree[u].l = l;
    tree[u].r = r;
    tree[u].lazy = 0;
    if (l == r) {
        tree[u].sum = a[l];
        
        return;
    }
    
    int mid = (l + r) >> 1;
    build(l, mid, u * 2);
    build(mid + 1, r, u * 2 + 1);
    tree[u].sum = tree[u * 2].sum + tree[u * 2 + 1].sum;
    
    return;
}

void push_down(int u) {
    if (tree[u].lazy) {
        int llen = tree[u * 2].r - tree[u * 2].l + 1;
        int rlen = tree[u * 2 + 1].r - tree[u * 2 + 1].l + 1;
        
        tree[u * 2].sum += llen * tree[u].lazy;
        tree[u * 2 + 1].sum += rlen * tree[u].lazy;
        
        tree[u * 2].lazy += tree[u].lazy;
        tree[u * 2 + 1].lazy += tree[u].lazy;
        
        tree[u].lazy = 0;
    }
    
    return;
}

int query(int l, int r, int u) {
    if (tree[u].l >= l && tree[u].r <= r) {
        return tree[u].sum;
    }
    
    push_down(u);
    
    int mid = (tree[u].l + tree[u].r) >> 1, sum = 0;
    if (mid >= l) sum += query(l, r, u * 2);
    if (mid < r) sum += query(l, r, u * 2 + 1);
    
    return sum;
}

void update(int l, int r, int k, int u) {
    if (tree[u].l >= l && tree[u].r <= r) {
        tree[u].sum += (tree[u].r - tree[u].l + 1) * k;
        tree[u].lazy += k;
        
        return;
    }
    
    push_down(u);
    
    int mid = (tree[u].l + tree[u].r) >> 1;
    if (mid >= l) update(l, r, k, u * 2);
    if (mid < r) update(l, r, k, u * 2 + 1);
    tree[u].sum = tree[u * 2].sum + tree[u * 2 + 1].sum;
    
    return;
}

signed main() {
    cin >> n >> q;
    for (int i = 1; i <= n; i ++) cin >> a[i];
    
    build(1, n, 1);
    
    while (q --) {
        int op;
        cin >> op;
        if (op == 1) {
            int x, y, k;
            cin >> x >> y >> k;
            update(x, y, k, 1);
        } else {
            int l, r;
            cin >> l >> r;
            cout << query(l, r, 1) << endl;
        }
    }
    
    return 0;
}

例题 2

上文讲解了只有区间求和时的线段树写法,因为加法的优秀性质,这种情况代码实现非常简单。但是大部分情况下,线段树不止需要支持一种操作,可能会需要支持多种更新操作。此时需要考虑不同操作之间的互相影响,一般需要分别维护独立的懒标记。

P1253 扶苏的问题 - 洛谷

本题中需要同时维护区间推平和区间加操作,并维护区间最值。先考虑两种操作对于区间最值的影响。对于区间推平操作,区间最值会修改为所推平的值;对于区间加操作,最值加上所加的值。

显然在下放懒标记时,我们必须钦定某种下放顺序以保证信息正确。由于区间推平会毁灭所有的区间加操作,所以我们钦定先推平后加。

那么在此种顺序下,考虑操作间影响如下表:

TAG \ 操作 区间推平 区间加
区间推平 修改为后推平的值 无影响
区间加 消灭 加和

由此得出两种修改操作进行时对懒标记的影响。

AC Code

#include <iostream>
#include <cstring>
#include <algorithm>

#define int long long 

using namespace std;

const int N = 1e6 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;

struct node{
    int l, r, Max, add_lazy, change_lazy;
} tree[N * 4];

int n, q, a[N];

void build(int l, int r, int u) {
    tree[u].l = l;
    tree[u].r = r;
    tree[u].add_lazy = 0;
    tree[u].change_lazy = INF;
    if (l == r) {
        tree[u].Max = a[l];
        
        return;
    }
    
    int mid = (l + r) >> 1;
    build(l, mid, u * 2);
    build(mid + 1, r, u * 2 + 1);
    tree[u].Max = max(tree[u * 2].Max, tree[u * 2 + 1].Max);
    
    return;
}

void push_down(int u) {
    if (tree[u].change_lazy != INF) {
        tree[u * 2].Max = tree[u].change_lazy;
        tree[u * 2 + 1].Max = tree[u].change_lazy;

        tree[u * 2].change_lazy = tree[u].change_lazy;
        tree[u * 2 + 1].change_lazy = tree[u].change_lazy;

        tree[u].change_lazy = INF;
        tree[u * 2].add_lazy = 0;
        tree[u * 2 + 1].add_lazy = 0;
    }
    
    if (tree[u].add_lazy) {
        tree[u * 2].Max += tree[u].add_lazy;
        tree[u * 2 + 1].Max += tree[u].add_lazy;

        tree[u * 2].add_lazy += tree[u].add_lazy;
        tree[u * 2 + 1].add_lazy += tree[u].add_lazy;

        tree[u].add_lazy = 0;
    }
    
    return;
}

int query(int l, int r, int u) {
    if (tree[u].l >= l && tree[u].r <= r) {
        return tree[u].Max;
    }
    
    push_down(u);
    
    int mid = (tree[u].l + tree[u].r) >> 1, ans = -INF;
    if (mid >= l) ans = max(ans, query(l, r, u * 2));
    if (mid < r) ans = max(ans, query(l, r, u * 2 + 1));
    
    return ans;
}

void update(int l, int r, int k, int u) {
    if (tree[u].l >= l && tree[u].r <= r) {
        tree[u].Max += k;
        tree[u].add_lazy += k;
        
        return;
    }
    
    push_down(u);
    
    int mid = (tree[u].l + tree[u].r) >> 1;
    if (mid >= l) update(l, r, k, u * 2);
    if (mid < r) update(l, r, k, u * 2 + 1);
    tree[u].Max = max(tree[u * 2].Max, tree[u * 2 + 1].Max);
    
    return;
}

void change(int l, int r, int k, int u) {
    if (tree[u].l >= l && tree[u].r <= r) {
        tree[u].Max = k;
        tree[u].change_lazy = k;
        tree[u].add_lazy = 0;
        
        return;
    }
    
    push_down(u);
    
    int mid = (tree[u].l + tree[u].r) >> 1;
    if (mid >= l) change(l, r, k, u * 2);
    if (mid < r) change(l, r, k, u * 2 + 1);
    tree[u].Max = max(tree[u * 2].Max, tree[u * 2 + 1].Max);
    
    return;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> q;
    for (int i = 1; i <= n; i ++) cin >> a[i];
    
    build(1, n, 1);
    
    while (q --) {
        int op;
        cin >> op;
        if (op == 1) {
            int x, y, k;
            cin >> x >> y >> k;
            change(x, y, k, 1);
        } else if (op == 2) {
            int l, r, k;
            cin >> l >> r >> k;
            update(l, r, k, 1);
        } else {
            int l, r;
            cin >> l >> r;
            cout << query(l, r, 1) << endl;
        }
    }
    
    return 0;
}

拓展

(待办)

  • 线段树维护最大子段和
  • 动态开点线段树
  • 权值线段树
  • 线段树二分
  • 势能分析

参考资料

线段树基础 - OI Wiki

2026/8/4

posted @ 2026-08-04 22:24  D'Angelo  阅读(5)  评论(0)    收藏  举报