数据结构——线段树

数据结构------线段树

  • [数据结构 线段树](#数据结构 线段树)
  • 线段树维护更多类型的信息
    • [P1816 忠诚 - 洛谷 RMQ问题](#P1816 忠诚 - 洛谷 RMQ问题)
    • [P3870 开关 - 洛谷](#P3870 开关 - 洛谷)
    • [P2184 贪婪大陆 - 洛谷](#P2184 贪婪大陆 - 洛谷)
    • [P1438 无聊的数列 - 洛谷](#P1438 无聊的数列 - 洛谷)
  • 多种区间操作
  • 线段树与其他知识结合
    • [线段树 + 分治](#线段树 + 分治)
      • [P4513 小白逛公园 - 洛谷](#P4513 小白逛公园 - 洛谷)
      • [P2572 [SCOI2010\] 序列操作 - 洛谷](#P2572 [SCOI2010] 序列操作 - 洛谷)
    • [势能线段树 + 剪枝](#势能线段树 + 剪枝)
      • [P4145 花神游历各国 - 洛谷](#P4145 花神游历各国 - 洛谷)
      • [Problem - 438D - Codeforces](#Problem - 438D - Codeforces)
    • [权值线段树 + 离散化](#权值线段树 + 离散化)
      • [P1908 逆序对 - 洛谷](#P1908 逆序对 - 洛谷)
    • [线段树 + 数学](#线段树 + 数学)
      • [P5142 区间方差 - 洛谷](#P5142 区间方差 - 洛谷)
      • [P10463 Interval GCD - 洛谷](#P10463 Interval GCD - 洛谷)
  • OJ参考

数据结构 线段树

线段树 (Segment tree)是一种数据结构,是算法入门和进阶的分水岭。

因为线段树的整体代码量较大,需要有树、递归和分治思想的基础,还能与其他知识点组合成综合题,甚至还能用于优化动态规划。

线段树长用于解决以下问题:

  1. 有 n ( n ≤ 10 5 ) n(n \leq 10^5) n(n≤105) 个数, q ( 1 ≤ 10 5 ) q(1 \leq 10^5) q(1≤105) 次操作,每次操作为询问区间 [ l , r ] [l, r] [l,r] 的和。可用前缀和解决。

  2. 有 n ( n ≤ 10 5 ) n(n \leq 10^5) n(n≤105) 个数, q ( 1 ≤ 10 5 ) q(1 \leq 10^5) q(1≤105) 次操作,操作有两种:

    a. 查询区间 [ l , r ] [l, r] [l,r] 的和;

    b. 将第 i i i 个数修改成 x x x。因为这个操作,前缀和就无法解决这个问题,且问题很可能存在两个操作大量地交替进行。

  3. 有 n ( n ≤ 10 5 ) n(n \leq 10^5) n(n≤105) 个数, q ( 1 ≤ 10 5 ) q(1 \leq 10^5) q(1≤105) 次操作,操作有两种:

    a. 查询区间 [ l , r ] [l, r] [l,r] 的和;

    b. 将区间 [ l , r ] [l, r] [l,r] 的数全部修改成 x x x。因为这个操作,前缀和更无法解决这个问题。

  4. 有 n ( n ≤ 10 5 ) n(n \leq 10^5) n(n≤105) 个数, q ( 1 ≤ 10 5 ) q(1 \leq 10^5) q(1≤105) 次操作,每次操作为区间 [ l , r ] [l, r] [l,r] 的最大值或者最小值。这个操作,前缀和同样无法解决这个问题,需要使用单调队列求滑动窗口的最值,但长度会变化。

第 4 个是 RMQ(区间内最大、最小值查询Range Minimum/Maximum Query)问题。

以上问题,采用暴力解法很有可能会超时。这时需要引入功能更强大的数据结构:线段树。

线段树是一种二叉树数据结构 ,常用来维护区间信息 ,可以在 O ( log ⁡ n ) \text{O}(\log n) O(logn) 级别的时间复杂度内完成:区间的单点修改、区间修改、区间查询(区间和、区间最大最小值)等操作。

单点即长度为 1 的区间。长度为 1 的区间也是区间。

区间在数组中可看成一段,或一个线段,所以这种数据结构被叫做线段树。

区间和线段树的构建

线段树是基于分治思想的二叉树树中的每一个结点都会维护一段区间的信息 。其中叶结点存储元素本身非叶结点维护区间内元素的信息。构建线段树时也可以通过分治思想进行。

以数组 a = [ 5 , 1 , 3 , 0 , 2 , 7 , 4 , 5 , 8 ] a = [5, 1, 3, 0, 2, 7, 4, 5, 8] a=[5,1,3,0,2,7,4,5,8] 为例,如果查询的是区间和,我们会创建出来这样一棵树来维护信息:

根据构建方式,可以得到以下性质:

  • 线段树的每个结点都维护一个区间的信息

  • 线段树中的根节点维护整个区间的信息,叶子结点维护长度为 1 的区间信息。

  • 可以用结构体数组来实现线段树 ,类似堆的存储方式,也就是二叉树的静态存储 。此时父节点的编号为 p p p 时,左孩子编号为 p × 2 p \times 2 p×2,右孩子编号为 p × 2 + 1 p \times 2 + 1 p×2+1 。

    因为线段树的一个结点保存有多个信息,所以存储线段树,要么用多个数组描述,要么弄一个结构体数组进行描述。

  • 若当前结点维护的区间为 [ l , r ] [l, r] [l,r],那么左右孩子分别维护 [ l , m i d ] [l, mid] [l,mid] 以及 [ m i d + 1 , r ] [mid + 1, r] [mid+1,r] 区间的信息。

  • 线段树的空间,需要开最大区间的 4 倍。这个结论可通过推导获得:

    当区间长度为 2 x 2^x 2x 时,得到的线段树就是一个满二叉树。此外的区间长度都是在满二叉树的基础上,左、右子树的叶结点上按顺序添加子树。

    且这里的完全二叉树的树高可以认为是 h = log 2 n + 2 h=\text{log}_2 n+2 h=log2n+2 ,则线段树的总的结点数 N = 2 h − 1 = 2 log 2 n + 2 − 1 = 4 n − 1 N=2^h-1=2^{\text{log}_2 n+2}-1=4n-1 N=2h−1=2log2n+2−1=4n−1 。所以若原始区间的长度为 n n n ,则线段树所需空间大小 N N N 至少是 4 n 4n 4n 。

    理想情况下,线段树为满二叉树,此时的结点总数为 n + n 2 + n 4 + ⋯ + 2 + 1 = n 1 − 1 2 n 1 − 1 2 = 2 n − n 2 n − 1 ≈ 2 n − 1 n+\frac{n}{2}+\frac{n}{4}+\dots+2+1=n\frac{1-\frac{1}{2^n}}{1-\frac{1}{2}}=2n-\frac{n}{2^{n-1}}\approx 2n-1 n+2n+4n+⋯+2+1=n1−211−2n1=2n−2n−1n≈2n−1 。但实际上线段树最后一层还有很多空余,所以为保证线段树能正常建树,空间大小 N N N 至少是 4 n 4n 4n 。

所以建树的时间复杂度: O ( n ) \text{O}(n) O(n) 。实际上是 4 n 4n 4n ,即最差情况下整个线段树的空间都会遍历一遍。

参考程序(封装):

cpp 复制代码
using vi = vector<int>;
typedef long long LL;
struct Segment_tree {
    struct Node {
        int l;
        int r;
        LL sum;
        Node(int _l = 0, int _r = 0, LL _sum = 0)
            : l(_l), r(_r), sum(_sum) {}
    };
    vector<Node> sgt; // segment tree,存储区间和信息的线段树本体

    Segment_tree(vi &arr) {
        sgt.resize(arr.size() * 4);
        // 分治建树
        _build(1, arr, 1, arr.size() - 1);
    }

    void _build(int p, vi &arr, int l, int r) {
        sgt[p] = {l, r, 0};
        if (l >= r) { // 可以是等于
            sgt[p].sum = arr[l];
            return;
        }
        int mid = (l + r) / 2;
        _build(p * 2, arr, l, mid);
        _build(p * 2 + 1, arr, mid + 1, r);
        adjust_info(p); // 线段树调整信息,建议额外封装
    }

    void adjust_fa(int p) {
        sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum;
    }
};

不封装:

cpp 复制代码
// 这两个宏是为了写代码方便
// 平时使用 STL 做抗压训练,除非对时间要求太过变态
#define lc p << 1
#define rc p << 1 | 1
typedef long long LL;
const int N = 1e5 + 10;

LL a[N]; // 原始数组
struct Node {
    LL l, r, sum;
} tr[N << 2];
// 整合左右孩子的信息
void adjust_fa(int p) {
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}

// 建树
void build(int p, int l, int r) {
    tr[p] = {l, r, 0};    // 初始化
    if (l == r) {         // 叶结点
        tr[p].sum = a[l]; // 更新 sum 的值
        return;
    }
    int mid = (l + r) >> 1; // (l+r)/2
    build(lc, l, mid);      // 构建左子树
    build(rc, mid + 1, r);  // 构建右子树
    // tr[p].sum = tr[lc].sum + tr[rc].sum;
    adjust_fa(p); // 线段树调整信息,建议额外封装
}

个人更习惯使用封装版本,因此后续除了个别数据量过大的问题,均为封装的线段树。

区间查询

对于一个待查询的区间,用拆分 + + + 拼凑的思想,将区间拆分成小区间,用这个小区间的信息拼凑成大区间的信息,在线段树的结点中收集结果。具体流程:

  1. 从根节点出发,向下递归。

  2. 如果当前结点维护的区间信息包含在待查询的区间内

    l <= sgt[p].l && sgt[p].r <= r),直接返回结点维护的信息。

  3. 如果左区间有重叠(l <= mid),去左子树上找结果。

  4. 如果右区间有重叠(r>mid),去右子树上找结果。

以数组 a = [ 5 , 1 , 3 , 0 , 2 , 2 , 7 , 4 , 5 , 8 ] a = [5, 1, 3, 0, 2, 2, 7, 4, 5, 8] a=[5,1,3,0,2,2,7,4,5,8] 为例,如果查询的是区间 [ 3 , 8 ] [3, 8] [3,8] 的和,维护的信息如下:

参考程序:

cpp 复制代码
using LL = long long;
LL Segment_tree::query(int p, int l, int r) {
    if (l <= sgt[p].l && sgt[p].r <= r) // 当前区间为待查询区间的子区间
        return sgt[p].sum;
    LL sum = 0;
    int mid = (sgt[p].l + sgt[p].r) / 2;
    if (l <= mid)
        sum += query(p * 2, l, r);
    if (mid < r)
        sum += query(p * 2 + 1, l, r);
    return sum;
}

时间复杂度:最差情况是子树的左、右子树都包含要查询的区间的信息。以左子树为例,假设每次都是最差的情况,若每次递归遍历时, mid 始终包含在区间内,则只会遍历右子树的根结点,而最左端会遍历到叶结点。右子树也是如此。

所以实际遍历的结点数的数量级大致为树高,所以整体的时间复杂度为 O ( log ⁡ n ) \text{O}(\log n) O(logn) 。

P3374 【模板】树状数组 1 - 洛谷 线段树单点修改

P3374 【模板】树状数组 1 - 洛谷

树状数组也是一种数据结构。凡是树状数组的问题,线段树大都能解决,除非出题人弄个很大的数据量,只有用更轻量级的树状数组才能解决。

具体流程:

  1. 递归找到叶子结点,并且维护修改之后的信息;
  2. 然后一路向上回溯,修改所有路径上的结点信息,使的维护的信息为修改之后的信息。

如果是对单个位置上的数执行:减去一个数,乘上一个数,除以一个数的操作,都可以转换成加上一个数。

单点修改参考:

cpp 复制代码
// 单点修改
void Segment_tree::modify(int p, int x, LL k) {
    if (sgt[p].l == x && sgt[p].r == x) {
        sgt[p].sum += k;
        return;
    }
    int mid = (sgt[p].l + sgt[p].r) / 2;
    if (x <= mid)
        modify(p * 2, x, k);
    else
        modify(p * 2 + 1, x, k);
    adjust_info(p);
}

P3374 【模板】树状数组 1 - 洛谷 参考程序:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
using vl = vector<LL>;
struct Segment_tree {
    struct Node {
        int l;
        int r;
        LL sum;
        Node(int _l = 0, int _r = 0, LL _sum = 0) : l(_l), r(_r), sum(_sum) {}
    };
    vector<Node> sgt; // segment tree,存储区间和信息的线段树本体

    Segment_tree(vl &arr) {
        sgt.resize(arr.size() * 4);
        // 分治建树
        _build(1, arr, 1, arr.size() - 1);
    }

    void _build(int p, vl &arr, int l, int r) {
        sgt[p] = {l, r, 0};
        if (l >= r) {
            sgt[p].sum = arr[l];
            return;
        }
        int mid = (l + r) / 2;
        _build(p * 2, arr, l, mid);
        _build(p * 2 + 1, arr, mid + 1, r);
        adjust_info(p); // 线段树调整信息,建议额外封装
    }

    void adjust_info(int p) {
        sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum;
    }

    LL query(int p, int l, int r) {
        if (l <= sgt[p].l && sgt[p].r <= r)
            return sgt[p].sum;
        LL sum = 0, mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            sum += query(p * 2, l, r);
        if (mid < r)
            sum += query(p * 2 + 1, l, r);
        return sum;
    }

    // 单点修改
    void modify(int p, int x, LL k) {
        if (sgt[p].l == x && sgt[p].r == x) {
            sgt[p].sum += k;
            return;
        }
        int mid = (sgt[p].l + sgt[p].r) / 2;
        if (x <= mid)
            modify(p * 2, x, k);
        else
            modify(p * 2 + 1, x, k);
        adjust_info(p);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    vl a;
    cin >> n >> m;
    a.resize(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    Segment_tree sgt(a);
    while (m--) {
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 1)
            sgt.modify(1, x, y);
        else
            cout << sgt.query(1, x, y) << '\n';
    }
    return 0;
}

区间修改

依旧是例子 a = [ 5 , 1 , 3 , 0 , 2 , 2 , 7 , 4 , 5 , 8 ] a = [5, 1, 3, 0, 2, 2, 7, 4, 5, 8] a=[5,1,3,0,2,2,7,4,5,8] ,对区间 [ 4 , 9 ] [4,9] [4,9] 上每个元素增加 2。如果按照单点修改的方式,把所有 [ 4 , 9 ] [4,9] [4,9] 所覆盖的叶结点全部修改,时间复杂度将是 O ( n ) \text{O}(n) O(n)。

懒标记思路

若某个结点维护的区间 [ l , r ] [l,r] [l,r] 被修改的区间 [ x , y ] [x,y] [x,y] 完全覆盖时,如果想在 O ( 1 ) \text{O}(1) O(1) 时间内修改区间维护的信息,那么左右子树完全没有必要立刻修改。可以等到下次查询的时候,再去处理。

借助这样的思路,在原有线段树的基础上,在每一个结点中额外维护一个标记

  • 当前结点维护的区间 [ l , r ] [l,r] [l,r] 被待查询区间 [ x , y ] [x,y] [x,y] 完全覆盖 时,停止递归 ,根据区间长度维护出增加元素之后的和。不去处理左右孩子,而是做好标记
  • 等到下次修改或者查询操作 ,遇到该节点时,再把标记下放给左右孩子下放时只会下放一层 ,同时清空上层递归的标记

这个标记有多重称呼,有懒标记,有延迟标记。这里统一称呼为懒标记。通过懒标记即可把时间控制的与查询时间一致,都是 log ⁡ ( n ) \log(n) log(n) 级别。

例如对线段树 [ 5 , 1 , 3 , 0 , 2 , 2 , 7 , 4 , 5 , 8 ] [5, 1, 3, 0, 2, 2, 7, 4, 5, 8] [5,1,3,0,2,2,7,4,5,8] 的区间 [ 4 , 9 ] [4,9] [4,9] 进行修改并打上标记 add (或lz,变量名不同但功能一样,因为图是网络上找的,代码是自己摸索的):

然后再查询 [ 5 , 7 ] [5,7] [5,7] 的元素,同时下方懒标记。

参考程序(集大成的线段树模板):

cpp 复制代码
typedef long long LL;
using vl = vector<LL>;
struct Segment_tree {
    struct Node {
        int l;  // 区间左端点,闭区间
        int r;  // 区间右端点,闭区间
        LL sum; // 线段树维护的区间和信息
        LL lz;  // 区间和的懒标记
        Node(int _l = 0, int _r = 0, LL _sum = 0, LL _lz = 0)
            : l(_l), r(_r), sum(_sum), lz(_lz) {}
    };
    vector<Node> sgt; // segment tree,存储区间和信息的线段树本体

    Segment_tree(vl &arr) {
        sgt.resize(arr.size() * 4);
        // 分治建树
        _build(1, arr, 1, arr.size() - 1);
    }
    // 分治建树
    void _build(int p, vl &arr, int l, int r) {
        sgt[p] = {l, r, 0, 0};
        if (l >= r) {
            sgt[p].sum = arr[l];
            return;
        }
        int mid = (l + r) / 2;
        _build(p * 2, arr, l, mid);
        _build(p * 2 + 1, arr, mid + 1, r);
        adjust_fa(p); // 线段树结点调整父结点调整信息,建议额外封装
    }

    // 调整父结点的信息
    inline void adjust_fa(int p) {
        sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum;
    }

    // 调整子结点的信息,重点是懒标记下放
    void adjust_ch(int p, LL lz) {
        _adjust_ch(p * 2, lz);
        _adjust_ch(p * 2 + 1, lz);
        sgt[p].lz = 0; // 清空自身懒标记
    }
    inline void _adjust_ch(int p, LL lz) {
        sgt[p].sum += (sgt[p].r - sgt[p].l + 1) * lz;
        sgt[p].lz += lz;
    }

    // 区间查询
    LL query(int p, int l, int r) {
        if (l <= sgt[p].l && sgt[p].r <= r)
            return sgt[p].sum;
        // 在原有的基础上新增懒标记下放
        adjust_ch(p, sgt[p].lz);
        LL sum = 0, mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            sum += query(p * 2, l, r);
        if (mid < r)
            sum += query(p * 2 + 1, l, r);
        return sum;
    }

    // 区间修改
    void modify(int p, int l, int r, LL k) {
        if (l <= sgt[p].l && sgt[p].r <= r) {
            _adjust_ch(p, k);
            return;
        }
        // 修改时也要先查询,凡是查询都要下放懒标记
        adjust_ch(p, sgt[p].lz);
        int mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            modify(p * 2, l, r, k);
        if (mid < r)
            modify(p * 2 + 1, l, r, k);
        adjust_fa(p);
    }
};

简单总结:当涉及区间修改,加上懒标记之后,查询和修改操作递归到某个区间后:

  1. 如果当前结点维护的区间 [l, r] 包含在查询的区间 [sgt[p].l, sgt[p].r] 中(l<=sgt[p].l&&sgt[p].r<=r):
    • 此时就可以根据区间长度计算出区间和(或别的区间维护的信息),没有必要继续递归下去。
    • 那么,利用区间长度计算出区间和,打上一个懒标记,就可以向上返回。
  2. 如果当前结点维护的区间 [ l , r ] [l, r] [l,r] 只有一部分在查询区间 [ x , y ] [x, y] [x,y] 中:
    • 把该节点存储的懒标记下发一层( adjust_ch);
    • 根据查询区间的范围,递归到左右区间;
    • 等左右区间处理完毕之后,维护当前结点的区间和信息 adjust_fa

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

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

模板题,考察区间查询加区间修改。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
using vl = vector<LL>;
struct Segment_tree {
    struct Node {
        int l;  // 区间左端点,闭区间
        int r;  // 区间右端点,闭区间
        LL sum; // 线段树维护的区间和信息
        LL lz;  // 区间和的懒标记
        Node(int _l = 0, int _r = 0, LL _sum = 0, LL _lz = 0)
            : l(_l), r(_r), sum(_sum), lz(_lz) {}
    };
    vector<Node> sgt; // segment tree,存储区间和信息的线段树本体

    Segment_tree(vl &arr) {
        sgt.resize(arr.size() * 4);
        // 分治建树
        _build(1, arr, 1, arr.size() - 1);
    }
    // 分治建树
    void _build(int p, vl &arr, int l, int r) {
        sgt[p] = {l, r, 0, 0};
        if (l >= r) {
            sgt[p].sum = arr[l];
            return;
        }
        int mid = (l + r) / 2;
        _build(p * 2, arr, l, mid);
        _build(p * 2 + 1, arr, mid + 1, r);
        adjust_fa(p); // 线段树结点调整父结点调整信息,建议额外封装
    }

    // 调整父结点的信息
    inline void adjust_fa(int p) {
        sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum;
    }

    // 调整子结点的信息,重点是懒标记下放
    void adjust_ch(int p, LL lz) {
        _adjust_ch(p * 2, lz);
        _adjust_ch(p * 2 + 1, lz);
        sgt[p].lz = 0; // 清空自身懒标记
    }
    inline void _adjust_ch(int p, LL lz) {
        sgt[p].sum += (sgt[p].r - sgt[p].l + 1) * lz;
        sgt[p].lz += lz;
    }

    // 区间查询
    LL query(int p, int l, int r) {
        if (l <= sgt[p].l && sgt[p].r <= r)
            return sgt[p].sum;
        // 在原有的基础上新增懒标记下放
        adjust_ch(p, sgt[p].lz);
        LL sum = 0, mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            sum += query(p * 2, l, r);
        if (mid < r)
            sum += query(p * 2 + 1, l, r);
        return sum;
    }

    // 区间修改
    void modify(int p, int l, int r, LL k) {
        if (l <= sgt[p].l && sgt[p].r <= r) {
            _adjust_ch(p, k);
            return;
        }
        // 修改时也要先查询,凡是查询都要下放懒标记
        adjust_ch(p, sgt[p].lz);
        int mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            modify(p * 2, l, r, k);
        if (mid < r)
            modify(p * 2 + 1, l, r, k);
        adjust_fa(p);
    }
};

int main() {
    int n, m;
    vl a;
    cin >> n >> m;
    a.resize(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    Segment_tree stt(a);
    for (int i = 0, T = m; i < T; i++) {
        int op = 0;
        cin >> op;
        if (op == 1) {
            LL l, r, k;
            cin >> l >> r >> k;
            stt.modify(1, l, r, k);
        } else if (op == 2) {
            LL l, r;
            cin >> l >> r;
            cout << stt.query(1, l, r) << "\n";
        }
    }
    return 0;
}

P3368 【模板】树状数组 2 - 洛谷

P3368 【模板】树状数组 2 - 洛谷

树状数组模板题,考察长度为 1 的区间查询加区间修改。可用线段树解决。

实际上单点修改并不依赖懒标记,因为回溯的过程中就会修改沿途的值。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
using vl = vector<LL>;
struct Segment_tree {
    struct Node {
        int l;  // 区间左端点,闭区间
        int r;  // 区间右端点,闭区间
        LL sum; // 线段树维护的区间和信息
        LL lz;  // 区间和的懒标记
        Node(int _l = 0, int _r = 0, LL _sum = 0, LL _lz = 0)
            : l(_l), r(_r), sum(_sum), lz(_lz) {}
    };
    vector<Node> sgt; // segment tree,存储区间和信息的线段树本体

    Segment_tree(vl &arr) {
        sgt.resize(arr.size() * 4);
        // 分治建树
        _build(1, arr, 1, arr.size() - 1);
    }
    // 分治建树
    void _build(int p, vl &arr, int l, int r) {
        sgt[p] = {l, r, 0, 0};
        if (l >= r) {
            sgt[p].sum = arr[l];
            return;
        }
        int mid = (l + r) / 2;
        _build(p * 2, arr, l, mid);
        _build(p * 2 + 1, arr, mid + 1, r);
        adjust_fa(p); // 线段树结点调整父结点调整信息,建议额外封装
    }

    // 调整父结点的信息
    inline void adjust_fa(int p) {
        sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum;
    }

    // 调整子结点的信息,重点是懒标记下放
    void adjust_ch(int p, LL lz) {
        _adjust_ch(p * 2, lz);
        _adjust_ch(p * 2 + 1, lz);
        sgt[p].lz = 0; // 清空自身懒标记
    }
    inline void _adjust_ch(int p, LL lz) {
        sgt[p].sum += (sgt[p].r - sgt[p].l + 1) * lz;
        sgt[p].lz += lz;
    }

    // 区间查询
    LL query(int p, int l, int r) {
        if (l <= sgt[p].l && sgt[p].r <= r)
            return sgt[p].sum;
        // 在原有的基础上新增懒标记下放
        adjust_ch(p, sgt[p].lz);
        LL sum = 0, mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            sum += query(p * 2, l, r);
        if (mid < r)
            sum += query(p * 2 + 1, l, r);
        return sum;
    }

    // 单点修改
    void modify(int p, int x, LL k) {
        if (sgt[p].l == x && sgt[p].r == x) {
            sgt[p].sum += k;
            return;
        }
        int mid = (sgt[p].l + sgt[p].r) / 2;
        if (x <= mid)
            modify(p * 2, x, k);
        else
            modify(p * 2 + 1, x, k);
        adjust_fa(p);
    }

    // 区间修改
    void modify(int p, int l, int r, LL k) {
        if (l <= sgt[p].l && sgt[p].r <= r) {
            _adjust_ch(p, k);
            return;
        }
        // 修改时也要先查询,凡是查询都要下放懒标记
        adjust_ch(p, sgt[p].lz);
        int mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            modify(p * 2, l, r, k);
        if (mid < r)
            modify(p * 2 + 1, l, r, k);
        adjust_fa(p);
    }
};

int main() {
    int n, m;
    vl a;
    cin >> n >> m;
    a.resize(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    Segment_tree stt(a);
    for (int i = 0, T = m; i < T; i++) {
        int op = 0;
        cin >> op;
        if (op == 1) {
            LL l, r, k;
            cin >> l >> r >> k;
            stt.modify(1, l, r, k);
        } else if (op == 2) {
            LL l;
            cin >> l;
            cout << stt.query(1, l, l) << "\n";
        }
    }
    return 0;
}

线段树使用条件小结

由于懒标记的加入,使得线段树能够高效的应对多种类型的区间修改以及查询。这里简单总结一下,面对不同问题时,应该如何实现线段树以及实现过程中的细节问题。

  1. 在实现线段树的时候,可以根据下面几个方面来记忆以及修改模板代码:

    • 根据查询以及修改操作,决定结构体中维护什么信息。例如单点操作就没必要加懒标记,区间操作为了保证时间复杂度建议加上。
    • adjust_fa根据左右孩子维护的信息更新当前结点维护的信息 。例如这里的是区间和线段树,于是维护信息的方式是左、右结点的 sum 相加,若是别的信息例如最大值,则更滑运算方式为求 max
    • adjust_ch当前结点的懒信息往下发一层让左右孩子接收懒信息并更新维护的信息同时自身清空当前结点懒信息。例如区间和线段树是整个区间进行加、减,但若是重置的话则是赋值。
    • _adjust_ch当前区间收到修改操作之后更新当前结点维护的信息并把懒标记进行继承
    • build:分治建树,遇到叶结点时返回否则递归处理左右孩子然后整合左右孩子的信息
    • modify遇到完全覆盖的区间直接修改 ;否则有懒标记就先分给左右孩子懒标记并进行修改然后递归处理左右区间最后整合左右孩子的信息
    • query遇到完全覆盖的区间 ,直接返回结点维护的信息 ;否则有懒信息就先分给左右孩子懒信息然后整合左右区间的查询信息
  2. 线段树的代码长度毕竟很长,只要不是机器,写这么长的代码就有概率写错,例如实现时可能会出错的部分细节问题:

    • _adjust_ch 函数只把懒信息存了下来,没有修改区间维护的信息。
    • query 以及 modify 操作,没有分配懒信息。
    • adjust_ch 之后,没有清空当前结点的懒标记。
    • mid 时用错区间,导致段错误(数组越界、野指针、栈溢出问题)。
  3. 线段树想做到单次区间修改操作的时间复杂度为 O ( log ⁡ n ) \text{O}(\log n) O(logn),那么在一段范围上执行修改操作需要能够在常数时间复杂度内得到需要维护的信息 (即 操作的时间复杂度为 O ( 1 ) \text{O}(1) O(1) ,例如区间和只需进行一个加法和乘法),否则区间操作会退化到 O ( n ) \text{O}(n) O(n) 。这个在很多时候可以决定问题是否能用线段树对数据进行维护

    • 例如对区间进行修改的方式是对每个数进行开根号,而区间查询依旧是查询区间和,则每个结点的 sum 在修改后并不方便更新,因为此时 sum 的和原本的值有关,此时只能老老实实更新结点,懒标记就失去了作用。

线段树能够维护信息的种类很多,是一个非常灵活的数据结构,需要通过大量案例来熟悉并总结线段树的使用以及实现。

其中这里的部分函数和各种资料的描述并不相同,但都是同一功能的函数的不同叫法:

  • adjust_fa:对应 pushup 。这里选择调整父结点的缩写作为函数名,使得函数更富有形象。
  • adjust_ch:对应 pushdown。这里选择调整子结点的缩写作为函数名。
  • _adjust_ch:对应 lazy。核心都是下方延迟标记,这里选择和 adjust_ch 进行配套。

线段树维护更多类型的信息

对于维护其他的信息,思考以下几个方面:

  • 结构体存储什么信息;
  • 父结点维护的信息如何通过左右孩子拿到;
  • 懒标记发给左右孩子的时候,如何更新出维护的信息以及懒标记。

P1816 忠诚 - 洛谷 RMQ问题

P1816 忠诚 - 洛谷

线段树维护区间最小值,不需要修改区间,算是 RMQ 的模板题。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct Segment_tree {
    struct Node {
        int l;
        int r;
        int mmin;
        Node(int _l = 0, int _r = 0, int _mmin = 1e5 + 1)
            : l(_l), r(_r), mmin(_mmin) {}
    };
    vector<Node> sgt; // segment tree,存储区间和信息的线段树本体

    Segment_tree(vi &arr) {
        sgt.resize(arr.size() * 4);
        // 分治建树
        _build(1, arr, 1, arr.size() - 1);
    }
    // 分治建树
    void _build(int p, vi &arr, int l, int r) {
        sgt[p] = {l, r, 0};
        if (l >= r) {
            sgt[p].mmin = arr[l];
            return;
        }
        int mid = (l + r) / 2;
        _build(p * 2, arr, l, mid);
        _build(p * 2 + 1, arr, mid + 1, r);
        adjust_fa(p); // 线段树结点调整父结点调整信息,建议额外封装
    }

    // 调整父结点的信息
    inline void adjust_fa(int p) {
        sgt[p].mmin = min(sgt[p * 2].mmin, sgt[p * 2 + 1].mmin);
    }

    // 区间查询
    int query(int p, int l, int r) {
        if (l <= sgt[p].l && sgt[p].r <= r)
            return sgt[p].mmin;
        int mmin = 1e5 + 1, mid = (sgt[p].l + sgt[p].r) / 2;
        if (l <= mid)
            mmin = min(mmin, query(p * 2, l, r));
        if (mid < r)
            mmin = min(mmin, query(p * 2 + 1, l, r));
        return mmin;
    }
};

int main() {
    int n, m;
    vi a;
    cin >> n >> m;
    a.resize(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    Segment_tree stt(a);
    for (int i = 0, T = m; i < T; i++) {
        int l, r;
        cin >> l >> r;
        cout << stt.query(1, l, r) << ' ';
    }
    return 0;
}

P3870 开关 - 洛谷

P3870 [TJOI2009\] 开关 - 洛谷](https://www.luogu.com.cn/problem/P3870) 重要性质: 1. 当一盏灯的操作次数为奇数时,灯才算执行操作;否则就是纹丝未动。 2. 区间长度 − - − 当前亮着的灯数 = = = 操作后亮着的灯数。 所以可维护一个线段树。 `Node`:区间、亮着的灯数 `num` 、操作次数的奇偶 `op` (懒标记)。 `modify`:若结点区间和待查询区间重叠,则记一次操作;否则先将自身结点的 `op` 奇偶累加到子结点上,子结点根据叠加的 `op` 奇偶决定是否操作。 `query`:查询区间的 `sum` 值。 ```cpp #include using namespace std; using vi = vector; struct Segment_tree { struct Node { int l; int r; int num; int op; // 调整次数的奇偶 Node(int l = 0, int r = 0, int num = 0, int op = 0) : l(l), r(r), num(num), op(op) {} }; vector sgt; Segment_tree(int len) { sgt.resize(4 * len); _build(1, 1, len - 1); } void _build(int p, int l, int r) { sgt[p] = {l, r, 0, 0}; if (l >= r) return; int mid = (l + r) / 2; _build(p * 2, l, mid); _build(p * 2 + 1, mid + 1, r); } void motify(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) { _adjust_ch(p, 1); // 上传1表示修改区间的决心 return; } adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) motify(p * 2, l, r); if (mid < r) motify(p * 2 + 1, l, r); adjust_fa(p); } void adjust_fa(int p) { sgt[p].num = sgt[p * 2].num + sgt[p * 2 + 1].num; } void adjust_ch(int p) { _adjust_ch(p * 2, sgt[p].op); _adjust_ch(p * 2 + 1, sgt[p].op); sgt[p].op = 0; } void _adjust_ch(int p, int cnt) { if (cnt) sgt[p].num = sgt[p].r - sgt[p].l + 1 - sgt[p].num; sgt[p].op = (sgt[p].op + cnt) % 2; } int query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].num; adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2, sum = 0; if (l <= mid) sum += query(p * 2, l, r); if (r > mid) sum += query(p * 2 + 1, l, r); return sum; } }; int main() { int n, m; cin >> n >> m; Segment_tree stt(n + 1); for (int i = 0; i < m; i++) { int op, l, r; cin >> op >> l >> r; if (!op) stt.motify(1, l, r); else cout << stt.query(1, l, r) << '\n'; } return 0; } ``` ### P2184 贪婪大陆 - 洛谷 [P2184 贪婪大陆 - 洛谷](https://www.luogu.com.cn/problem/P2184) 题目涉及多次区间内的信息修改和区间信息查询,适合使用线段树求解。 每个区间都有起点和终点,统计区间 `[l,r]` 内的地雷种数,可用 `[1,r]` 内的地雷区起点数,减去 `[1,l-1]` 的地雷区终点数,如下图所示,`[1,r]` 的起点数是 7 ,`[1,l-1]` 的终点数是 2 ,于是 `[l,r]` 内的地雷种数是 5 。 ![请添加图片描述](https://i-blog.csdnimg.cn/direct/11b3a034f4ff4bd6a81068aacb7d03cd.png) 所以线段树维护的额外信息是以当前区间内的点为起点或终点的地雷区间数。因为要修改和查询起点数和终点数 2 个信息,所以直接安排成长度为 2 的数组, `modify` 和 `query` 上传一个标志变量 `op` 进行辨别,也不需要使用懒标记。 ```cpp #include using namespace std; using vi = vector; struct Segment_tree { struct Node { int l; int r; int pnt[2]; // pnt[0]表示区间内的起点数,pnt[1]表示终点数 Node(int l = 0, int r = 0, int p1 = 0, int p2 = 0) : l(l), r(r) { pnt[0] = p1; pnt[1] = p2; } }; vector sgt; Segment_tree(int len) { sgt.resize(4 * len); _build(1, 1, len - 1); } void _build(int p, int l, int r) { sgt[p] = {l, r, 0, 0}; if (l >= r) return; int mid = (l + r) / 2; _build(p * 2, l, mid); _build(p * 2 + 1, mid + 1, r); } void motify(int p, int l, int r, int op) { if (l <= sgt[p].l && sgt[p].r <= r) { sgt[p].pnt[op] += 1; return; } int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) motify(p * 2, l, r, op); if (mid < r) motify(p * 2 + 1, l, r, op); adjust_fa(p, op); } void adjust_fa(int p, int op) { sgt[p].pnt[op] = sgt[p * 2].pnt[op] + sgt[p * 2 + 1].pnt[op]; } int query(int p, int l, int r, int op) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].pnt[op]; int mid = (sgt[p].l + sgt[p].r) / 2, sum = 0; if (l <= mid) sum += query(p * 2, l, r, op); if (r > mid) sum += query(p * 2 + 1, l, r, op); return sum; } }; int main() { // freopen("in.in", "r", stdin); int n, m; cin >> n >> m; Segment_tree stt(n + 1); for (int i = 0; i < m; i++) { int op, l, r; cin >> op >> l >> r; if (op == 1) { // 起点和终点分别进行单点记录 stt.motify(1, l, l, 0); stt.motify(1, r, r, 1); } else { //[1,r]内的起点数减去[1,l-1]内的终点数即为雷的总数 cout << stt.query(1, 1, r, 0) - stt.query(1, 1, l - 1, 1) << '\n'; } } return 0; } ``` ### P1438 无聊的数列 - 洛谷 [P1438 无聊的数列 - 洛谷](https://www.luogu.com.cn/problem/P1438) #### 直接维护 这题是修改区间且查询长度为 1 的区间的和,且区间修改的方式是加上一段等差数列。例如测试样例: \[ 1 2 3 4 5 \] + 1 3 5 \[ 1 3 6 9 5 \] \\begin{matrix}\&\[1\&2\&3\&4\&5\]\\\\+\&\&1\&3\&5\\\\\\hline \&\[1\&3\&6\&9\&5\]\\end{matrix} +\[1\[12133364595\]5\] ,所以查询 a 3 a_3 a3 输出 6 。 因此可尝试维护一个额外信息为区间和的线段树,修改区间的操作是整个区间加上一整个等差数列的和,操作的时间复杂度为 O ( 1 ) \\text{O}(1) O(1) ,可以使用。 然后就是考虑线段树的细节: `Node` :维护区间信息`[l,r]` ,维护区间和 `sum` 和懒标记:首项 `k` 、公差 `d` 。 `adjust_fa` :左右区间和相加。 `adjust_ch`:懒标记下放给左区间和右区间。虽然懒标记是 `{k,d}` ,但实际下放的其实是一个等差数列,下放给左区间和右区间的等差数列并不相同。 * 下放给左区间的懒标记:正常下放。 * 下放给右区间的懒标记:`{k+(mid+1-sgt[p].l),d}` 。 例如上一个数列划分成 2 个区间: \[ 1 2 3 ∣ 4 5 \] \\begin{matrix}\[1\&2\&3\&\|\&4\&5\]\\end{matrix} \[123∣45\] ,则下放的右区间是 `{4,5}` 。用懒标记描述的话就是 m i d = 5 + 1 2 = 3 mid=\\frac{5+1}{2}=3 mid=25+1=3 ,则 4 = 1 + ( m i d + 1 − l ) × d = 1 + ( 3 + 1 − 1 ) × 1 4=1+(mid+1-l)\\times d=1+(3+1-1)\\times 1 4=1+(mid+1−l)×d=1+(3+1−1)×1 。 `_adjust_ch` :区间和加上等差数列的和,可通过前 n n n 项和公式求解,等差数列的数量为区间长度。同时懒标记可叠加为新的等差数列。 `modify`:当区间重叠时,执行 `_adjust_ch` 操作即可。但要注意重叠的 `p` 区间的左端和 待修改区间 `[l,r]` 的左端不重叠。 `query`:可直接照搬原来的模板。 [P1438 无聊的数列 - 洛谷](https://www.luogu.com.cn/problem/P1438) 参考程序: ```cpp #include using namespace std; using LL = long long; using vi = vector; struct Segment_tree { struct Node { int l, r; LL sum, k, d; }; vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); _build(1, a, 1, a.size() - 1); } // 分治建树 void _build(int p, vi &a, int l, int r) { sgt[p] = {l, r, 0, 0, 0}; if (l >= r) { sgt[p].sum = a[l]; return; } int mid = (l + r) / 2; _build(p * 2, a, l, mid); _build(p * 2 + 1, a, mid + 1, r); adjust_fa(p); } // 更新父结点 void adjust_fa(int p) { sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum; } // 懒标记(k,d)下放 void adjust_ch(int p) { int mid = (sgt[p].l + sgt[p].r) / 2; // 左区间正常 _adjust_ch(p * 2, sgt[p].k, sgt[p].d); // 右区间需要取等差数列后半段 _adjust_ch(p * 2 + 1, sgt[p].k + (mid + 1 - sgt[p].l) * sgt[p].d, sgt[p].d); sgt[p].k = sgt[p].d = 0; } void _adjust_ch(int p, LL k, LL d) { // 更新方式为原sum加上等差数列的和 sgt[p].sum += (k + k + (sgt[p].r - sgt[p].l) * d) * (sgt[p].r - sgt[p].l + 1) / 2; // 懒标记叠加为新的等差数列 sgt[p].k += k; sgt[p].d += d; } void modify(int p, int l, int r, LL k, LL d) { if (l <= sgt[p].l && sgt[p].r <= r) { // p区间左端点可能和l右差距 _adjust_ch(p, k + (sgt[p].l - l) * d, d); return; } adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) modify(p * 2, l, r, k, d); if (r > mid) modify(p * 2 + 1, l, r, k, d); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].sum; adjust_ch(p); LL sum = 0; int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) sum += query(p * 2, l, r); if (l > mid) sum += query(p * 2 + 1, l, r); return sum; } }; int main() { // freopen("in.in", "r", stdin); int n, m; vi a; cin >> n >> m; a.resize(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); for (int T = m; T--;) { int op; cin >> op; if (op == 1) { int l, r; LL k, d; cin >> l >> r >> k >> d; stt.modify(1, l, r, k, d); } else { int x; cin >> x; cout << stt.query(1, x, x) << '\n'; } } return 0; } ``` #### 借助差分 [P1438 无聊的数列 - 洛谷](https://www.luogu.com.cn/problem/P1438) 例如对数列 \[ a 1 , a 2 , a 3 , a 4 , a 5 \] \[a_1,a_2,a_3,a_4,a_5\] \[a1,a2,a3,a4,a5\] ,给 \[ 1 , 4 \] \[1,4\] \[1,4\] 的内容加上一个长度为 4 的等差数列: \[ a 1 + k , a 2 + k + d , a 3 + k + 2 d , a 4 + k + 3 d , a 5 \] \[a_1+k,a_2+k+d,a_3+k+2d,a_4+k+3d,a_5\] \[a1+k,a2+k+d,a3+k+2d,a4+k+3d,a5\] ,然后再求差分数组: \[ d 1 + k , d 2 + d , d 3 + d , d 4 + d , d 5 − ( k + 3 d ) \] \[d_1+k,d_2+d,d_3+d,d_4+d,d_5-(k+3d)\] \[d1+k,d2+d,d3+d,d4+d,d5−(k+3d)\] ,其中 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=ai−ai−1 。 因此在差分数列的区间 `[l,r]` 上加上一个等差数列的操作: 1. `d[l]+k` 。 2. 对 `[l+1,r]` 区间的差分数组统一加上 `d` 。 3. 对 `d[r+1]` 减去 `k+(r-l)*d` 。 若使用区间和线段树维护这个差分数组,则这个操作都是加上或减去一个数,时间复杂度为 O ( 1 ) \\text{O}(1) O(1) ,而查询指定的 a p a_p ap 只需查询 \[ 1 , p \] \[1,p\] \[1,p\] 的差分数组的区间和即可。因此这个题就可以用最开始的区间和线段树模板解决。 参考程序: ```cpp #include using namespace std; using LL = long long; using vi = vector; struct Segment_tree { struct Node { int l, r; LL sum, lz; Node(int _l = 0, int _r = 0, LL _sum = 0, LL _lz = 0) : l(_l), r(_r), sum(_sum), lz(_lz) {} }; vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); _build(1, a, 1, a.size() - 1); } void _build(int p, vi &a, int l, int r) { sgt[p] = {l, r, 0, 0}; if (l >= r) { sgt[p].sum = a[l]; return; } int mid = (l + r) / 2; if (l <= mid) _build(p * 2, a, l, mid); if (r > mid) _build(p * 2 + 1, a, mid + 1, r); adjust_fa(p); } void adjust_fa(int p) { sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum; } void adjust_ch(int p) { _adjust_ch(p * 2, sgt[p].lz); _adjust_ch(p * 2 + 1, sgt[p].lz); sgt[p].lz = 0; } void _adjust_ch(int p, LL lz) { sgt[p].sum += (sgt[p].r - sgt[p].l + 1) * lz; sgt[p].lz += lz; } void modify(int p, int l, int r, LL k) { // l> n >> m; a.resize(n + 2, 0); for (int i = 1, x; i <= n; i++) { cin >> x; a[i] += x; // 转化为差分数组 a[i + 1] -= x; } Segment_tree stt(a); for (int T = m; T--;) { int op; cin >> op; if (op == 1) { int l, r; LL k, d; cin >> l >> r >> k >> d; stt.modify(1, l, l, k); stt.modify(1, l + 1, r, d); // l+1可能大于r if (r + 1 <= n + 1) // 询问可能出现修改区间边界的情况 stt.modify(1, r + 1, r + 1, -1 * (k + (r - l) * d)); } else { int x; cin >> x; cout << stt.query(1, 1, x) << endl; } } return 0; } ``` ## 多种区间操作 有时在区间上执行多种修改操作,此时需要**确定每种操作的优先级** ,进而维护每种信息。确定的方式为:考虑 `adjust_ch` 函数时,当新的任务到来时,如何**确定多种操作的先后顺序**。 ### P3373 【模板】线段树 2 - 洛谷 [P3373 【模板】线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3373) 这个线段树有加法和乘法 2 种区间操作,这两种操作在线段树中的时间复杂度都是 O ( 1 ) \\text{O}(1) O(1) ,因此可以使用线段树。 但若每个区间都维护 2 个懒标记,假设同一个区间同时被标记多次加法和乘法,当查询到这个区间时,加法、乘法谁先执行就是个问题,其中一种情况是乘法的懒标记先被记录,然后是加法,再然后懒标记下放时,在子结点时先算加法再算乘法就可能出错。 #### 数学推理统一操作 这时可以考虑 2 种懒标记共存,同时设计一种计算方式,让所有懒标记都能顺利更新。设计时要考虑 A 2 2 A_2\^2 A22 种先后顺序。 假设原始的区间和是 s 1 s_1 s1 ,假设第 1 次加法操作要求区间内的每个数加上 a d 1 ad_1 ad1 ,第 1 次乘法操作要求区间内的每个数乘 m u 1 mu_1 mu1 ,第 2 次加法操作要求区间内的每个数加上 a d 2 ad_2 ad2 ,第 2 次乘法操作是 m u 2 mu_2 mu2 ,同理还有 ( a d 3 , m u 3 ) (ad_3,mu_3) (ad3,mu3) 、 ( a d 4 、 m u 4 ) (ad_4、mu_4) (ad4、mu4) 等等。 若无论什么情况都是先算加法,再算乘法: ( ( s 1 + a d 1 × l e n ) × m u 1 + a d 2 × l e n ) × m u 2 = s 1 × m u 1 × m u 2 + a d 1 × m u 1 × m u 2 × l e n + l e n × a d 2 × m u 2 = ( s 1 + ( a d 1 + a d 2 m u 1 ) l e n ) × m u 1 m u 2 \\begin{aligned}\&((s_1+ad_1\\times len)\\times mu_1+ad_2\\times len)\\times mu_2 \\\\=\&s_1\\times mu_1\\times mu_2+ad_1\\times mu_1\\times mu_2\\times len+\\\\\&len\\times ad_2\\times mu_2 \\\\=\&(s_1+(ad_1+\\frac{ad_2}{mu_1})len)\\times mu_1mu_2\\end{aligned} ==((s1+ad1×len)×mu1+ad2×len)×mu2s1×mu1×mu2+ad1×mu1×mu2×len+len×ad2×mu2(s1+(ad1+mu1ad2)len)×mu1mu2 这个等式描述了无论来几套加法、乘法操作 ( a d n , m u n ) (ad_n,mu_n) (adn,mun) ,都按照 a d = a d 1 + a d 2 m u 1 + a d 3 m u 2 + ... ad=ad_1+\\frac{ad_2}{mu_1}+\\frac{ad_3}{mu_2}+\\dots ad=ad1+mu1ad2+mu2ad3+... , m u = ∏ i = 1 n m i mu=\\prod\\limits_{i=1}\^{n}m_i mu=i=1∏nmi 进行懒标记存储。 其中的除法操作 a d i m u i − 1 \\frac{ad_i}{mu_{i-1}} mui−1adi 在这个全是整数的环境下会自动进行取整,丢失精度,使用浮点数也需要考虑取整的情况。也许这个思路可行,但这里不使用。 若无论什么情况都是先算乘法,再算加法: ( s 1 × m u 1 + a d 1 × l e n ) × m u 2 + a d 2 × l e n = s 1 × m u 1 × m u 2 + ( a d 1 × m u 2 + a d 2 ) × l e n \\begin{aligned}\&(s_1\\times mu_1+ad_1\\times len)\\times mu_2+ad_2\\times len \\\\=\&s_1\\times mu_1\\times mu_2+(ad_1\\times mu_2+ ad_2)\\times len\\end{aligned} =(s1×mu1+ad1×len)×mu2+ad2×lens1×mu1×mu2+(ad1×mu2+ad2)×len 此时按照 m u n = ∏ i = 1 n m u i mu_n=\\prod\\limits_{i=1}\^{n}mu_i mun=i=1∏nmui 和 a d n = a d n − 1 × m u n + a d n ad_n=ad_{n-1}\\times mu_n+ad_n adn=adn−1×mun+adn 的方式存储懒标记,则可完成统一操作。 [P3373 【模板】线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3373) 参考: ```cpp #include using namespace std; using LL = long long; using vi = vector; using vl = vector; using vvl = vector>; LL mod = 1; struct Segment_tree { struct Node { int l, r; LL sum; LL ad, mu; // 懒标记 Node(int _l = 0, int _r = 0, int _sum = 0, int _ad = 0, int _mu = 0) { l = _l, r = _r, sum = _sum; ad = _ad, mu = _mu; } }; vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); _build(1, a, 1, a.size() - 1); } void _build(int p, vi &a, int l, int r) { sgt[p] = {l, r, 0, 0, 1}; // mu懒标记需要初始化为1 if (l >= r) { sgt[p].sum = a[l]; return; } int mid = (l + r) / 2; if (l <= mid) _build(p * 2, a, l, mid); if (r > mid) _build(p * 2 + 1, a, mid + 1, r); adjust_fa(p); } void adjust_fa(int p) { sgt[p].sum = (sgt[p * 2].sum + sgt[p * 2 + 1].sum) % mod; } void adjust_ch(int p) { _adjust_ch(p * 2, sgt[p].ad, sgt[p].mu); _adjust_ch(p * 2 + 1, sgt[p].ad, sgt[p].mu); sgt[p].ad = 0, sgt[p].mu = 1; } void _adjust_ch(int p, LL ad, LL mu) { sgt[p].sum = (sgt[p].sum * mu + ad * (sgt[p].r - sgt[p].l + 1)) % mod; sgt[p].ad = (sgt[p].ad * mu + ad) % mod; sgt[p].mu = (sgt[p].mu * mu) % mod; } void modify(int p, int l, int r, int ad, int mu) { if (l <= sgt[p].l && sgt[p].r <= r) { _adjust_ch(p, ad, mu); return; } adjust_ch(p); int mid = (sgt[p].r + sgt[p].l) / 2; if (l <= mid) modify(p * 2, l, r, ad, mu); if (mid < r) modify(p * 2 + 1, l, r, ad, mu); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].sum; adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; LL sum = 0; if (l <= mid) sum = (sum + query(p * 2, l, r)) % mod; if (mid < r) sum = (sum + query(p * 2 + 1, l, r)) % mod; return sum; }; }; int main() { // freopen("in.in", "r", stdin); int n, q; cin >> n >> q >> mod; vi a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); for (int T = q; T--;) { int op, l, r, k; cin >> op >> l >> r; if (op == 3) cout << stt.query(1, l, r) << '\n'; else { cin >> k; if (op == 1) // 乘法操作 stt.modify(1, l, r, 0, k); else stt.modify(1, l, r, k, 1); } } return 0; } ``` #### 题目特供的矩阵乘法统一操作 在上文的线段树中,每个区间都有 2 个信息,区间和和区间长。因此可构建矩阵 m x = \[ s u m l e n 0 0 \] mx=\\begin{bmatrix}sum\&len\\\\0\&0\\end{bmatrix} mx=\[sum0len0\] 。 * 当出现乘 k k k 的操作时, 构建矩阵 A = \[ k 0 0 1 \] A=\\begin{bmatrix}k\&0\\\\0\&1\\end{bmatrix} A=\[k001\] , m x × A mx\\times A mx×A 即可更新 s u m sum sum 。 * 当出现加 k k k 的操作时,构建矩阵 A = \[ 1 0 k 1 \] A=\\begin{bmatrix}1\&0\\\\k\&1\\end{bmatrix} A=\[1k01\] , m x × A mx\\times A mx×A 即可更新 s u m sum sum 。 此时**乘法和加法都可以统一成矩阵乘法** ,于是线段树的结点除了区间信息,还可维护一个 m x mx mx 矩阵和一个 l z lz lz 矩阵, m x mx mx 矩阵同上, l z lz lz 矩阵作为线段树的懒标记。开始时 l z lz lz 为单位矩阵,之后的每一次操作都可乘进 l z lz lz 中不用考虑顺序影响结果,懒标记下放时也可将 l z lz lz 乘进子结点的懒标记。 但因为引入了矩阵乘法,虽然每次乘法的运行次数只有 8 + 4 ,但因为这个题的询问次数过多,这些 O ( 1 ) \\text{O}(1) O(1) 的操作居然也会影响程序耗时,所以线段树的每个区间需要额外维护一个 `flag` ,当 l z lz lz 不为单位矩阵时, `flag=1` ,此时可以下放懒标记,否则不能下放。 用矩阵乘法的话,就不能用之前的 `vector` 按需申请的模式,否则还是会超时。 > 之前的线段树,每次查询都会下放懒标记,无论懒标记是否存在都是如此,这个模式也可优化。具体取决于场景。 [P3373 【模板】线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3373) 参考: ```cpp #include using namespace std; using LL = long long; const int N = 1e5 + 10; LL mod = 1; void mxmul(LL a[][2], LL b[][2]) { LL c[2][2] = {0}; for (int i = 0; i < 2; i++) for (int j = 0; j < 2; j++) for (int k = 0; k < 2; k++) c[i][j] = (c[i][j] + a[i][k] * b[k][j]) % mod; for (int i = 0; i < 2; i++) for (int j = 0; j < 2; j++) a[i][j] = c[i][j]; } struct Segment_tree { int l, r; LL ans[2][2]; // {{区间和,区间长},{0}} LL lz[2][2]; // 懒标记 bool flag; Segment_tree(int _l = 0, int _r = 0, LL _sum = 0) { l = _l, r = _r, ans[0][0] = _sum; ans[0][1] = r - l + 1; flag = 0; unitlz(); } void unitlz() { lz[0][0] = lz[1][1] = 1; lz[1][0] = lz[0][1] = 0; } } sgt[N << 2]; int a[N]; void adjust_fa(int p) { sgt[p].ans[0][0] = (sgt[p * 2].ans[0][0] + sgt[p * 2 + 1].ans[0][0]) % mod; } void build(int p, int l, int r) { sgt[p] = {l, r, 0}; if (l >= r) { sgt[p].ans[0][0] = a[l]; return; } int mid = (l + r) / 2; if (l <= mid) build(p * 2, l, mid); if (mid < r) build(p * 2 + 1, mid + 1, r); adjust_fa(p); } void _adjust_ch(int p, LL lz[][2]) { mxmul(sgt[p].ans, lz); mxmul(sgt[p].lz, lz); sgt[p].flag = 1; } void adjust_ch(int p) { _adjust_ch(p * 2, sgt[p].lz); _adjust_ch(p * 2 + 1, sgt[p].lz); sgt[p].unitlz(); sgt[p].flag = 0; } void modify(int p, int l, int r, int k, int op) { if (l <= sgt[p].l && sgt[p].r <= r) { LL t[2][2] = {0}; if (op == 1) { t[0][0] = k; t[1][1] = 1; } else { t[1][0] = k; t[0][0] = t[1][1] = 1; } _adjust_ch(p, t); return; } if (sgt[p].flag) adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) modify(p * 2, l, r, k, op); if (mid < r) modify(p * 2 + 1, l, r, k, op); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].ans[0][0]; if (sgt[p].flag) adjust_ch(p); LL sum = 0, mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) sum = (sum + query(p * 2, l, r)) % mod; if (mid < r) sum = (sum + query(p * 2 + 1, l, r)) % mod; return sum; } int n, q; template void read(T &x) { // 快速读取数字,使用不优化的cin也能过 x = 0; char c = getchar(); T flag = 1; while (!(c >= '0' && c <= '9')) { if (c == '-') flag = -1; c = getchar(); } while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); } x *= flag; } int main() { // freopen("in.in", "r", stdin); read(n); read(q); read(mod); for (int i = 1; i <= n; i++) read(a[i]); build(1, 1, n); while (q--) { int op, l, r; read(op); read(l); read(r); if (op == 3) printf("%lld\n", query(1, l, r)); else { int k; cin >> k; modify(1, l, r, k, op); } } return 0; } ``` ### P1253 扶苏的问题 - 洛谷 [P1253 扶苏的问题 - 洛谷](https://www.luogu.com.cn/problem/P1253) #### 数学推理统一操作 将操作试着这样解读: 1. 区间内的值全部改成 x x x 相当于先乘 0 ,再加 x x x 。 2. 区间内的值全部改成 x x x 相当于直接加 x x x ,或先乘 1 再加 x x x 。 如此一旦出现全部改成 x x x 的操作,则曾经的所有操作全部作废,所以这里将是否重置操作解释成先乘 0 还是先乘 1,然后再加上 x x x ,这样就能将 2 个操作就能转换成加法或乘法,懒标记就能进行统一。 在实现上可以将[P3373 【模板】线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3373) 的代码的合并区间和的逻辑改成求子区间的最大值,去掉所有取模,以及 `query` 函数查询时,初始值赋值为无穷小即可。 但因数据量太大,需要使用 `scanf` 或对 `cin` 进行优化。 ```cpp #include using namespace std; using LL = long long; using vi = vector; using vl = vector; using vvl = vector>; struct Segment_tree { struct Node { int l, r; LL mmax; LL ad, mu; // 懒标记 Node(int _l = 0, int _r = 0, int _mmax = 0, int _ad = 0, int _mu = 0) { l = _l, r = _r, mmax = _mmax; ad = _ad, mu = _mu; } }; vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); _build(1, a, 1, a.size() - 1); } void _build(int p, vi &a, int l, int r) { sgt[p] = {l, r, 0, 0, 1}; if (l >= r) { sgt[p].mmax = a[l]; return; } int mid = (l + r) / 2; if (l <= mid) _build(p * 2, a, l, mid); if (r > mid) _build(p * 2 + 1, a, mid + 1, r); adjust_fa(p); } void adjust_fa(int p) { sgt[p].mmax = max(sgt[p * 2].mmax, sgt[p * 2 + 1].mmax); } void adjust_ch(int p) { _adjust_ch(p * 2, sgt[p].ad, sgt[p].mu); _adjust_ch(p * 2 + 1, sgt[p].ad, sgt[p].mu); sgt[p].ad = 0, sgt[p].mu = 1;//默认都是乘1加0表示不变 } void _adjust_ch(int p, LL ad, LL mu) { sgt[p].mmax = sgt[p].mmax * mu + ad; sgt[p].ad = sgt[p].ad * mu + ad; sgt[p].mu = sgt[p].mu * mu; } void modify(int p, int l, int r, int ad, int mu) { if (l <= sgt[p].l && sgt[p].r <= r) { _adjust_ch(p, ad, mu); return; } adjust_ch(p); int mid = (sgt[p].r + sgt[p].l) / 2; if (l <= mid) modify(p * 2, l, r, ad, mu); if (mid < r) modify(p * 2 + 1, l, r, ad, mu); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].mmax; adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; LL mmax = -1e18; // 无穷小不可设置为0 if (l <= mid) mmax = max(mmax, query(p * 2, l, r)); if (mid < r) mmax = max(mmax, query(p * 2 + 1, l, r)); return mmax; }; }; int main() { // freopen("in.in", "r", stdin); ios::sync_with_stdio(0); cin.tie(0);// 没有优化就超时 cout.tie(0); int n, q; cin >> n >> q; vi a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); for (int T = q; T--;) { int op, l, r, k; cin >> op >> l >> r; if (op == 3) cout << stt.query(1, l, r) << '\n'; else { cin >> k; if (op == 1) // 区间内的和全部修改为x,可理解成先乘0再加x stt.modify(1, l, r, k, 0); else stt.modify(1, l, r, k, 1); } } return 0; } ``` #### 懒标记下放遵循先后顺序 这一题只是一个意外,其实并不是所有的操作都能根据数学规律整合在一起,若出现类似的多种区间操作,则懒标记下放就要严格遵守优先级次序。 例如这里一旦出现全部改成 x x x 的操作,则曾经的所有操作全部作废,所以修改的操作毋庸置疑是最高的。于是当出现修改操作时,优先进行重置操作,然后再将懒标记 `ad` 加到子区间的懒标记中。此时 `Node` 还需要维护一个 `bool` 变量作为懒标记,判断是否需要重置。 之后 `_adjust_ch` 根据传递来的 `bool` 变量判断是否需要重置,若需要则优先重置,再将懒标记 `ad` 进行叠加。 除了这些细节,其他的组件大体上都不变。 ```cpp #include using namespace std; typedef long long LL; const int N = 1e6 + 10; int n, q; int a[N]; struct Node { int l, r; LL mmax, ad, mu; // 懒标记 bool vis; // 确认是否需要重置处理 } sgt[N << 2]; // N*4 void adjust_fa(int p) { sgt[p].mmax = max(sgt[p * 2].mmax, sgt[p * 2 + 1].mmax); } void _adjust_ch(int p, bool vis, LL mu, LL ad) { // 先处理重置,再处理加法 if (vis) { sgt[p].mmax = mu; sgt[p].ad = 0; sgt[p].mu = mu; sgt[p].vis = vis; } sgt[p].mmax += ad; sgt[p].ad += ad; } void adjust_ch(int p) { _adjust_ch(p * 2, sgt[p].vis, sgt[p].mu, sgt[p].ad); _adjust_ch(p * 2 + 1, sgt[p].vis, sgt[p].mu, sgt[p].ad); sgt[p].vis = sgt[p].mu = sgt[p].ad = 0; } void build(int p, int l, int r) { sgt[p] = {l, r, a[l], 0, 0, 0}; if (l == r) return; int mid = (l + r) >> 1; build(p * 2, l, mid); build(p * 2 + 1, mid + 1, r); adjust_fa(p); } void modify(int p, int l, int r, bool vis, LL mu, LL ad) { if (l <= sgt[p].l && sgt[p].r <= r) { _adjust_ch(p, vis, mu, ad); return; } adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) >> 1; if (l <= mid) modify(p * 2, l, r, vis, mu, ad); if (mid < r) modify(p * 2 + 1, l, r, vis, mu, ad); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].mmax; adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; LL mmax = -1e18; // 无穷小不可设置为0 if (l <= mid) mmax = max(mmax, query(p * 2, l, r)); if (mid < r) mmax = max(mmax, query(p * 2 + 1, l, r)); return mmax; }; int main() { scanf("%d%d", &n, &q); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); build(1, 1, n); while (q--) { int op, l, r, x; scanf("%d%d%d", &op, &l, &r); if (op == 1) { scanf("%d", &x); modify(1, l, r, 1, x, 0); } else if (op == 2) { scanf("%d", &x); modify(1, l, r, 0, 0, x); } else printf("%lld\n", query(1, l, r)); } return 0; } ``` ## 线段树与其他知识结合 从这里开始,题目多为曾经的问题求解的拓展,或几种知识点的综合考察,有部分为中档压轴题候选,入坑需谨慎。 ### 线段树 + 分治 线段树本身就是基于分治思想的二叉树。那么对于**很多可以通过分治解决的问题** ,**涉及查询时** ,**也可以通过线段树来维护** 。其中,最经典的就是[P1115 最大子段和 - 洛谷](https://www.luogu.com.cn/problem/P1115) 问题,但这个题的数据量整体偏小,且只需要求出整个区间的最大子段和。 线段树的区间查询是由多个小区间中的信息拼凑而成。面对上述问题,在某些情况下,查询过程中单点返回一个值是不足以拼凑出结果的,需要返回的是一个集合,这个集合可用一个结构体进行描述。 #### P4513 小白逛公园 - 洛谷 [P4513 小白逛公园 - 洛谷](https://www.luogu.com.cn/problem/P4513) [P1115 最大子段和 - 洛谷](https://www.luogu.com.cn/problem/P1115)的拓展题。 根据 [分治思想的应用-CSDN博客](https://blog.csdn.net/m0_73693552/article/details/158349896?spm=1001.2014.3001.5501) ,最大子段和的求解可拆解成 2 子问题:先求左边的最大子段和,再求右边的最大子段和。然后再从中间向两边扩展求横跨 2 个区间的最大子段和, 3 个子段和取最大值,其中每个区间都可以再求子问题。 ![请添加图片描述](https://i-blog.csdnimg.cn/direct/e151042154a84a35937a626a66aeb415.png) 然后题目给出 2 种操作:修改某个长度为 1 的区间的值,和求最大子段和。既涉及修改区间又涉及查询区间,且最大子段和也可通过分治解决,于是可用线段树求解。且单点查询不涉及懒标记。 然后就是线段树的组件分析: `Node` :线段树的结点除了区间 `[l,r]` 外,还有要求的当前区间的最大子段和 `maxx` 。此外根据 P1115 最大子段和的求解思路,每个区间都要提供一个从自身和左、右断点接壤的最大子段和 `lmax` 和 `rmax` ,甚至父结点的 `lmax` 和 `rmax` 可能超过自身维护区间的一半,所以还要维护一个自身的区间和,总共 6 个变量。 ![请添加图片描述](https://i-blog.csdnimg.cn/direct/ef311f9fa6b84f71abb184ed341f2805.png) `adjust_fa` :根据子结点更新父结点的信息同样要更新 4 个:`maxx` 、`lmax`、`rmax`、`sum`。每个变量的更新方式如下: ```cpp void Segment_tree::adjust_fa(Node &aim, Node &lc, Node &rc) { aim.maxx = max(max(lc.maxx, rc.maxx), lc.rmax + rc.lmax); aim.lmax = max(lc.lmax, lc.sum + rc.lmax); aim.rmax = max(rc.rmax, rc.sum + lc.rmax); aim.sum = lc.sum + rc.sum; } ``` `modify`:长度为 1 的区间修改。注意遍历到长度为 1 的区间时,若不符合要求也不应继续查询,而是终止。 `query`:查询指定区间的子段和。因为 4 个成员变量的更新方式复杂,只输出一个 `maxx` 不足以传递合适的信息,需要返回 `Node` 结构体表达完整的信息。若待查询的区间只在左半区和右半区,则直接去对应半区找;否则就是横跨 2 个半区,则定义 `Node` 对象接收左、右半区的信息进行整合,然后再返回。 [P4513 小白逛公园 - 洛谷](https://www.luogu.com.cn/problem/P4513) 参考: ```cpp #include using namespace std; struct Node { int l, r, maxx; int lmax, rmax, sum; }; using vi = vector; struct Segment_tree { vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); build(1, a, 1, a.size() - 1); } void build(int p, vi &a, int l, int r) { sgt[p] = {l, r, a[l], a[l], a[l], a[l]}; if (l >= r) return; int mid = (l + r) / 2; build(p * 2, a, l, mid); build(p * 2 + 1, a, mid + 1, r); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } void adjust_fa(Node &aim, Node &lc, Node &rc) { aim.maxx = max(max(lc.maxx, rc.maxx), lc.rmax + rc.lmax); aim.lmax = max(lc.lmax, lc.sum + rc.lmax); aim.rmax = max(rc.rmax, rc.sum + lc.rmax); aim.sum = lc.sum + rc.sum; } void modify(int p, int x, int v) { if (sgt[p].l == x && sgt[p].r == x) { sgt[p] = {sgt[p].l, sgt[p].r, v, v, v, v}; return; } else if (sgt[p].l == sgt[p].r) return; int mid = (sgt[p].l + sgt[p].r) / 2; if (x <= mid) modify(p * 2, x, v); else modify(p * 2 + 1, x, v); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } Node query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p]; int mid = (sgt[p].l + sgt[p].r) / 2; if (r <= mid) return query(p * 2, l, r); if (l > mid) return query(p * 2 + 1, l, r); Node L, R, ret; L = query(p * 2, l, r); R = query(p * 2 + 1, l, r); adjust_fa(ret, L, R); return ret; } }; int main() { // freopen("in.in", "r", stdin); ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int n, T; cin >> n >> T; vi a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); while (T--) { int op, x, y; cin >> op >> x >> y; if (op == 1) cout << stt.query(1, min(x, y), max(x, y)).maxx << '\n'; else stt.modify(1, x, y); } return 0; } ``` #### P2572 \[SCOI2010\] 序列操作 - 洛谷 \[P2572 [SCOI2010\] 序列操作 - 洛谷](https://www.luogu.com.cn/problem/P2572) > 这一个题**包含了之前的几乎所有的线段树的操作**,是这一篇章里综合考验最高的题。若能做出来,或没有犯线段树操作上相关的失误,则说明线段树的各种知识点和绝大部分技巧已基本掌握。 所有修改操作分 2 类:修改和取反。其中**修改拥有最高优先级** ,所以只要出现修改,则曾经的所有修改操作全部都要被重置。然后是取反,**取反偶数次则相当于啥也不做**。 所有查询操作中求区间内有多少个 1 ,在 01 序列可用区间和描述。查询有多少个连续的 0 和 1 ,则和最大子段和一样,答案来自左区间、右区间和横跨 2 个区间的连续个 1 ,可通过分治求解。 若整个序列可通过线段树维护,子结点合并和父结点的修改和取反 2 个懒标记都可在 O ( 1 ) \\text{O}(1) O(1) 时间内完成,因此可以使用。确定使用线段树之后就是分析各个组件的构成: `Node`:除了基本的区间信息 `[l,r]` ,还有 `{s1,mx1,l1,r1}` 分别表示区间内 1 的总数、最长的连续个 1 的长度、从区间左端点开始的最长的连续个 1 的长度、从区间右端点开始的最长的连续个 1 的长度,以及 2 个懒标记 `{change,xr}` 表示修改为什么和是否取反,`change=-1` 表示不修改,`xr` 为 1 表示确定取反。 但取反后仅靠现有的信息将无法更新和 1 有关的区间信息,因为取反后, 1 的信息变成了 0 的, 0 的信息变成了 1 的,所以还需要额外维护 `{s0,mx0,l0,r0}` 的信息,当出现取反操作时,可通过交换间接实现整个区间的取反。因此 `Node` 需要维护 12 个变量,调试及其困难。 `adjust_fa`:整合子结点的信息到父结点。这里暂记 `p` 为父结点,`lc` 为左子树,`rc` 为右子树。则 `p.s1=lc.s1+rc.s1` 就是左、右子树的 1 的数量只和,`{mx1,l1,r1}` 的更新和最大子段和基本相同,若硬要区分差别,则 `p.l1` 的更新可观察 `lc.s0` 是否为 0 ,是的话 `p.l1=lc.s1+rc.l1;` 。同理 0 相关的信息只需在 1 的信息的基础上,把 1 改成 0 ,把 0 改成 1 即可。和[P4513 小白逛公园 - 洛谷](https://www.luogu.com.cn/problem/P4513)一样,可将参数设置成 3 个结构体,再进行合并,后续的分治操作也能用的到。 `_adjust_ch`:之前提到,修改的优先级最高,所以若 2 个懒标记同时存在,则一定是取反是后来新加的操作。因此无论什么情况,都要根据当前操作先修改成 0 和 1 ,并重置所有懒标记,然后再取反,注意取反时不能变动修改成 0 和 1 后的懒标记,且若判断出已经出现偶数次取反,则取反的懒标记被清除。 `adjust_ch`:这里要修改的信息整合成结构体统一上传。也可上传操作序号,然后严格按照 先修改01后取反的操作进行,此时要分别处理 2 个懒标记。 剩下的 `query` 和 `modify` 基本差不多,都是整合结构体变量的信息。 ```cpp #include using namespace std; using vi = vector; struct Segment_tree { struct Node { int l, r; int s0, s1, mx0, mx1; int l1, r1, l0, r0, change, xr; }; vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); build(1, a, 1, a.size() - 1); } void build(int p, vi &a, int l, int r) { sgt[p] = {l, r, 1 - a[l], a[l], 1 - a[l], a[l], a[l], a[l], 1 - a[l], 1 - a[l], -1, 0}; if (l >= r) return; int mid = (l + r) / 2; build(p * 2, a, l, mid); build(p * 2 + 1, a, mid + 1, r); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } void adjust_fa(Node &x, Node &lc, Node &rc) { x.s1 = lc.s1 + rc.s1; x.l1 = lc.s0 == 0 ? lc.s1 + rc.l1 : lc.l1; x.r1 = rc.s0 == 0 ? rc.s1 + lc.r1 : rc.r1; x.mx1 = max(max(lc.mx1, rc.mx1), lc.r1 + rc.l1); x.s0 = lc.s0 + rc.s0; x.l0 = lc.s1 == 0 ? lc.s0 + rc.l0 : lc.l0; x.r0 = rc.s1 == 0 ? rc.s0 + lc.r0 : rc.r0; x.mx0 = max(max(lc.mx0, rc.mx0), lc.r0 + rc.l0); } void adjust_ch(int p) { _adjust_ch(p * 2, p); _adjust_ch(p * 2 + 1, p); sgt[p].change = -1, sgt[p].xr = 0; } void _adjust_ch(int x, int y) { auto &p = sgt[x], &fa = sgt[y]; if (fa.change != -1) { // 只改01 p.s1 = fa.change == 1 ? (p.r - p.l + 1) : 0; p.mx1 = p.l1 = p.r1 = p.s1; p.s0 = (p.r - p.l + 1) - p.s1; p.mx0 = p.l0 = p.r0 = p.s0; p.change = fa.change; p.xr = 0; } if (fa.xr == 1) { // 只取反 swap(p.s0, p.s1); swap(p.mx0, p.mx1); swap(p.l0, p.l1); swap(p.r0, p.r1); // 注意这里不能覆盖只改01的懒标记否则会出错 p.xr = p.xr == 1 ? 0 : 1; // 偶数次取反等于啥也不做 } } void modify(int p, int l, int r, int change, int xr) { if (l <= sgt[p].l && sgt[p].r <= r) { sgt[0].change = change; // 将修改操作打包后统一上传 sgt[0].xr = xr; _adjust_ch(p, 0); return; } else if (sgt[p].l == sgt[p].r) return; adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) modify(p * 2, l, r, change, xr); if (mid < r) modify(p * 2 + 1, l, r, change, xr); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } Node query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p]; adjust_ch(p); int mid = (sgt[p].l + sgt[p].r) / 2; if (r <= mid) return query(p * 2, l, r); if (l > mid) return query(p * 2 + 1, l, r); Node ret, L, R; L = query(p * 2, l, r); R = query(p * 2 + 1, l, r); adjust_fa(ret, L, R); return ret; } void bfs() { // 打印线段树 queue q; q.push(1); int r = 0, nr = 1; while (q.size()) { int front = q.front(); q.pop(); cout << "["; cout << sgt[front].l << ","; cout << sgt[front].r << ","; cout << sgt[front].s1 << ","; cout << sgt[front].mx1 << ","; cout << sgt[front].l1 << ","; cout << sgt[front].r1 << ","; cout << sgt[front].s0 << ","; cout << sgt[front].mx0 << ","; cout << sgt[front].l0 << ","; cout << sgt[front].r0 << ","; cout << sgt[front].change << ","; cout << sgt[front].xr << "] "; if (front * 2 < sgt.size() && sgt[front * 2].l) { q.push(front * 2); r = front * 2; // 持续更新新的最右结点 } if (front * 2 + 1 < sgt.size() && sgt[front * 2 + 1].l) { q.push(front * 2 + 1); r = front * 2 + 1; } if (front == nr) { // 遍历到当前行的最右结点 cout << '\n'; nr = r; } } cout << "------------------------------------------\n"; } }; int main() { // freopen("in.in", "r", stdin); int n, m; cin >> n >> m; vi a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); // stt.bfs(); // bfs观察树的状况,调试用,代码通过后选择保留 for (int T = m; T--;) { int op, l, r; cin >> op >> l >> r; switch (op) { case 0: stt.modify(1, l + 1, r + 1, 0, 0); break; case 1: stt.modify(1, l + 1, r + 1, 1, 0); break; case 2: stt.modify(1, l + 1, r + 1, -1, 1); break; case 3: cout << stt.query(1, l + 1, r + 1).s1 << '\n'; break; case 4: cout << stt.query(1, l + 1, r + 1).mx1 << '\n'; break; } // stt.bfs(); } return 0; } ``` ### 势能线段树 + 剪枝 线段树在维护区间修改操作时,有些操作是无法当场修改并做好延时标记,比如对整个区间的每一个数执行开根号操作。这时只能遍历所有的点全部修改。 但若**在修改的过程中** 发现,**整个区间在修改到一定程度的时候** ,**整个区间就无需修改** ,此时**就可以通过剪枝操作**,优化区间的修改。 这样的线段树也叫作**势能线段树**。这里的"势能"是一个很形象的比喻,大体意思就是球掉落地面时,弹起的高度越来越低,最大重力势能越来越小,当球降落到零势能面,就无法再降低高度。 > 这里用比喻来描述这类线段树的特点:线段树的某个区间的属性经过某种操作很多次之后将无法再继续使用这种操作,或使用这种操作后没有任何效果时可以把这个区间所在的子树进行剪枝。 #### P4145 花神游历各国 - 洛谷 [P4145 上帝造题的七分钟 2 / 花神游历各国 - 洛谷](https://www.luogu.com.cn/problem/P4145) 这题涉及区间修改和求区间和,解决方法很多,这里尝试使用线段树。 这题的开根号操作无法使用延迟标记。因为开根号时就要当场更新区间和,即 s u m ≠ ∑ i a i \\sqrt{sum}\\ne \\sum\\limits_{i}\\sqrt a_i sum =i∑a i ,此时只能枚举。但若在线段树上遍历叶结点,时间复杂度是 O ( n log ⁡ n ) \\text{O}(n\\log n) O(nlogn) ,还不如直接遍历数组,因为还有 m m m 次询问,即使遍历数组也会超时。 若使用线段树维护数据,则当一整个子树的最大值为 1 时,此时整个子树就没有开根号的必要。数据的最大值是 10 12 \\sqrt{10\^{12}} 1012 ,开最多 6 次根号就会变成 1 ,因此开根号操作越多,子树中的 1 也就越多,此时大量的子树就可以被剪枝。因此在区间和线段树的基础上再维护一个子树中的最大值信息,用于判断是否对指定子树进行剪枝。 所以线段树的各个组件: `Node` :区间信息 `[l,r]` ,区间和 `sum` 和区间最大值 `maxx` 。 `modify` :因为修改操作只有开根,所以当遍历到叶结点时,直接用 `sqrt` 然后赋值给整型,系统自动向下取整。因为开根无法使用延迟标记记录,所以需要遍历区间内的所有叶结点。当整个子树的最大值为 1 时,则在遍历过程中对整个子树进行剪枝,否则将无法使用线段树通过这个题。 其他的组件参考上文模板即可。 ```cpp #include using namespace std; using LL = long long; using vl = vector; struct Segment_Tree { struct Node { int l, r; LL sum, maxx; }; vector sgt; Segment_Tree(vl &a) { sgt.resize(a.size() * 4); build(1, a, 1, a.size() - 1); } void build(int p, vl &a, int l, int r) { sgt[p] = {l, r, a[l], a[l]}; if (l >= r) return; int mid = (l + r) >> 1; build(p * 2, a, l, mid); build(p * 2 + 1, a, mid + 1, r); adjust_fa(p); } void adjust_fa(int x) { Node &p = sgt[x], &lc = sgt[x * 2], &rc = sgt[x * 2 + 1]; p.sum = lc.sum + rc.sum; p.maxx = max(lc.maxx, rc.maxx); } void modify(int p, int l, int r) { if (sgt[p].maxx == 1) // 剪枝:当子树最大值为1时不需要再开根号 return; if (sgt[p].l == sgt[p].r) { sgt[p].sum = sqrt(sgt[p].sum); sgt[p].maxx = sqrt(sgt[p].maxx); return; } int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) modify(p * 2, l, r); if (r > mid) modify(p * 2 + 1, l, r); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].sum; int mid = (sgt[p].l + sgt[p].r) / 2; LL sum = 0; if (l <= mid) sum += query(p * 2, l, r); if (mid < r) sum += query(p * 2 + 1, l, r); return sum; } }; int main() { // freopen("in.in", "r", stdin); int n, T; cin >> n; vl a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; cin >> T; Segment_Tree stt(a); while (T--) { int op, l, r; cin >> op >> l >> r; if (op) cout << stt.query(1, min(l, r), max(l, r)) << '\n'; else stt.modify(1, min(l, r), max(l, r)); } return 0; } ``` #### Problem - 438D - Codeforces [Problem - 438D - Codeforces](https://codeforces.com/problemset/problem/438/D) [CF438D The Child and Sequence - 洛谷](https://www.luogu.com.cn/problem/CF438D) 数列的操作分 2 种:取模和修改单个数,同时需要查询区间和,可用线段树。 取模无法使用懒标记存储,因为对区间的所有数取模,求出来的和依旧有概率大于模数,而区间和在取模后必定小于模数,即: s u m % x ≤ ∑ i a i % x sum\\%x\\leq \\sum\\limits_{i}a_i\\% x sum%x≤i∑ai%x。 所以对于取模,依旧要遍历结点。 当子树的最大值小于模数 x x x 时没有取模的必要,此时可以进行剪枝。而 a % b = c a\\% b=c a%b=c , a = b k + c a=bk+c a=bk+c ,因为 c \< b c\ 2 c bk+c\>2c bk+c\>2c ,于是 c \< a 2 c\<\\frac{a}{2} c\<2a ,所以最差情况下 a a a 在模 b b b 后无限接近 a 2 \\frac{a}{2} 2a ,且每次都缩小一半,直到小于 b b b ,此时 a 2 t \< b \\frac{a}{2\^t}\ log ⁡ 2 a b t\>\\log_{2}\\frac{a}{b} t\>log2ba ,也就是说, a a a 在模同一个数 b b b 最差情况约 ⌊ log ⁡ 2 a b ⌋ \\lfloor\\log_{2}\\frac{a}{b}\\rfloor ⌊log2ba⌋ 次后,就无法再继续取模,次数上有限制,可以进行剪枝。 因为修改也是长度为 1 的区间的操作,所以不依赖懒标记,直接找到叶结点修改,并更新各个结点即可。 综上,线段树组件: `Node`:区间信息`[l,r]`,区间和 `sum`,区间最大值`maxx` 。 `modify`:上传 `op` 区分是哪一种操作。`op==2` 时是取模操作,优先判断是否需要剪枝,然后对叶结点取模;`op==3` 时是修改操作,找到叶结点修改。 其他的组件和上文相同,便不再赘述。 [Problem - 438D - Codeforces](https://codeforces.com/problemset/problem/438/D) 参考: ```cpp #include using namespace std; using LL = long long; using vi = vector; struct Segment_tree { struct Node { int l, r; LL sum, maxx; }; vector sgt; Segment_tree(vi &a) { sgt.resize(a.size() * 4); build(1, a, 1, a.size() - 1); } void build(int p, vi &a, int l, int r) { sgt[p] = {l, r, a[l], a[l]}; if (l == r) return; int mid = (l + r) >> 1; build(p * 2, a, l, mid); build(p * 2 + 1, a, mid + 1, r); adjust_fa(p); } void adjust_fa(int p) { sgt[p].sum = sgt[p * 2].sum + sgt[p * 2 + 1].sum; sgt[p].maxx = max(sgt[p * 2].maxx, sgt[p * 2 + 1].maxx); } void modify(int p, int l, int r, int op, int x) { if (op == 2 && sgt[p].maxx < x) // 剪枝 return; if (op == 2 && sgt[p].l == sgt[p].r) { sgt[p].sum %= x; sgt[p].maxx %= x; return; } else if (op == 3 && l == sgt[p].l && sgt[p].r == r) { sgt[p].sum = x; sgt[p].maxx = x; return; } int mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) modify(p * 2, l, r, op, x); if (mid < r) modify(p * 2 + 1, l, r, op, x); adjust_fa(p); } LL query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].sum; int mid = (sgt[p].l + sgt[p].r) / 2; LL sum = 0; if (l <= mid) sum += query(p * 2, l, r); if (mid < r) sum += query(p * 2 + 1, l, r); return sum; } }; int main() { // freopen("in.in", "r", stdin); int n, m; cin >> n >> m; vi a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); for (int T = m; T--;) { int op, l, r, k; LL x; cin >> op; if (op == 1) { cin >> l >> r; cout << stt.query(1, l, r) << '\n'; } else if (op == 2) { cin >> l >> r >> x; stt.modify(1, l, r, op, x); } else { cin >> k >> x; stt.modify(1, k, k, op, x); } } return 0; } ``` ### 权值线段树 + 离散化 问题:对于一组数据,查询 \[ x , y \] \[x, y\] \[x,y\] 之间的数,一共出现了多少次?针对这样的问题,就可以用权值线段树来解决。 相较于普通的线段树,权值线段树维护区间内的数出现的次数: * 结点的区间信息表示:数据的值域; * 结点的权值信息表示:这些数据一共出现的次数。 比如数据: a = \[ 1 , 5 , 5 , 2 , 2 , 4 , 1 , 1 \] a = \[1, 5, 5, 2, 2, 4, 1, 1\] a=\[1,5,5,2,2,4,1,1\] ,对应的权值线段树为: ![请添加图片描述](https://i-blog.csdnimg.cn/direct/3c83fd5744964616928b22a768fbbc99.png) 实际做题中,数据的值域一般很大(例如 10 9 10\^9 109 ),如果仅仅考虑数的大小而不考虑具体的值,常常会把原始数据离散化。 #### P1908 逆序对 - 洛谷 [P1908 逆序对 - 洛谷](https://www.luogu.com.cn/problem/P1908) 这题可用归并排序、线段树和树状数组解决。这里使用线段树。 首先,因为这题的数据最大可达 10 9 10\^9 109 ,线段树不可能开 4 × 10 9 4\\times 10\^9 4×109 个空间,因此必须离散化处理,得到每个数的映射。 然后,建立一个空的权值线段树,重新遍历原始数组,每遍历一个数,就在权值线段树中修改这个数的数量。至于查询逆序对的数量,可在记录的同时,顺便查询权值线段树中大于当前数的区间的权值和即可(边记录边查询,线段树只有已遍历过的数的信息)。 所以用线段树统计逆序对数量的算法,本质是优化 2 层循环的暴力枚举算法: ```cpp using LL = long long; LL ans(vector &a) { LL ans = 0; for (int i = 2; i <= n; i++) for (int j = 1; j < i; j++) // 引入线段树的目的是优化这一层循环 if (a[i] < a[j]) ++ans; return ans; } ``` 总的时间复杂度是 O ( n log ⁡ n ) \\text{O}(n\\log n) O(nlogn) ,离散化处理的排序操作占大头,比归并排序多遍历几次数组。 逆序对统计的线段树参考: ```cpp #include using namespace std; using LL = long long; using vi = vector; struct Segment_tree { struct Node { int l, r; int t; }; vector sgt; Segment_tree(int sz) { sgt.resize((sz + 1) * 4); build(1, 1, sz); } void build(int p, int l, int r) { sgt[p] = {l, r, 0}; if (l == r) // 建立空的权值线段树 return; int mid = (l + r) / 2; build(p * 2, l, mid); build(p * 2 + 1, mid + 1, r); adjust_fa(p); } void adjust_fa(int p) { sgt[p].t = sgt[p * 2].t + sgt[p * 2 + 1].t; } int query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p].t; int sum = 0, mid = (sgt[p].l + sgt[p].r) / 2; if (l <= mid) sum += query(p * 2, l, r); if (mid < r) sum += query(p * 2 + 1, l, r); return sum; } void modify(int p, int x) { if (sgt[p].l == sgt[p].r && sgt[p].l == x) { sgt[p].t++; // 更新数量 return; } int mid = (sgt[p].l + sgt[p].r) / 2; if (x <= mid) modify(p * 2, x); else modify(p * 2 + 1, x); adjust_fa(p); } }; int main() { // freopen("in.in", "r", stdin); int n; // ump[a[i]]==ip unordered_map ump; cin >> n; vi a(n + 1, 0), b; for (int i = 1; i <= n; i++) cin >> a[i]; // 备份 b = a; // 对a[i]做离散化处理 sort(a.begin() + 1, a.end()); for (int i = 1, ip = 0; i <= n; i++) { if (ump.count(a[i]) > 0) continue; ump[a[i]] = ++ip; } // 初始化一个空的权值线段树 Segment_tree stt(ump.size()); // 每遍历1个数,就用权值线段树进行统计 stt.modify(1, ump[b[1]]); LL ans = 0; for (int i = 2; i <= n; i++) { stt.modify(1, ump[b[i]]); // 统计逆序对时只需查询已记录的数即可 ans += stt.query(1, ump[b[i]] + 1, ump.size()); } cout << ans; return 0; } ``` ### 线段树 + 数学 线段树维护的信息与数学有关时,一般需要先做一些数学推导,才能确定线段树中的节点维护哪些信息,比较考验数学功底了。 之前的线段树基本都是区间和区间最值,相对 "比较好维护" 。 #### P5142 区间方差 - 洛谷 [P5142 区间方差 - 洛谷](https://www.luogu.com.cn/problem/P5142) 公式中有平均数 a a a ,不方便计算,于是将方差公式的 a a a 去掉,也可以保留但要用区间和去求。 d = 1 n ∑ i = 1 n ( a i − a ) 2 = 1 n ∑ i = 1 n ( a i 2 + a 2 − 2 a a i ) = 1 n ( ∑ i = 1 n a i 2 + n a 2 − 2 a ∑ i = 1 n a i ) = 1 n ∑ i = 1 n a i 2 + ( 1 n ∑ i = 1 n a i ) 2 − 2 n ( 1 n ∑ i = 1 n a i ) ∑ i = 1 n a i = 1 n ∑ i = 1 n a i 2 + 1 n 2 ( ∑ i = 1 n a i ) 2 − 2 n 2 ( ∑ i = 1 n a i ) 2 = 1 n ∑ i = 1 n a i 2 − 1 n 2 ( ∑ i = 1 n a i ) 2 \\begin{aligned} d\&=\\frac{1}{n}\\sum\\limits_{i=1}\^{n}(a_i-a)\^2\\\\ \&=\\frac{1}{n}\\sum\\limits_{i=1}\^{n}(a_i\^2+a\^2-2aa_i)\\\\ \&=\\frac{1}{n}(\\sum\\limits_{i=1}\^{n}a_i\^2+na\^2-2a\\sum\\limits_{i=1}\^{n}a_i)\\\\ \&=\\frac{1}{n}\\sum\\limits_{i=1}\^{n}a_i\^2+(\\frac{1}{n}\\sum\\limits_{i=1}\^{n}a_i)\^2-\\frac{2}{n}(\\frac{1}{n}\\sum\\limits_{i=1}\^{n}a_i)\\sum\\limits_{i=1}\^{n}a_i\\\\ \&=\\frac{1}{n}\\sum\\limits_{i=1}\^{n}a_i\^2+\\frac{1}{n\^2}(\\sum\\limits_{i=1}\^{n}a_i)\^2-\\frac{2}{n\^2}(\\sum\\limits_{i=1}\^{n}a_i)\^2\\\\ \&=\\frac{1}{n}\\sum\\limits_{i=1}\^{n}a_i\^2-\\frac{1}{n\^2}(\\sum\\limits_{i=1}\^{n}a_i)\^2 \\end{aligned} d=n1i=1∑n(ai−a)2=n1i=1∑n(ai2+a2−2aai)=n1(i=1∑nai2+na2−2ai=1∑nai)=n1i=1∑nai2+(n1i=1∑nai)2−n2(n1i=1∑nai)i=1∑nai=n1i=1∑nai2+n21(i=1∑nai)2−n22(i=1∑nai)2=n1i=1∑nai2−n21(i=1∑nai)2 题目要求既要修改指定某个数的值,又要求指定范围内的数的方差,所以用线段树来维护数据。因为只修改长度为 1 的区间的值,所以不需要使用延迟标记。 线段树维护方差显然不可取,因为子结点维护的方差和父结点没有必然联系。 线段树的结点存储当前区间内所有数据的和,以及所有数据的平方和。父结点的这 2 个参数均来自 2 个子结点的参数之和取模。 查询时需要查询 2 个数据,因此返回整个结构体。同时代码的数据量很大,稍不注意就会漏掉取模,造成 " WA 声" 一片。 ```cpp #include using namespace std; using LL = long long; using vl = vector; const LL MOD = 1e9 + 7; LL qpow(LL a, LL b, LL c) { LL ans = 1; while (b) { if (b & 1) ans = ans * a % c; a = a * a % c; b >>= 1; } return ans; } struct Segment_tree { struct Node { int l, r; LL s, s2; // s即\sum a[i],s2即\sum (a[i])^2 }; vector sgt; Segment_tree(vl &a) { sgt.resize(a.size() * 4); build(1, a, 1, a.size() - 1); } void build(int p, vl &a, int l, int r) { sgt[p] = {l, r, 0, 0}; if (l >= r) { sgt[p] = {sgt[p].l, sgt[p].r, a[l] % MOD, a[l] * a[l] % MOD}; return; } int mid = (l + r) / 2; build(p * 2, a, l, mid); build(p * 2 + 1, a, mid + 1, r); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } void adjust_fa(Node &p, Node &lc, Node &rc) { p.s = lc.s + rc.s % MOD; p.s2 = lc.s2 + rc.s2 % MOD; } void modify(int p, int x, LL y) { if (sgt[p].l == sgt[p].r && x == sgt[p].l) { sgt[p].s = y % MOD; sgt[p].s2 = y * y % MOD; return; } int mid = (sgt[p].l + sgt[p].r) / 2; if (x <= mid) modify(p * 2, x, y); else modify(p * 2 + 1, x, y); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } Node query(int p, int l, int r) { if (l <= sgt[p].l && sgt[p].r <= r) return sgt[p]; int mid = (sgt[p].l + sgt[p].r) / 2; if (r <= mid) return query(p * 2, l, r); if (l > mid) return query(p * 2 + 1, l, r); Node ret, L = query(p * 2, l, r), R = query(p * 2 + 1, l, r); adjust_fa(ret, L, R); return ret; } }; int main() { // freopen("in.in", "r", stdin); int n, m; cin >> n >> m; vl a(n + 1, 0); for (int i = 1; i <= n; i++) cin >> a[i]; Segment_tree stt(a); for (int T = m; T--;) { int op, x, y; cin >> op >> x >> y; if (op == 1) stt.modify(1, x, y); else { // 区间长度的平方也会溢出 LL len = (y - x + 1) % MOD; LL n1 = qpow(len, MOD - 2, MOD), n2 = qpow(len * len % MOD, MOD - 2, MOD); Segment_tree::Node tmp = stt.query(1, x, y); LL part1 = n1 * (tmp.s2 % MOD) % MOD; LL part2 = n2 * (tmp.s * tmp.s % MOD) % MOD; // 括号不能去掉,否则会产生奇怪的优先级顺序 cout << (part1 - part2 % MOD + MOD) % MOD << '\n'; } } return 0; } ``` #### P10463 Interval GCD - 洛谷 [P10463 Interval GCD - 洛谷](https://www.luogu.com.cn/problem/P10463) 参考《算法竞赛进阶指南》,没有这个结论几乎无从下手: 根据 "《九章算术》之更相减损术" ,我们知道 gcd ( x , y ) = gcd ( x , y − x ) \\text{gcd}(x,y)=\\text{gcd}(x,y-x) gcd(x,y)=gcd(x,y−x) 。它可以进一步扩展到三个数的情况: gcd ( x , y , z ) = gcd ( x , y − x , z − y ) \\text{gcd}(x,y,z)=\\text{gcd}(x,y-x,z-y) gcd(x,y,z)=gcd(x,y−x,z−y) 。 > 这里证明这个结论对任意多个整数都成立: > > 设 gcd ( a 1 , a 2 , ... , a n ) = d 1 \\text{gcd}(a_1,a_2,\\dots,a_n)=d_1 gcd(a1,a2,...,an)=d1 , gcd ( a 1 , a 2 − a 1 , ... , a n − a n − 1 ) = d 2 \\text{gcd}(a_1,a_2-a_1,\\dots,a_n-a_{n-1})=d_2 gcd(a1,a2−a1,...,an−an−1)=d2 。 > > 证明 d 1 ≤ d 2 d_1\\leq d_2 d1≤d2 : > > 因上式成立,故 d 1 d_1 d1 是 a i a_i ai 的约数。 > > 所以 a i = k i d 1 a_i=k_id1 ai=kid1 ,而 a i − a i − 1 = ( k i − k i − 1 ) d 1 a_i-a_{i-1}=(k_i-k_i-1)d_1 ai−ai−1=(ki−ki−1)d1 还是 d 1 d_1 d1 的约数, > > 所以 gcd ( a 1 , a 2 − a 1 , ... , a n − a n − 1 ) ≥ d 1 \\text{gcd}(a_1,a_2-a_1,\\dots,a_n-a_{n-1})\\geq d_1 gcd(a1,a2−a1,...,an−an−1)≥d1 , d 1 d_1 d1 是 { a 1 , a 2 − a 1 , ... , a n − a n − 1 } \\{a_1,a_2-a_1,\\dots,a_n-a_{n-1}\\} {a1,a2−a1,...,an−an−1} 的一个公约数,但不知是否是最大的。此时只需证明 d 2 ≤ d 1 d_2\\leq d_1 d2≤d1 即可证明结论成立。 > > 证明 d 2 ≤ d 1 d_2\\leq d_1 d2≤d1 : > > 同理 d 2 d_2 d2 是 a 1 a_1 a1 和 a i − a i − 1 a_i-a_{i-1} ai−ai−1 的约数,则 a i − a i − 1 = k i d 2 a_i-a_{i-1}=k_id_2 ai−ai−1=kid2 。 > > 而 a 1 + a 2 − a 1 = a 2 = ( k 1 + k 2 ) d 2 a_1+a_2-a_1=a_2=(k_1+k_2)d_2 a1+a2−a1=a2=(k1+k2)d2 ,同理还可求 a i − a i − 1 + a i − 1 − a i − 2 + ⋯ + a 1 = a i = ( k i + k i − 1 + ⋯ + 1 ) d 2 a_i-a_{i-1}+a_{i-1}-a_{i-2}+\\dots +a_1=a_i=(k_i+k_{i-1}+\\dots+1)d_2 ai−ai−1+ai−1−ai−2+⋯+a1=ai=(ki+ki−1+⋯+1)d2 。 > > 所以 d 2 d_2 d2 也是 { a 1 , a 2 , ... , a n } \\{a_1,a_2,\\dots,a_n\\} {a1,a2,...,an} 的一个约数,所以 gcd ( a 1 , a 2 , ... , a n ) = d 1 ≥ d 2 \\text{gcd}(a_1,a_2,\\dots,a_n)=d_1\\geq d_2 gcd(a1,a2,...,an)=d1≥d2 。 > > 因为 d 1 ≤ d 2 d_1\\leq d_2 d1≤d2 和 d 1 ≥ d 2 d_1\\geq d_2 d1≥d2 同时成立,所以 d 1 = d 2 d_1=d_2 d1=d2 ,所以证毕。 题目是区间修改 + + + 区间查询,可尝试使用线段树。区间修改为了保证时间复杂度,需要使用延时标记,此时就不好求 gcd ,因为父区间在增加 d d d 之后,不一定等于 2 个子区间增加 d d d 之后的 gcd。 但若有了 gcd ( a 1 , a 2 , ... , a n ) = gcd ( a 1 , a 2 − a 1 , ... , a n − a n − 1 ) \\text{gcd}(a_1,a_2,\\dots,a_n)=\\text{gcd}(a_1,a_2-a_1,\\dots,a_n-a_{n-1}) gcd(a1,a2,...,an)=gcd(a1,a2−a1,...,an−an−1) 这个结论就大为不同,此时线段树就可以维护差分序列,区间修改就变成了单点修改,不需要延时标记。 所以 `Node` 要维护的信息里就有 `gcd` 。同时题目要求的是 gcd ( a l , a l + 1 , ... , a r ) \\text{gcd}(a_l,a_{l+1},\\dots,a_r) gcd(al,al+1,...,ar) ,但第 1 个 a l a_l al 并不是差分而是原数组,要求整个式子需要知道 a l a_l al ,后续的 gcd ( a l + 1 , ... , a r ) \\text{gcd}(a_{l+1},\\dots,a_r) gcd(al+1,...,ar) 可通过线段树维护的信息获取,因此除了 gcd ,线段树的 `Node` 还需要维护区间和 `sum` 。 `adjust_fa`:根据结论,父结点的 gcd 可通过 2 个子结点的 gcd 求解。区间和不必多说。 `modify` 修改叶结点和上文基本差不多,就不过多赘述。 `query` 的查询分成查区间和的 `query_sum` 和查 gcd 的 `query_gcd` 。 ```cpp #include using namespace std; using LL = long long; using vl = vector; struct Segment_tree { struct Node { int l, r; LL sum, gcd; // 差分数组的区间和、gcd }; vector sgt; Segment_tree(vl &a) { sgt.resize(a.size() * 4); build(1, a, 1, a.size() - 2); } void build(int p, vl &a, int l, int r) { sgt[p] = {l, r, 0, 0}; if (l >= r) { sgt[p] = {l, r, a[l], a[l]}; // 一个数自己就是自己的最大约数 return; } int mid = (l + r) / 2; build(p * 2, a, l, mid); build(p * 2 + 1, a, mid + 1, r); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } void adjust_fa(Node &p, Node &lc, Node &rc) { p.sum = lc.sum + rc.sum; p.gcd = gcd(lc.gcd, rc.gcd); } LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } void modify(int p, int x, LL y) { if (sgt[p].l == sgt[p].r) { sgt[p].sum += y; sgt[p].gcd += y; return; } int mid = (sgt[p].l + sgt[p].r) / 2; if (x <= mid) modify(p * 2, x, y); else modify(p * 2 + 1, x, y); adjust_fa(sgt[p], sgt[p * 2], sgt[p * 2 + 1]); } LL query_sum(int p, int x, int y) { int l = sgt[p].l, r = sgt[p].r; if (x <= l && r <= y) return sgt[p].sum; int mid = (l + r) >> 1; LL sum = 0; if (x <= mid) sum += query_sum(p * 2, x, y); if (y > mid) sum += query_sum(p * 2 + 1, x, y); return sum; } LL query_gcd(int p, int x, int y) { int l = sgt[p].l, r = sgt[p].r; if (x <= l && r <= y) return sgt[p].gcd; int mid = (l + r) >> 1; LL g = 0; // 分治思想,最终gcd等于区间左、右的gcd if (x <= mid) g = gcd(query_gcd(p * 2, x, y), g); if (y > mid) g = gcd(query_gcd(p * 2 + 1, x, y), g); return g; } }; int main() { // freopen("in.in", "r", stdin); int n, m; cin >> n >> m; vl a(n + 2, 0); for (LL i = 1, x; i <= n; i++) { cin >> x; a[i] += x; a[i + 1] -= x; } Segment_tree stt(a); for (int T = m; T--;) { char ch; int l, r; LL d; cin >> ch >> l >> r; if (ch == 'C') { cin >> d; stt.modify(1, l, d); if (r + 1 < a.size() - 1) stt.modify(1, r + 1, -d); } else { LL sum = stt.query_sum(1, 1, l); // 查询区间和 LL g = 0; // 查询区间最⼤公约数 if (l + 1 <= r) g = stt.query_gcd(1, l + 1, r); LL ret = stt.gcd(sum, g); cout << abs(ret) << endl; } } return 0; } ``` 线段树还有扫描线、线段树合并、树套树等知识点,后期若有机会再进行补充。 ## OJ参考 1. 线段树构建 [P3374 【模板】树状数组 1 - 洛谷](https://www.luogu.com.cn/problem/P3374) [P3372 【模板】线段树 1 - 洛谷](https://www.luogu.com.cn/problem/P3372) [P3368 【模板】树状数组 2 - 洛谷](https://www.luogu.com.cn/problem/P3368) 2. 线段树维护更多类型的信息 [P1816 忠诚 - 洛谷](https://www.luogu.com.cn/problem/P1816) \[P3870 [TJOI2009\] 开关 - 洛谷](https://www.luogu.com.cn/problem/P3870) [P2184 贪婪大陆 - 洛谷](https://www.luogu.com.cn/problem/P2184) [P1438 无聊的数列 - 洛谷](https://www.luogu.com.cn/problem/P1438) 一题多解 3. 多重区间操作 [P3373 【模板】线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3373) 一题多解 [P1253 扶苏的问题 - 洛谷](https://www.luogu.com.cn/problem/P1253) 4. 线段树 + + + 分治的综合题 [P1115 最大子段和 - 洛谷](https://www.luogu.com.cn/problem/P1115) 这题有太多解法,这里作为案例引入,不提供线段树解法。 [P4513 小白逛公园 - 洛谷](https://www.luogu.com.cn/problem/P4513) \[P2572 [SCOI2010\] 序列操作 - 洛谷](https://www.luogu.com.cn/problem/P2572) 5. 线段树 + + + 剪枝的综合题 [P4145 上帝造题的七分钟 2 / 花神游历各国 - 洛谷](https://www.luogu.com.cn/problem/P4145) [CF438D The Child and Sequence - 洛谷](https://www.luogu.com.cn/problem/CF438D) [Problem - 438D - Codeforces](https://codeforces.com/problemset/problem/438/D) 6. 权值线段树 + + + 离散化处理 [P1908 逆序对 - 洛谷](https://www.luogu.com.cn/problem/P1908) 一题多解 7. 线段树 + + + 数学 [P5142 区间方差 - 洛谷](https://www.luogu.com.cn/problem/P5142) [P10463 Interval GCD - 洛谷](https://www.luogu.com.cn/problem/P10463)

相关推荐
王老师青少年编程5 小时前
2025年3月GESP真题及题解(C++七级): 选择题和判断题(题解)
c++·真题·gesp·答案·csp·信奥赛·七级
白鲸开源5 小时前
(三)ODS/明细层落地设计要点:把数据接入层打造成“稳定可运维”的基础设施
大数据·数据结构·数据库
sycmancia5 小时前
C++——多态
开发语言·c++
苏纪云6 小时前
蓝桥杯知识点——day2
数据结构·算法·蓝桥杯
iFlyCai6 小时前
数据结构与算法之希尔排序
数据结构·算法·排序算法
shehuiyuelaiyuehao6 小时前
算法1,移动零
数据结构·算法·排序算法
shehuiyuelaiyuehao6 小时前
算法2,复写零
数据结构·算法
像污秽一样6 小时前
算法设计与分析-算法效率分析基础-习题1.1
c语言·数据结构·c++·算法
2401_8846022715 小时前
程序人生-Hello’s P2P
c语言·c++
初中就开始混世的大魔王15 小时前
2 Fast DDS Library概述
c++·中间件·信息与通信