分享|(线段树) 力扣线段树 题集 记录
3278
2024.01.19
2025.02.22
发布于 未知归属地
  1. 前言

    1. 🔖思路与技巧
    2. 🔖碎碎念
    3. 🔖题目关联表
  2. 题单

    1. 🏷️53. 最大子数组和

      1. ⭐区间合并 字串 (最大子序和)
    2. 🏷️239. 滑动窗口最大值

      1. ⭐经典RMQ问题
      2. ⭐区间求最值
      3. 题意与思路
      4. Code
    3. 🏷️307. 区域和检索 - 数组可修改

      1. ⭐单点操作 区间求和
      2. 🍀加权&覆盖
      3. 题意与思路
      4. Code
    4. 🏷️327. 区间和的个数

      1. ⭐离散化+线段树
      2. 题意与思路
      3. Code
    5. 🏷️654. 最大二叉树

      1. ⭐区间求最值(返回下标)
      2. 题意与思路
      3. Code
    6. 🏷️699. 掉落的方块

      1. ⭐区间覆盖 区间求最值
      2. 🍀四种动态开点法
      3. 题意与思路
      4. Code
    7. 🏷️715. Range 模块

      1. ⭐区间true|false 区间计数
      2. ⭐动态开点(卡内存)
      3. 题意与思路
      4. Code
    8. 🏷️729. 我的日程安排表 I

    9. 🏷️731. 我的日程安排表 II

      1. ⭐区间计次 区间求次数
      2. ⭐小查询次数的动态开点
      3. 题意与思路
      4. Code
    10. 🏷️732. 我的日程安排表 III

    11. 🏷️918. 环形子数组的最大和

      1. ⭐区间合并 最大子数组和
      2. 题意与思路
      3. Code
    12. 🏷️933. 最近的请求次数

      1. ⭐区间累计 区间求和
      2. ⭐动态开点(卡内存)
      3. 题意与思路
      4. Code
    13. 🏷️1109. 航班预订统计

      1. ⭐线段树处理差分
      2. 题意与思路
      3. Code
    14. 🏷️1146. 快照数组

      1. ⭐可持久化线段树
      2. 题意与思路
      3. Code
    15. 🏷️1622. 奇妙序列

      1. ⭐区间加乘
      2. ⭐多状态维护
      3. 题意与思路
      4. Code
    16. 🏷️1893. 检查是否区域内所有整数都被覆盖

      1. ⭐区间覆盖 true or false 区间计数
      2. 题意与思路
      3. Code
    17. 🏷️2166. 设计位集

      1. ⭐true|false综合题
      2. 题意与思路
      3. 🗑️Code
    18. 🏷️2213. 由单个字符重复的最长子字符串

      1. ⭐区间合并(最长连续字符)
      2. 题意与思路
      3. Code
    19. 🏷️2407. 最长递增子序列 II

      1. ⭐300. 最长递增子序列
      2. 题意与思路
      3. Code
    20. 🏷️2276. 统计区间中的整数数目

      1. ⭐区间覆盖 true or false 区间计数
      2. ⭐动态开点 指针
      3. 题意与思路
      4. Code
    21. 🏷️2286. 以组为单位订音乐会的门票

      1. ⭐线段树上二分
      2. ⭐多状态维护(最值+前缀和)
      3. 题意与思路
      4. Code
    22. 🏷️2569. 更新数组后处理求和查询

      1. ⭐区间 true|false 反转
      2. 题意与思路
      3. Code
    23. 🏷️2736. 最大和查询

      1. ⭐二维偏序模板题
      2. ⭐区间维护最值
      3. 题意与思路
      4. Code
  3. END

    1. 🔗ref

前言

本文主要记录力扣中出现的能够用线段树处理的题目。

线段树是一种非常强大的数据结构。但是很多用线段树能处理的题也会有别的解法。

线段树的核心难点在于如何对区间的性质状态进行维护和转移,这点和动态规划很像。


注意:本文不适合连一点线段树概念都没有的小白观看。

由于力扣一直将我的帖子不过审核,因此这里就不贴关于线段树的其他学习内容了。

🔖思路与技巧

一般线段树可能需要的状态标记不止一个,因此推荐使用结构体来封装。因为在结构体中加一个变量非常简单,重新多开一个数组会比较麻烦。

关于动态开点的线段树,通常以下几种方式

  • 借助语言的数据结构自动开点

  • 指针new节点

  • 提前估点,用数组下标指向

其实基础好的话这两种方式可以很快掌握。因此这绝对不是你不会处理线段树题目的绊脚石。因为线段树的核心就在状态维护上,而不是怎么开点。不要把这个作为学不好的借口。

由于线段树的一些接口的部分参数是固定的,其实可以在做一个间接层的封装。而且在动态开点的题目中,可以隔离内部使用的实际数据结构。

🔖碎碎念

大多数线段树的code都非常适合抽离出来作为模板。基于面向对象语言的话,完全可以写成一个类。

一方面避免与题目或者说业务代码有融合,另一方面可以直接cv掉这个class并更清晰的进行修改。

线段树的整体的框架比较固定,本文中出现的code均为楼主本人个人的模板和风格。读者应该将重点放在线段树的状态维护上!

除了一些比较裸的题,比较难的线段树的题是会配合另一种算法,做一些数值状态维护操作的题。可见在很多难题中线段树只是作为一种数据维护的辅助方式。

在本文中会看到一些类似甚至相同的维护操作。可能是调用主题函数需要不同,一方面是可以多验证下模板,另一方面是我已经写好了舍不得删掉了。

🔖题目关联表

有关联的题有很多,这里放一些比较有代表性质的题。

需求典型例题
单点操作🏷️307. 区域和检索 - 数组可修改
加权|覆盖🏷️307. 区域和检索 - 数组可修改
动态开点🏷️699. 掉落的方块
区间合并🏷️918. 环形子数组的最大和
🏷️2213. 由单个字符重复的最长子字符串
可持久化线段树🏷️1146. 快照数组

题单

🏷️53. 最大子数组和

⭐区间合并 字串 (最大子序和)

🔗完善版:🏷️918. 环形子数组的最大和

更完善代码见918题,但是918也只有build和query,没有修改的update操作。

🏷️239. 滑动窗口最大值

⭐经典RMQ问题

⭐区间求最值

题意与思路

本题是一道好题,此处给出多种解法方案的具体代码。


给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值


这是一个很经典的模板例题,区间求最值。

但是这里没有update操作,因此不用lazy和pushDown。

最后注意,这种与最值相关的题目,返回的默认值都需要注意,不是无脑的0,也不是无脑的不是0。

Code

#define INF 0x3f3f3f3f
#define ls (root << 1)
#define rs (root << 1 | 1)
// 区间查最值
class SegTree {
private:
    // 本题没有更新update操作
    // 不用lazy
    struct Node {
        int val;
        // int lazy;
    };
    vector<Node> tree;
    vector<int> arr;
    int n;

public:
    SegTree(vector<int>& arr, int n) {
        this->n = n;
        this->arr = arr;
        this->tree.resize(n * 4);
    }

    void pushUp(int root) {
        tree[root].val = max(tree[ls].val, tree[rs].val);
    }

    void build(int root, int left, int right) {
        if (left == right) {
            tree[root].val = arr[left];
            return ;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root);
    }

    // 查询最大值,默认无效返回无穷小
    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return -INF;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        // 本题没有更新update操作
        // 不用pushDown
        int mid = (right - left) / 2 + left;
        return max(
            query(ls, left, mid, from, to),
            query(rs, mid + 1, right, from, to)
        );
    }
};

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        const int n = nums.size();
        // [0, n - 1] -> [1, n]
        nums.insert(nums.begin(), 0);

        SegTree tree(nums, n);
        tree.build(1, 1, n);

        vector<int> ans;
        for (int i = 1; i + k - 1 <= n; i += 1) {
            ans.push_back(tree.query(1, 1, n, i, i + k - 1));
        }

        return ans;
    }
};

🏷️307. 区域和检索 - 数组可修改

⭐单点操作 区间求和

🍀加权&覆盖

题意与思路

本题是一道好题,此处给出多种解法方案的具体代码。


给你一个数组 nums ,请你完成两类查询。

  1. 其中一类查询要求 更新 数组 nums 下标对应的值
  2. 另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 ,其中 left <= right

典型的单点操作型。单点操作可以不用lazy和pushDown,相对比较简单。

但是此处还是给出区间操作的代码。因为单点就是一个长度为1的区间。

同时本题的题面是覆盖,但是实际操作可以使用加权的方式,此处也会给出。

Code

加权式

单点加权,区间求和
区间加权,区间求和
// 单点加权,区间求和
#define ls (root << 1)
#define rs (root << 1 | 1)
class SegTree {
private:
    // 单点操作不用lazy
    struct Node {
        int val = 0;
    };
    vector<Node> tree;
    vector<int>  arr;
    int n = 0;

public:
    SegTree() = default;
    // [1, n]
    SegTree(vector<int>& arr, int n) {
        this->arr = arr;
        this->n = n;
        this->tree.resize(n * 4);
    }

public:
    void pushUp(int root) {
        tree[root].val = tree[ls].val + tree[rs].val;
    }

    void build(int root, int left, int right) {
        if (left == right) {
            tree[root].val = arr[left];
            return ;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root);
    }

    // 对pos点施加增量val
    void update(int root, int left, int right, int pos, int val) {
        if (pos < left || pos > right) {
            return ;
        }
        if (pos <= left && right <= pos) {
            tree[root].val += val;
            return ;
        }

        // 单点操作不用pushDown
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, pos, val);
        update(rs, mid + 1, right, pos, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (to < left || right < from) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        int mid = (right - left) / 2 + left;
        return query(ls, left, mid, from, to)
             + query(rs, mid + 1, right, from, to);
    }
};

class NumArray {
private:
    // [1, n]
    vector<int> arr;
    int n = 0;
    SegTree tree;

public:
    // nums: [0, n-1]
    // -> arr: [1, n]
    NumArray(vector<int>& nums) {
        n = nums.size();
        arr = std::move(nums);
        arr.insert(arr.begin(), 0);
        tree = SegTree(arr, n);
        tree.build(1, 1, n);
    }
    
    // [1, n]
    // 算出增量
    void update(int index, int val) {
        index += 1;
        int diff = val - arr[index];
        arr[index] = val;
        tree.update(1, 1, n, index, diff);
    }
    
    // [1, n]
    int sumRange(int left, int right) {
        left += 1, right += 1;
        return tree.query(1, 1, n, left, right);
    }
};

覆盖式

单点覆盖,区间求和
区间覆盖,区间求和
// 单点覆盖,区间求和
#define ls (root << 1)
#define rs (root << 1 | 1)
class SegTree {
private:
    // 单点操作不用lazy
    struct Node {
        int val = 0;
    };
    vector<Node> tree;
    vector<int>  arr;
    int n = 0;

public:
    SegTree() = default;
    // [1, n]
    SegTree(vector<int>& arr, int n) {
        this->arr = arr;
        this->n = n;
        this->tree.resize(n * 4);
    }

public:
    void pushUp(int root) {
        tree[root].val = tree[ls].val + tree[rs].val;
    }

    void build(int root, int left, int right) {
        if (left == right) {
            tree[root].val = arr[left];
            return ;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root);
    }

    // 对pos点覆盖为val
    void update(int root, int left, int right, int pos, int val) {
        if (pos < left || pos > right) {
            return ;
        }
        if (pos <= left && right <= pos) {
            tree[root].val = val;
            return ;
        }

        // 单点操作不用pushDown
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, pos, val);
        update(rs, mid + 1, right, pos, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (to < left || right < from) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        int mid = (right - left) / 2 + left;
        return query(ls, left, mid, from, to)
             + query(rs, mid + 1, right, from, to);
    }
};

class NumArray {
private:
    // [1, n]
    vector<int> arr;
    int n = 0;
    SegTree tree;

public:
    // nums: [0, n-1]
    // -> arr: [1, n]
    NumArray(vector<int>& nums) {
        n = nums.size();
        arr = std::move(nums);
        arr.insert(arr.begin(), 0);
        tree = SegTree(arr, n);
        tree.build(1, 1, n);
    }
    
    // [1, n]
    // 直接覆盖
    void update(int index, int val) {
        index += 1;
        arr[index] = val;
        tree.update(1, 1, n, index, val);
    }
    
    // [1, n]
    int sumRange(int left, int right) {
        left += 1, right += 1;
        return tree.query(1, 1, n, left, right);
    }
};

🏷️327. 区间和的个数

⭐离散化+线段树

题意与思路

给你一个整数数组 nums 以及两个整数 lowerupper 。求数组中,值位于范围 [lower, upper] (包含 lowerupper)之内的 区间和的个数

区间和 S(i, j) 表示在 nums 中,位置从 ij 的元素之和,包含 ij (ij)。


只读一遍可以说题意非常难懂,这里简答描述下,并举例:

输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。
  • nums[0] = -2
  • nums[1] = 5
  • nums[2] = -1

能构成的所有区间,且在[lower, upper]范围内的是:

  • [0, 0] = -2 = -2
  • [0, 1] = -2 + 5 = 3
  • [0, 2] = -2 + 5 + -1 = 2
  • [1, 1] = 5 = 5
  • [1, 2] = 5 + -1 = 4
  • [2, 2] = -1 = -1

因此,说白了就是要枚举所有区间的和,然后判断是否在范围内


对于快速求和,很容易想到前缀和来处理。

目的是统计pre[j] - pre[i] ∈ [low, up]的数量

方式是通过遍历pre[j] 查询缓存的pre[i]的数量

这个查询是处于[0...j-1]的,不能是j位置本身

  • <=> low <= (pre[j] - pre[i]) <= up
  • <=> -up <= (-pre[j] + pre[i]) <= -low
  • <=> (pre[j] - up) <= pre[i] <= (pre[j] - low)

这是一种比较常见的模型,固定遍历一个序列,并查询历史缓存,统计结束后在将当前记录到缓存中。经典的两数之和也是这中思路,但是要想到这种转换在不同的复杂题中并不简单。

最后注意,本题的数据量很大,因此需要离散化。

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
using ll = long long;

class SegTree {
private:
    int n;
    vector<int> tree;

public:
    SegTree(int n) : n(n), tree(n * 4) {}

    void pushUp(int root) {
        tree[root] = tree[ls] + tree[rs];
    }

    // 单点加权
    // 没有lazy,没有pushDown
    void update(int root, int left, int right, int pos, int val) {
        if (pos < left || pos > right) {
            return ;
        }
        // 长度为1的单点
        if (pos <= left && right <= pos) {
            tree[root] += val;
            return ;
        }

        int mid = (right - left) / 2 + left;
        update(ls, left, mid, pos, val);
        update(rs, mid + 1, right, pos, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root];
        }

        int mid = (right - left) / 2 + left;
        return query(ls, left, mid, from, to) + 
            query(rs, mid + 1, right, from, to);
    }

public:
    void update(int pos, int val) {
        update(1, 1, n, pos, val);
    }

    int query(int left, int right) {
        return query(1, 1, n, left, right);
    }
};

class Solution {
public:
    int countRangeSum(vector<int>& nums, int lower, int upper) {
        int n = nums.size();
        
        //! [前缀和]
        vector<ll> pre(n + 1);
        for (int i = 1; i <= n; i += 1) {
            pre[i] = pre[i - 1] + nums[i - 1];
        }
        //! [前缀和]

        //! [离散化]
        // 去重 + 排序
        set<ll> st;
        for (auto x : pre) {
            st.insert(x);
            st.insert(x - upper);
            st.insert(x - lower);
        }
        // 离散化的映射数组 [1, n]从1开始
        unordered_map<ll, int> index;
        for (int idx = 1; auto x : st) {
            index[x] = idx;
            idx += 1;
        }
        //! [离散化]

        //! 利用线段树做计数缓存
        SegTree tree(index.size());
        int ans = 0;
        for (auto x : pre) {
            int left = index[x - upper];
            int right = index[x - lower];

            ans += tree.query(left, right);
            tree.update(index[x], 1);
        }

        return ans;
    }
};

🏷️654. 最大二叉树

⭐区间求最值(返回下标)

题意与思路

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边子数组前缀上 构建左子树。
  3. 递归地在最大值 右边子数组后缀上 构建右子树。

返回 nums 构建的 *最大二叉树*


本题讲的就是将数组创建一个二叉树的过程,属于分治的思路。

主框架的代码还是比较简单的。

而题意是求区间的最大值,很容易想到用线段树来处理。

但这里题目还需要一个信息是下标,由于本题是所有数值不同的,因此可以使用一个hash来记录数值的下标。

但是此处给出直接维护下标的线段树代码。

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
class SegTree {
private:
    vector<int> tree;   // 记录下标
    vector<int> arr;
    int n;

public:
    SegTree() = default;
    SegTree(vector<int>& arr, int n) {
        this->n = n;
        this->arr = arr;
        this->tree.resize(n * 4);
    }

    void pushUp(int root) {
        if (arr[tree[ls]] > arr[tree[rs]]) {
            tree[root] = tree[ls];
        } else {
            tree[root] = tree[rs];
        }
    }

    void build(int root, int left, int right) {
        if (left == right) {
            tree[root] = left;
            return ;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root); 
    }

    // 注意,返回的是下标
    int query(int root, int left, int right, int from, int to) {
        if (to < left || from > right) {
            return -1;
        }
        if (from <= left && right <= to) {
            return tree[root];
        }

        int mid = (right - left) / 2 + left;
        // 注意,这里返回的是下标
        // 还要转成实际位置上的数值比较后才能返回
        int lIdx = query(ls, left, mid, from, to);
        int rIdx = query(rs, mid + 1, right, from, to);
        if (lIdx == -1) {
            return rIdx;
        }
        else if (rIdx == -1) {
            return lIdx;
        }
        else {
            if (arr[lIdx] > arr[rIdx]) {
                return lIdx;
            } else {
                return rIdx;
            }
        } // if return
    }
};

class Solution {
private:
    int n;
    SegTree tree;
    vector<int> arr;

    TreeNode* dfs(int left, int right) {
        if (left > right) {
            return nullptr;
        }

        // 在[left, right]范围内获取最大值
        int idx = tree.query(1, 1, n, left, right);
        return new TreeNode {
            arr[idx],
            dfs(left, idx - 1),
            dfs(idx + 1, right)
        };
    }

public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        this->n = nums.size();
        arr = nums;
        // [0, n-1] -> [1, n]
        arr.insert(arr.begin(), 0);
        tree = SegTree(arr, n);
        tree.build(1, 1, n);

        // 闭区间
        return dfs(1, n);
    }
};

🏷️699. 掉落的方块

⭐区间覆盖 区间求最值

🍀四种动态开点法

题意与思路

直接说需求,不断落入方块,并会垂直的叠加。

每落一个统计全局最高值。


标准的线段树动态开点的模板题。


方法1 map自动开点

最简单的一种,也是效率最低的一种。

根据自己的语言,选择一种和数组类似操作的数据结构,然后和非动态开点一样的编写。

比如C++中的std::map,std::unordered_map。但这种方式效率很低,查询此时到1e5后就基本不行了。

一些恶心的1e4的也会被卡。但如果不卡的话会非常爽。

另一个缺点就是高强度依赖于所使用语言的特性。

方法2 指针申请法

在记录节点中,维护左右子树节点的地址。

每次使用时检查一下指针是否位空,若空和申请内存。

但是频繁的申请内存也比较慢,且会出现内存碎片等问题。

一些没有CG的语言还需要有自己处理内存的意识,当然一般来说刷题方面可以不释放。

方法3 数组估计法

效率最高和使用难度最大的一种方法。

预先申请一个足够大的数组,当需要使用的时候,再取出对应的点。而这又是最难的一点。

这块空间的计算方式大致如下,若需要保险点,可以增加下一参数。

const int N   ;						  // 区间长度
const int logN;						  // log2(区间长度)的上取整
const int M   ;						  // 调用次数
const int nodeCnt = 4 * M * logN;     // 可以调整参数和加偏移做个保守的计算

这里不做具体解释,要解释完全可以再开一篇文章了。

方法4 指针和数组结合法

建议直接看代码。

这也解释了,为什么笔者把动态开点操作单独拎出来作为一个函数来进行编写。

这里同时兼顾了,数组法的效率和指针法的灵活。

同时,这里也不是严格要求直接把总大小算好。

可以使用分块的思路,比如每次都申请1k个大小,用完再申请一块空间。

Code

效果对比,注意这个效果对比仅做参考:

用时内存
map法528ms174.8 MB
指针法176ms145.4 MB
数组法68ms48.8 MB
指针 & 数组44ms67.9 MB
map
指针法
数组法
指针&数组
#define ls (root << 1)
#define rs (root << 1 | 1)
// 区间覆盖,区间查最值
// 动态开点 map
class SegTree {
private:
    struct Node {
        int val  = 0;
        int lazy = 0;
    };

    // 借助c++中的map自动开点
    unordered_map<int, Node> tree;
    int n = 1e9;

public:
    SegTree(int n) : n(n) {}

    // 维护最值
    void pushUp(int root) {
        tree[root].val = max(tree[ls].val, tree[rs].val);
    }

    // 区间覆盖
    void pushDown(int root) {
        if (tree[root].lazy != 0) {
            tree[ls].val  = tree[root].val;
            tree[ls].lazy = tree[root].lazy;
            tree[rs].val  = tree[root].val;
            tree[rs].lazy = tree[root].lazy;

            tree[root].lazy = 0;
        }
    }

    // 区间覆盖
    void update(int root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return;
        }
        if (from <= left && right <= to) {
            tree[root].val  = val;
            tree[root].lazy = val;
            return;
        }

        pushDown(root);
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root);
        int mid = (right - left) / 2 + left;
        return max(
            query(ls, left, mid, from, to),
            query(rs, mid + 1, right, from, to)
        );
    }

public:  // 统一动态开点时外部调用接口
    void update(int left, int right, int val) {
        update(1, 1, n, left, right, val);
    }

    int query(int left, int right) {
        return query(1, 1, n, left, right);
    }
};

class Solution {
private:    
    static const int N = 1e8 + 1e6 + 1;
    
public:
    vector<int> fallingSquares(vector<vector<int>>& positions) {
        const int n = positions.size();
        SegTree   tree(N);

        vector<int> ans(n);
        for (int i = 0; i < n; i += 1) {
            int len   = positions[i][1];
            int left  = positions[i][0];
            int right = left + len - 1;
            // 查询旧值,再累计长度
            int val = tree.query(left, right);
            tree.update(left, right, val + len);
            // 整个区间的最大值
            ans[i] = tree.query(1, N);
        }

        return ans;
    }
};

🏷️715. Range 模块

⭐区间true|false 区间计数

⭐动态开点(卡内存)

题意与思路

区间全覆盖为true|false,然后区间计数有多少个true


如果能思考出区间覆盖为bool,然后统计长度,那思维上还是比较容易的。

但是本题对C++极度不友好,甚至这个调用量是1e4次。必须得估点。

这里放几个提交

Code

// 手写递增分配器
template<typename Type, size_t N>
class TreeAllocator {
    Type node[N];
    size_t i = 0;

public:
    Type* get() {
        auto p = node + i;
        i += 1;
        new (p) Type{};
        return p;
    }
};

// 区间 true | false
// 区间统计
class SegmentTree {
private:
    struct Node {
        int val  = 0;
        int lazy = -1;

        Node* left  = nullptr;
        Node* right = nullptr;
    };

    TreeAllocator<Node, 500010> alloc;
    Node*     root = nullptr;
    const int n    = 1e9 + 7;

public:
    SegmentTree() {
        root = alloc.get();
    }

private:
    void checkSon(Node* root) {
        if (nullptr == root->left) {
            root->left = alloc.get();
        }
        if (nullptr == root->right) {
            root->right = alloc.get();
        }
    }

    void pushUp(Node* root) {
        root->val = root->left->val + root->right->val;
    }

    // 每次调用 pushDown 首先第一步检查左右子树
    void pushDown(Node* root, int left, int right) {
        checkSon(root);

        if (root->lazy == -1) {
            return;
        }
        // lazy 只有1|0 表示true|false
        const int mid      = left + (right - left) / 2;
        const int leftLen  = mid - left + 1;
        const int rightLen = right - mid;
        
        root->left->val   = leftLen * root->lazy;
        root->right->val  = rightLen * root->lazy;
        root->left->lazy  = root->lazy;
        root->right->lazy = root->lazy;
        root->lazy        = -1;
    }

private:
    // bool val
    // true | false 的全覆盖
    void update(Node* root, int left, int right, int from, int to, bool val) {
        if (from > right || to < left) {
            return;
        }
        if (from <= left && right <= to) {
            root->val  = val * (right - left + 1);
            root->lazy = val;
            return;
        }

        pushDown(root, left, right);
        const int mid = left + (right - left) / 2;
        update(root->left, left, mid, from, to, val);
        update(root->right, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(Node* root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return root->val;
        }

        pushDown(root, left, right);
        const int mid = left + (right - left) / 2;
        return query(root->left, left, mid, from, to) +
               query(root->right, mid + 1, right, from, to);
    }

public:
    void add(int left, int right, int val) {
        update(root, 1, n, left, right, val);
    }

    int ask(int left, int right) {
        return query(root, 1, n, left, right);
    }
};

class RangeModule {
    SegmentTree tree;

public:
    RangeModule() {
    }

    // RangeModule 的所有查询更新是 `left <= x < right`
    // SegmentTree 的所有查询是 `[left, right]`

    void addRange(int left, int right) {
        tree.add(left, right - 1, true);
    }

    bool queryRange(int left, int right) {
        // 查询区间为true的点数是否为区间长度
        return tree.ask(left, right - 1) == (right - left);
    }

    void removeRange(int left, int right) {
        tree.add(left, right - 1, false);
    }
};

🏷️729. 我的日程安排表 I

🏷️731. 我的日程安排表 II

⭐区间计次 区间求次数

⭐小查询次数的动态开点

题意与思路

日程安排表123都是这个模板

课程表2要求计次不能超过1,每次计次累计1点单位。


本题的查询次数比较少,因此直接使用了map的自动开点方式。

请注意本题的左端点是0。并且根节点不能随意定,具体规律未知,因此还是推荐使用传统的指针和数组的动态开点法。

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
// 区间累计
// 区间求最值
class SegTree {
private:
    struct Node {
        int val = 0;
        int lazy = 0;
    };

    unordered_map<int, Node> tree;
    const int n = 1e9 + 10;

public:
    SegTree() {}

    void pushUp(int root) {
        tree[root].val = max(tree[ls].val, tree[rs].val);
    }

    void pushDown(int root) {
        if (tree[root].lazy == 0) {
            return ;
        }

        tree[ls].val  += tree[root].lazy;
        tree[ls].lazy += tree[root].lazy;
        tree[rs].val  += tree[root].lazy;
        tree[rs].lazy += tree[root].lazy;
        
        tree[root].lazy = 0;
    }

    void update(int root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return ;
        }
        if (from <= left && right <= to) {
            tree[root].val  += val;
            tree[root].lazy += val;
            return ;
        }

        pushDown(root);
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root);
        int mid = (right - left) / 2 + left;
        return max(
            query(ls, left, mid, from, to),
            query(rs, mid + 1, right, from, to)
        );
    }

public: 
    // 注意:本题的左端点是0,根节点也不是乱定的
    // 因此,不推荐用传统的下标法,即便本分代码可以ac
    void update(int left, int right, int val) {
        update(1, 0, n, left, right, val);
    }
    
    int query(int left, int right) {
        return query(1, 0, n, left, right);
    }
};

class MyCalendarTwo {
private:
    SegTree tree;

public:
    MyCalendarTwo() {}
    
    // 要求不超过2
    bool book(int start, int end) {
        int left = start, right = end - 1;
        if (tree.query(left, right) >= 2) {
            return false;
        } else {
            // 每次贡献1次
            tree.update(left, right, 1);
            return true;
        }
    }
};

🏷️732. 我的日程安排表 III

🏷️918. 环形子数组的最大和

⭐区间合并 最大子数组和

题意与思路

获取一个环形数组的最大子数组和。

如果不是环形数组,那就是超级经典的53. 最大子数组和

而这里时环形,那么最直接的一种方式就是破换成链,然后固定区间的去获取最大值。


力扣的区间合并的例子不多,且这题没有修改的修改,只有查询,因此就不写update了。

一般一个节点需要至少记录一个区间的左连续状态,右连续状态,整体状态

放在本题,子数组指的是连续单位内元素的数值和。

这里存储4个状态:

  1. 区间最大值
  2. 左侧最大和
  3. 右侧最大和
  4. 区间和

合并时主要有三个松弛因素,其中合并分两种:

  1. 左侧最大和
  2. 右侧最大和
  3. 合并最大和
    1. 左区间全部+右区间左侧
    2. 右区间全部+左区间右侧

而左右的连续状态,也有合并的情况,具体的可以参见下方代码。

Code

#define INF 0x3f3f3f3f
#define ls  (root << 1)
#define rs  (root << 1 | 1)

// 连续子数组的最大和
// 本模板 无update 无lazy
class SegTree {
private:
    // 根据题意,求最大和
    // 默认取-inf,视为非法值
    struct Node {
        int val  = -INF;  // 区间最大和
        int lmax = -INF;  // 左侧连续最大和
        int rmax = -INF;  // 右侧连续最大和
        int sum  = -INF;  // 整合区间的总和
    };

    vector<Node> tree;
    vector<int>  arr;
    int          n;

public:
    SegTree(vector<int>& arr, int n) {
        this->n   = n;
        this->arr = arr;
        this->tree.resize(n * 4);
    }

    void pushUp(int root) {
        // 总和是两端的总和
        tree[root].sum = tree[ls].sum + tree[rs].sum;
        // 左侧连续区间
        // * (只选一边)左区间的左连续最大和
        // * (合并)右区间的左连续最大和 + 左区间的全部总和
        tree[root].lmax = max(tree[ls].lmax, tree[ls].sum + tree[rs].lmax);
        // 右侧连续区间
        // * (只选一边)右区间的右连续最大和
        // * (合并)左区间的右连续最大和 + 右区间的全部总和
        tree[root].rmax = max(tree[rs].rmax, tree[rs].sum + tree[ls].rmax);
        // 最重要的val(区间最大和)
        // * (合并)左区间的右连续最大和 + 右区间的左连续最大和
        // * (只选一边)左区间的val
        // * (只选一边)右区间的val
        tree[root].val = max({
            tree[ls].rmax + tree[rs].lmax, 
            tree[ls].val, 
            tree[rs].val
        });
    }

    void build(int root, int left, int right) {
        // 区间长度为1的单个区间的所有值都是一样的
        if (left == right) {
            tree[root].val = 
            tree[root].lmax = 
            tree[root].rmax =
            tree[root].sum  = 
                arr[left];
            return;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root);
    }

    Node query(int root, int left, int right, int from, int to) {
        // 无效范围返回一个无效值
        if (from > right || to < left) {
            return Node{};
        }
        if (from <= left && right <= to) {
            return tree[root];
        }

        int  mid   = (right - left) / 2 + left;
        Node lNode = query(ls, left, mid, from, to);
        Node rNode = query(rs, mid + 1, right, from, to);

        Node ans;
        // 同pushUp的维护规则
        ans.val  = max({
            lNode.rmax + rNode.lmax, 
            lNode.val, 
            rNode.val
        });
        ans.lmax = max(lNode.lmax, lNode.sum + rNode.lmax);
        ans.rmax = max(rNode.rmax, rNode.sum + lNode.rmax);
        // 区间总和在此处非核心维护信息
        // ans.sum = 0;
        return ans;
    }
};

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        const int n = nums.size();
        const int N = n * 2;

        // 破环成链
        vector<int> arr(N + 1);
        for (int i = 1; i <= n; i += 1) {
            arr[i] = arr[i + n] = nums[i - 1];
        }

        SegTree tree(arr, N);
        tree.build(1, 1, N);

        // 遍历新链,搜寻区间长度是原数组长度
        int ans = -INF;
        for (int i = 1; i <= n; i += 1) {
            ans = max(ans, tree.query(1, 1, N, i, i + n - 1).val);
        }

        return ans;
    }
};

🏷️933. 最近的请求次数

⭐区间累计 区间求和

⭐动态开点(卡内存)

题意与思路

要求在每个时间点上做一个标记,再统计区间[t - 3000, t]之间的计数。


很明显的的区间维护型问题。

但是本题限定了t的访问时递增的,因此可以用队列来处理。

但是本文是线段树专题,还是会用线段树来处理,并且下面的线段树是可以处理非递增和重复的访问的。

Code

// 区间累计
// 区间求和
class SegTree {
private:
    struct Node {
        int val  = 0;
        int lazy = 0;
        int ls   = -1;
        int rs   = -1;
    };

    const int n = 1e9 + 1;
    int tot = 0;
    vector<Node> tree;

public:
    SegTree() : tree(600000) {}

    void check(int root) {
        if (tree[root].ls == -1) {
            tree[root].ls = ++tot;
        }
        
        if (tree[root].rs == -1) {
            tree[root].rs = ++tot;
        }
    }

    void pushUp(int root) {
        int ls = tree[root].ls;
        int rs = tree[root].rs;
        tree[root].val = tree[ls].val + tree[rs].val;
    }

    void pushDown(int root, int left, int right) {
        check(root);

        int mid = (right - left) / 2 + left;
        int lLen = mid - left + 1;
        int rLen = right - mid;
        if (tree[root].lazy != 0) {
            int ls = tree[root].ls;
            int rs = tree[root].rs;

            tree[ls].val  += lLen * tree[root].lazy;
            tree[ls].lazy += tree[root].lazy;
            tree[rs].val  += rLen * tree[root].lazy;
            tree[rs].lazy += tree[root].lazy;

            tree[root].lazy = 0;
        }
    }

    void update(int root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return ;
        }
        if (from <= left && right <= to) {
            tree[root].val  += (right - left + 1) * val;
            tree[root].lazy += val;
            return ;
        }

        pushDown(root, left, right);
        int ls = tree[root].ls;
        int rs = tree[root].rs;
        
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root, left, right);
        int ls = tree[root].ls;
        int rs = tree[root].rs;

        int mid = (right - left) / 2 + left;
        return query(ls, left, mid, from, to) +
            query(rs, mid + 1, right, from, to);
    }

public:
    void update(int left, int right, int val) {
        update(0, 1, n, left, right, val);
    }

    int query(int left, int right) {
        return query(0, 1, n, left, right);
    }
};

class RecentCounter {
private:
    SegTree tree;

public:
    RecentCounter() {
    }
    
    // 题目保证了t是严格递增的
    // 但是本线段树可以应对`不递增`和`重复`的情况
    int ping(int t) {
        tree.update(t, t, 1);
        return tree.query(max(1, t - 3000), t);
    }
};

🏷️1109. 航班预订统计

⭐线段树处理差分

题意与思路

这是一道标准的差分题目,也可以用线段树来处理

本题属于区间累计,区间求和

注意本题要开longlong

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
class SegTree {
private:
    struct Node {
        long long val  = 0;
        long long lazy = 0;
    };

    int n;
    vector<Node> tree;

public:
    // 可以理解为build全是0
    SegTree(int n) : n(n), tree(n * 4) {}

    void pushUp(int root) {
        tree[root].val = tree[ls].val + tree[rs].val;
    }

    void pushDown(int root, int left, int right) {
        int mid  = (right - left) / 2 + left;
        int lLen = mid - left + 1;
        int rLen = right - mid;
        if (tree[root].lazy != 0) {
            tree[ls].val  += tree[root].lazy * lLen;
            tree[ls].lazy += tree[root].lazy;
            tree[rs].val  += tree[root].lazy * rLen;
            tree[rs].lazy += tree[root].lazy;
            
            tree[root].lazy = 0;
        }
    }

    void update(int root, int left, int right, int from, int to, long long val) {
        if (from > right || to < left) {
            return ;
        }
        if (from <= left && right <= to) {
            tree[root].val  += val * (right - left + 1);
            tree[root].lazy += val;
            return ;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        return query(ls, left, mid, from, to) +
            query(rs, mid + 1, right, from, to);
    }
};

class Solution {
public:
    vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
        SegTree tree(n);
        // 区间累计
        for (auto&& arr : bookings) {
            int left  = arr[0];
            int right = arr[1];
            int val   = arr[2];
            tree.update(1, 1, n + 1, left, right, val);
        }

        vector<int> ans(n);
        // 区间求和
        for (int i = 1; i <= n; i += 1) {
            ans[i - 1] = tree.query(1, 1, n + 1, i, i);
        }
        return ans;
    }
};

🏷️1146. 快照数组

⭐可持久化线段树

题意与思路

实现支持下列接口的「快照数组」- SnapshotArray:

  • SnapshotArray(int length) - 初始化一个与指定长度相等的 类数组 的数据结构。初始时,每个元素都等于 0
  • void set(index, val) - 会将指定索引 index 处的元素设置为 val
  • int snap() - 获取该数组的快照,并返回快照的编号 snap_id(快照号是调用 snap() 的总次数减去 1)。
  • int get(index, snap_id) - 根据指定的 snap_id 选择快照,并返回该快照指定索引 index 的值。

参见:P3919 【模板】可持久化线段树 1(可持久化数组)

如果连可持久化线段树这个词都没听过,想必本代码是很难写出来的。

还有就是处理可持久化线段树,必须得学会动态开点。

当然题解和评论区有很多别的解法可以去参考。

这里对于可持久化线段树,仅对空间方面做一些解释:

空间复杂度:

定义:

  • N为数值量
  • M为操作量

推导:

单次操作开点数量为log(N)

若存在build操作 则为 N*log(N)

考虑操作次数 M*log(N)

因此开点量为 max(N, M)*log(N)

Code

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

/**
 * 空间复杂度:
 * - N为数值量
 * - M为操作量
 * 单次操作开点数量为log(N)
 * 若存在build操作 则为 N*log(N)
 * 考虑操作次数 M*log(N)
 * 因此开点量为 max(N, M)*log(N)
 * ========================
 * 回到本题:
 * 数值范围 1e5
 * 调用次数 1e5
 * 在本题中范围和调用次数一致
 * int(log2(1e5)) = 17
 * 且本题没有build操作,因此max范围就是1e5
 */
constexpr int M = 1e5 + 10;

class SnapshotArray {
private:
    // 数组式动态开点
    struct Node {
        int val;
        int ls;
        int rs;
    } tree[M * 17];  // 动态开点的节点数(:范围 * log)
    int rootVer[M];  // 根节点版本 (:调用次数)
    int tot = 0;     // 开点标记
    int n   = -1;    // 线段树范围

    // 注意返回的需要用引用,不懂的请用宏
    inline int& ls(int seq) {
        return tree[seq].ls;
    }
    inline int& rs(int seq) {
        return tree[seq].rs;
    }

    /**
     * @brief
     * 每次update都会生成一个节点
     * @param pre   前一个版本的序号
     * @param cur   当前版本序号(注意:因为要生成版本,因此传入的是引用)
     * @param left
     * @param right
     * @param pos   单点操作
     * @param val   值覆盖
     */
    void update(int pre, int& cur, int left, int right, int pos, int val) {
        if (pos < left || pos > right) {
            return;
        }
        // 生成新节点
        cur = ++tot;
        // 复制前一个节点的所有信息
        // 包含数值(val)信息 左右节点指向
        tree[cur] = tree[pre];

        // left == right
        if (pos <= left && right <= pos) {
            tree[cur].val = val;
            return;
        }

        int mid = left + (right - left) / 2;
        // 左右共同递归
        update(ls(pre), ls(cur), left, mid, pos, val);
        update(rs(pre), rs(cur), mid + 1, right, pos, val);
    }

    // 单点查询,正常查询即可
    int query(int root, int left, int right, int pos) {
        if (pos < left || pos > right) {
            return 0;
        }
        // left == right
        if (pos <= left && right <= pos) {
            return tree[root].val;
        }

        int mid = left + (right - left) / 2;
        return query(ls(root), left, mid, pos) +
               query(rs(root), mid + 1, right, pos);
    }

private:
    // 可持久化的版本号
    int ver = 0;
    // 快照编号
    int snapCnt = -1;
    // 快照与版本的映射
    unordered_map<int, int> mp;

public:
    // 初始化线段树的结构
    SnapshotArray(int length) {
        n = length;
        memset(tree, 0, sizeof(tree));
        memset(rootVer, 0, sizeof(rootVer));
    }

    // 需要生成新版本
    void set(int index, int val) {
        // [0, n-1] -> [1, n]
        index += 1;
        // 每次更新,生成下一个版本
        update(rootVer[ver], rootVer[ver + 1], 1, n, index, val);
        // 版本号+1
        ver += 1;
    }

    // 需要生成新版本
    int snap() {
        // 快照号+1(注意,基于题意要求起始快照为0)
        snapCnt += 1;
        mp[snapCnt] = ver;
        // 版本也要生成新的
        rootVer[ver + 1] = rootVer[ver];
        ver += 1;
        return snapCnt;
    }

    // 不需要生成新版本
    int get(int index, int snap_id) {
        // [0, n-1] -> [1, n]
        index += 1;
        return query(rootVer[mp[snap_id]], 1, n, index);
    }
};

🏷️1622. 奇妙序列

⭐区间加乘

⭐多状态维护

题意与思路

请你实现三个 API appendaddAllmultAll 来实现奇妙序列。

请实现 Fancy 类 :

  • Fancy() 初始化一个空序列对象。
  • void append(val) 将整数 val 添加在序列末尾。
  • void addAll(inc) 将所有序列中的现有数值都增加 inc
  • void multAll(m) 将序列中的所有现有数值都乘以整数 m
  • int getIndex(idx) 得到下标为 idx 处的数值(下标从 0 开始),并将结果对 10^9 + 7 取余。如果下标大于等于序列的长度,请返回 -1

本质就是区间加乘,然后维护一下区间的数目。

注意,加法乘法的运算的优先等级是不一样的,要理性两者的顺序和逻辑。

本题是对所有数值操作的,要确认自己子区间是否正确,请看本题:洛谷:P3373 【模板】线段树 2

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
using ll          = long long;
constexpr int mod = 1e9 + 7;

class SegTree {
private:
    struct Node {
        ll val     = 0;
        ll lazyAdd = 0; // 加法默认0
        ll lazyMul = 1; // 乘法默认1
    };

    int          n = 1e5;
    vector<Node> tree;

public:
    SegTree() = default;
    SegTree(int n) : n(n), tree(n << 2) {
    }

    // 自底向上更新
    inline void pushUp(int root) {
        tree[root].val = (tree[ls].val + tree[rs].val) % mod;
    }

    // 自顶向下更新
    inline void pushDown(int root, int left, int right) {
        int mid      = (right - left) / 2 + left;
        int leftLen  = mid - left + 1;
        int rightLen = right - mid;
        // 基于一些题目的特殊情况,这里最好判断一下懒惰标记
        tree[ls].val = (tree[ls].val * tree[root].lazyMul + tree[root].lazyAdd * leftLen) % mod;
        tree[rs].val = (tree[rs].val * tree[root].lazyMul + tree[root].lazyAdd * rightLen) % mod;

        tree[ls].lazyMul = (tree[ls].lazyMul * tree[root].lazyMul) % mod;
        tree[rs].lazyMul = (tree[rs].lazyMul * tree[root].lazyMul) % mod;

        tree[ls].lazyAdd = (tree[ls].lazyAdd * tree[root].lazyMul + tree[root].lazyAdd) % mod;
        tree[rs].lazyAdd = (tree[rs].lazyAdd * tree[root].lazyMul + tree[root].lazyAdd) % mod;

        tree[root].lazyAdd = 0;
        tree[root].lazyMul = 1;
    }

    // 加法更新
    void updateAdd(int root, int left, int right, int from, int to, int val) {
        // 两个区间无重合
        if (from > right || to < left) {
            return;
        }
        // 包含在[from, to]的区间才更新
        if (from <= left && to >= right) {
            tree[root].val     = (tree[root].val + val * (right - left + 1)) % mod;
            tree[root].lazyAdd = (tree[root].lazyAdd + val) % mod;
            return;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        updateAdd(ls, left, mid, from, to, val);
        updateAdd(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    // 乘法更新
    void updateMul(int root, int left, int right, int from, int to, int val) {
        // 两个区间无重合
        if (from > right || to < left) {
            return;
        }
        // 包含在[from, to]的区间才更新
        if (from <= left && to >= right) {
            // 加法值也同步更新
            tree[root].val     = (tree[root].val * val) % mod;
            tree[root].lazyMul = (tree[root].lazyMul * val) % mod;
            tree[root].lazyAdd = (tree[root].lazyAdd * val) % mod;
            return;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        updateMul(ls, left, mid, from, to, val);
        updateMul(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    // 查询
    int query(int root, int left, int right, int from, int to) {
        // 两个区间无重合
        if (from > right || to < left) {
            return 0;
        }
        // 完全包含
        if (from <= left && to >= right) {
            return tree[root].val;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        return (query(ls, left, mid, from, to) +
                query(rs, mid + 1, right, from, to)) % mod;
    }
};

class Fancy {
private:
    static const int n = 1e5 + 10;

private:
    SegTree tree;
    int     cnt = 0;

public:
    Fancy() {
        tree = SegTree(n);
    }

    void append(int val) {
        cnt += 1;
        tree.updateAdd(1, 1, n, cnt, cnt, val);
    }

    void addAll(int inc) {
        tree.updateAdd(1, 1, n, 1, cnt, inc);
    }

    void multAll(int m) {
        tree.updateMul(1, 1, n, 1, cnt, m);
    }

    int getIndex(int idx) {
        idx += 1;
        if (idx > cnt) {
            return -1;
        }
        return tree.query(1, 1, n, idx, idx);
    }
};

🏷️1893. 检查是否区域内所有整数都被覆盖

⭐区间覆盖 true or false 区间计数

题意与思路

又是一道差分的题目,但这里的需求是是否覆盖到即可

而最后统计答案是,判断覆盖的总量是否和区间长度一致。


注意区间覆盖的lazy表示是非0/1

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
class SegTree {
private:
    struct Node {
        int val  = 0;
        int lazy = -1;  // true | false 的默认标记是非0/1
    };

    int n;
    vector<Node> tree;

public:
    // 可以理解为build全是0
    SegTree(int n) : n(n), tree(n * 4) {}

    void pushUp(int root) {
        tree[root].val = tree[ls].val + tree[rs].val;
    }

    void pushDown(int root, int left, int right) {
        int mid  = (right - left) / 2 + left;
        int lLen = mid - left + 1;
        int rLen = right - mid;
        if (tree[root].lazy != -1) {
            tree[ls].val  = tree[root].lazy * lLen;
            tree[ls].lazy = tree[root].lazy;
            tree[rs].val  = tree[root].lazy * rLen;
            tree[rs].lazy = tree[root].lazy;
            
            tree[root].lazy = -1;
        }
    }

    void update(int root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return ;
        }
        if (from <= left && right <= to) {
            tree[root].val  = val * (right - left + 1);
            tree[root].lazy = val;
            return ;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        return query(ls, left, mid, from, to) +
            query(rs, mid + 1, right, from, to);
    }
};

class Solution {
public:
    bool isCovered(vector<vector<int>>& ranges, int left, int right) {
        const int n = 50 + 1;
        SegTree tree(n);
        for (auto&& arr : ranges) {
            tree.update(1, 1, n, arr[0], arr[1], 1);
        }
        // 覆盖的数量是否等于[r, l]的长度
        return tree.query(1, 1, n, left, right) == (right - left + 1);
    }
};

🏷️2166. 设计位集

⭐true|false综合题

题意与思路

请直接看原题题意。多数语言的数据结构中因该也有类似的数据结构。

std::bitset - cppreference.com


说白了就是对区间内的各个点,范围的true|false的维护。

🗑️Code

首先,下方链接的代码是ac的

设计位集 - 提交记录 - 力扣(LeetCode)

但是笔者的目的是写一个区间操作版本,但是本题中各个api要么是对单个点的操作,要么是对区间整体的操作。

无法验证写的区间操作是否正确,其实有经验的可以看出这段代码是明显不能争取处理区间操作的。

自己也简单验证过确实是错的,但是不知道怎么改动。

在此希望能有看到的大佬指点一下。

🏷️2213. 由单个字符重复的最长子字符串

⭐区间合并(最长连续字符)

题意与思路

目标是求出区间内的最长连续字符。允许单点修改。


在本题中对区间合并写的比较详细:🔗🏷️918. 环形子数组的最大和


由于区间合并的线段树本身就写起来比较复杂了,这里就使用了单点修改的模式。

就是不用lazy,不用pushDown。

注意本题中合并的要求是所有连接的字符相等。

因此要在区间节点中记录好左右字符。

Code

#define INF (0x3f3f3f3f)
#define ls  (root << 1)
#define rs  (root << 1 | 1)

// 单点更新
// 区间合并
// 本线段树与该题强耦合,维护连续字符的最长长度
class SegTree {
private:
    // 有效长度的节点均为1
    // 注意:char的本质是一个整形数值
    struct Node {
        int lch  = -INF;  // 左侧字符
        int rch  = -INF;  // 右侧字符
        int lmax = -INF;  // 左侧连续最大长度
        int rmax = -INF;  // 右侧连续最大长度
        int sum  = -INF;  // 整个区间的总状态
    };

    int          n;
    string       s;
    vector<Node> tree;

public:
    // s: [1, n]
    SegTree(int n, const string& s) : n(n), s(" " + s), tree(n * 4) {
    }

    //! 区间合并的核心
    // 左右两个子节点贡献给上一层的大范围节点(尝试合并)
    void merge(Node& root, const Node& lnode, const Node& rnode, int left, int right) {
        const int mid  = (right - left) / 2 + left;
        const int lLen = mid - left + 1;
        const int rLen = right - mid;

        // 维护左右字符
        root.lch = lnode.lch;
        root.rch = rnode.rch;

        // 维护左连续
        //! 沿用左区间的左连续
        //! 考虑合并
        //! - 若左区间是个整体
        //! - 能与右区间的左端点相连
        root.lmax = lnode.lmax;
        if (lnode.sum == lLen && lnode.rch == rnode.lch) {
            root.lmax = max(root.lmax, lnode.sum + rnode.lmax);
        }

        // 维护右连续
        //! 沿用右区间的右连续
        //! 考虑合并
        //! - 若右区间是个整体
        //! - 且能与左区间的右端点相连
        root.rmax = rnode.rmax;
        if (rnode.sum == rLen && rnode.lch == lnode.rch) {
            root.rmax = max(root.rmax, rnode.sum + lnode.rmax);
        }

        // 维护整个区间的总状态
        //! 沿用左右两个区间的最值
        //! 考虑合并
        //! - 左区间的右端点 == 右区间的左端点
        root.sum = max(lnode.sum, rnode.sum);
        if (lnode.rch == rnode.lch) {
            root.sum = max(root.sum, lnode.rmax + rnode.lmax);
        }
    }

    void pushUp(int root, int left, int right) {
        merge(tree[root], tree[ls], tree[rs], left, right);
    }

    void build(int root, int left, int right) {
        // 找到了长度为1的单位的节点
        // 左右字符都是同一个
        // 左连续,右连续,总状态都是1单位
        if (left == right) {
            tree[root].lch = tree[root].rch = s[left];
            tree[root].lmax = tree[root].rmax = tree[root].sum = 1;
            return;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root, left, right);
    }

    // 注意,本题是单点操作
    // 不需要lazy和pushDown
    // 这个的递归判断是个人书写风格问题
    void update(int root, int left, int right, int pos, int val) {
        if (pos > right || pos < left) {
            return;
        }
        // 注意,这里的长度为1
        if (pos <= left && right <= pos) {
            tree[root].lch = tree[root].rch = val;
            tree[root].lmax = tree[root].rmax = tree[root].sum = 1;
            return;
        }

        int mid = (right - left) / 2 + left;
        update(ls, left, mid, pos, val);
        update(rs, mid + 1, right, pos, val);
        pushUp(root, left, right);
    }

    Node query(int root, int left, int right, int from, int to) {
        // 返回默认无效值
        if (from > right || to < left) {
            return Node{};
        }
        if (from <= left && right <= to) {
            return tree[root];
        }

        int  mid   = (right - left) / 2 + left;
        Node lnode = query(ls, left, mid, from, to);
        Node rnode = query(rs, mid + 1, right, from, to);
        Node ans;
        merge(ans, lnode, rnode, left, right);
        return ans;
    }
};

class Solution {
public:
    vector<int> longestRepeating(string s, string queryCharacters, vector<int>& queryIndices) {
        const int n = s.size();
        SegTree   tree(n, s);
        tree.build(1, 1, n);

        vector<int> ans(queryIndices.size());
        for (int i = 0; i < queryIndices.size(); i += 1) {
            int pos = queryIndices[i] + 1;
            int val = queryCharacters[i];

            tree.update(1, 1, n, pos, val);
            ans[i] = tree.query(1, 1, n, 1, n).sum;
        }

        return ans;
    }
};

🏷️2407. 最长递增子序列 II

⭐300. 最长递增子序列

题意与思路

给你一个整数数组 nums 和一个整数 k

找到 nums 中满足以下要求的最长子序列:

  • 子序列 严格递增
  • 子序列中相邻元素的差值 不超过 k

请你返回满足上述要求的 最长子序列 的长度。


具体讲解可以看灵神的讲解。

线段树优化 DP(Python/Java/C++/Go)


想要处理该题,需要先掌握普通最长递增子序列中的根据确定值域线段树处理方法,该方法复杂度为

具体到该题,我们需要在查询时把k作为一个影响查询范围因子即可。

这里主要说一个很坑的地方:

就是很多线段树的使用都是搜索范围[1, n]

但LIS的优化是基于前驱状态的缓存维护

比如要查询维护5时,需要先查询5-1=4的状态,再+1

而当要查询1时,1-1=0,但又要限定到线段树的搜寻范围[1, n],会出现max(1, 1-1)=1

就是说我们在查询1的前驱时会出现查到缓存的1。

两种解决方案

  • 特判1的情况
  • 修改查询范围从0开始

Code

#define ls (root << 1)
#define rs (root << 1 | 1)
class SegTree {
private:
    int n;
    // 维护一个最值,且单点更新
    vector<int> tree;

public:
    SegTree(int n) : n(n), tree(n * 4) {}

    void pushUp(int root) {
        tree[root] = max(tree[ls], tree[rs]);
    }

    // 区间覆盖
    // 单点更新
    void update(int root, int left, int right, int pos, int val) {
        if (pos > right || pos < left) {
            return ;
        }
        if (pos <= left && right <= pos) {
            tree[root] = val;
            return ;
        }

        int mid = (right - left) / 2 + left;
        update(ls, left, mid, pos, val);
        update(rs, mid + 1, right, pos, val);
        pushUp(root);
    }

    // 查询最值
    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return INT_MIN / 2;
        }
        if (from <= left && right <= to) {
            return tree[root];
        }

        int mid = (right - left) / 2 + left;
        return max(
            query(ls, left, mid, from, to),
            query(rs, mid + 1, right, from, to)
        );
    }
};

class Solution {
public:
    int lengthOfLIS(vector<int>& nums, int k) {
        // 获取最大值,确定值域
        const int N = *max_element(nums.begin(), nums.end());
        // 注意:下面搜寻的范围是[0, N]
        // 如果max(1...)可能会出现1给1累计的情况
        SegTree tree(N);

        // 回到值域 LIS
        // 用线段树维护递增数值的递增长度
        for (int pos : nums) {
            int left  = max(0, pos - k);    // 往前走k个单位
            int right = max(0, pos - 1);    // 前一个位置的状态
            int val   = tree.query(1, 0, N, left, right);   // 查询比当前pos小的区间的最大递增长度
            // 迭代更新
            // 将当前数值作用域缓存,贡献+1
            // 类似于dp[i-1] + 1
            tree.update(1, 0, N, pos, val + 1);
        }

        // 整范围内的最值
        return tree.query(1, 0, N, 1, N);
    }
};

🏷️2276. 统计区间中的整数数目

⭐区间覆盖 true or false 区间计数

⭐动态开点 指针

题意与思路

其实就是🔗🏷️1893. 检查是否区域内所有整数都被覆盖 的增强版。

增强在需要动态开点。

Code

class SegTree {
    struct Node {
        int   val   = 0;  // 放在本题是区间length或者说count
        int   lazy  = -1;
        Node* left  = nullptr;
        Node* right = nullptr;
    };

    int  n;
    Node root;

public:
    SegTree(int n = 1e9) : n(n) {}

private:
    // 注意,解引用的间接寻址,返回的是引用类型
    void check(Node* root) {
        if (root->left == nullptr) {
            root->left = new Node{};
        }
        if (root->right == nullptr) {
            root->right = new Node{};
        }
    }

    // 左右互斥区间合并
    void pushUp(Node* root) {
        root->val = root->left->val + root->right->val;
    }

    // 整块长度覆盖类型
    void pushDown(Node* root, int left, int right) {
        check(root);

        int mid = (right - left) / 2 + left;
        // lazy 只有0/1 就是全覆盖true||false的改变
        if (root->lazy != -1) {
            root->left->lazy  = root->lazy;
            root->left->val   = root->lazy * (mid - left + 1);
            root->right->lazy = root->lazy;
            root->right->val  = root->lazy * (right - mid);

            root->lazy = -1;
        }
    }

private:
    void update(Node* root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return;
        }
        if (from <= left && right <= to) {
            root->lazy = val;
            root->val  = val * (right - left + 1);
            return;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        update(root->left, left, mid, from, to, val);
        update(root->right, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(Node* root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return root->val;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        return query(root->left, left, mid, from, to) +
               query(root->right, mid + 1, right, from, to);
    }

public:
    void add(int left, int right, int val) {
        update(&root, 1, n, left, right, val);
    }

    int ask(int left, int right) {
        return query(&root, 1, n, left, right);
    }
};

class CountIntervals {
private:
    const int n = 1e9 + 1;
    SegTree   tree;

public:
    CountIntervals() {
        tree = SegTree(n);
    }

    void add(int left, int right) {
        tree.add(left, right, true);
    }

    int count() {
        return tree.ask(1, n);
    }
};

🏷️2286. 以组为单位订音乐会的门票

⭐线段树上二分

⭐多状态维护(最值+前缀和)

题意与思路

给定一个n*m的矩阵,即座位。

  • gather
    • 在maxRow前
    • 需要在同一行连续坐k个人
    • 返回坐下的位置(能则占座)
  • scatter
    • 在maxRow前
    • 坐k个人,可以零散
    • 返回能否坐(能则占座)

开门见山,本题很难。包含知识点众多,细节众多,技巧众多。

多状态维护的线段树练习题:P3373 【模板】线段树 2


首先多状态维护,一般会分别写对应的update和query。并且注意pushup和pushdown的时候都要维护,并注意相互间的影响。

然后关于二分的问题,是在query上根据左右子树的状态进行判断,选择一边进行递归。这就和普通的二分形式非常像。

Code

本代码参考蛙佬的解法:tsreaper-线段树上二分

这里没有写成一个类,因为与题意强耦合。

接口都是单点操作的模式,没有改成楼主平常写的区间式接口,因为蛙佬的代码一直是这么的精炼帅气。当然以后可能会重新封装一下。

#define ls (root << 1)
#define rs (root << 1 | 1)
class BookMyShow {
    using ll = long long;

private:
    struct Node {
        ll maxVal = 0;  // 座位最大值
        ll preSum = 0;  // 座位的前缀和
    };
    vector<Node> tree;

    // 记录清空操作后,被修改的那行还剩多少座位
    ll  remain;
    int n;  // 行数
    int m;  // 列数,每行的起始数量

private:
    // 最值维护
    // 前缀和维护
    void pushUp(int root) {
        tree[root].maxVal = max(tree[ls].maxVal, tree[rs].maxVal);
        tree[root].preSum = tree[ls].preSum + tree[rs].preSum;
    }

    void build(int root, int left, int right) {
        // 每行初始数量都为m
        // 且单个区间的节点前缀和就是m
        if (left == right) {
            tree[root].preSum = tree[root].maxVal = m;
            return;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root);
    }

    // 单点修改,覆盖为 val
    void update(int root, int left, int right, int pos, ll val) {
        if (left == right) {
            tree[root].preSum = tree[root].maxVal = val;
            return;
        }

        int mid = (right - left) / 2 + left;
        if (pos <= mid) {
            update(ls, left, mid, pos, val);
        } else {
            update(rs, mid + 1, right, pos, val);
        }
        pushUp(root);
    }

    // 查询编号最小
    // max >= val 的行
    int query_max(int root, int left, int right, ll val) {
        // 寻找单个行号,最终锁定到长度为1的区间
        if (left == right) {
            // 记录修改后的剩余值
            remain = tree[root].maxVal - val;
            return left;
        }

        int mid = (right - left) / 2 + left;
        // 注意,这里是和左子树比较
        if (tree[ls].maxVal >= val) {
            return query_max(ls, left, mid, val);
        } else {
            return query_max(rs, mid + 1, right, val);
        }
    }

    // 查询编号最小
    // pre >= val的行
    int query_presum(int root, int left, int right, ll val) {
        // 寻找单个行号,最终锁定到长度为1的区间
        if (left == right) {
            remain = tree[root].preSum - val;
            return left;
        }

        int mid = (right - left) / 2 + left;
        // 注意,这里是和左子树比较
        if (tree[ls].preSum >= val) {
            return query_presum(ls, left, mid, val);
        } else {
            // 注意这里,左侧不足所有去右边查
            // 但还要记住减去左侧的量
            return query_presum(rs, mid + 1, right, val - tree[ls].preSum);
        }
    }

public:
    // 搜寻根为1
    // 搜寻范围是[0, n-1]
    int Root  = 1;
    int Left  = 0;
    int Right = 1e9;

    BookMyShow(int n, int m) {
        this->n = n;
        this->m = m;
        tree.resize(n * 4);

        Root  = 1;
        Left  = 0;
        Right = n - 1;
        build(Root, Left, Right);
    }

    // 要求k个均放同一行
    vector<int> gather(int k, int maxRow) {
        // 从根节点查看组大的max都放不下
        // 保证下面获取的行号是有效的
        if (tree[Root].maxVal < k) {
            return {};
        }

        // 查询至少为k的行
        int idx = query_max(Root, Left, Right, k);
        if (idx > maxRow) {
            return {};
        }

        // 把第idx行的数覆盖为remain
        update(Root, Left, Right, idx, remain);
        // 返回坐标
        return {idx, (int)(m - remain - k)};
    }

    /// 用完的行数的递增标记
    int last = -1;
    // 要求k个能放完即可
    bool scatter(int k, int maxRow) {
        // 总量都不足k个
        // 保证下面获取的行号是有效的
        if (tree[Root].preSum < k) {
            return false;
        }

        // 查询前缀
        int idx = query_presum(Root, Left, Right, k);
        if (idx > maxRow) {
            return false;
        }

        // 将[0, idx]的行的数值都改为0
        // 用一个last标记作为缓存,这样可以递增,避免修改
        for (int i = last + 1; i < idx; i += 1) {
            update(Root, Left, Right, i, 0);
        }
        // 更新last
        last = max(last, idx - 1);

        // 针对idx行修改
        update(Root, Left, Right, idx, remain);
        return true;
    }
};

🏷️2569. 更新数组后处理求和查询

⭐区间 true|false 反转

题意与思路

给你两个下标从 0 开始的数组 nums1nums2 ,和一个二维数组 queries 表示一些操作。总共有 3 种类型的操作:

  1. 操作类型 1 为 queries[i] = [1, l, r] 。你需要将 nums1 从下标 l 到下标 r 的所有 0 反转成 1 并且所有 1 反转成 0lr 下标都从 0 开始。
  2. 操作类型 2 为 queries[i] = [2, p, 0] 。对于 0 <= i < n 中的所有下标,令 nums2[i] = nums2[i] + nums1[i] * p
  3. 操作类型 3 为 queries[i] = [3, 0, 0] 。求 nums2 中所有元素的和。

请你返回一个 数组,包含 所有第三种操作类型 的答案。


非常典型的acm题型的提问方式。

注意本题只与nums1有关,nums2本质没什么用。

想要反转true|false。可以利用异或^的性质。

Code

class SegTree {
#define ls (root << 1)
#define rs (root << 1 | 1)

private:
    struct SegTreeNode {
        // root range的总和
        int val = 0;
        // 只有两个状态 0/1
        int lazy = 0;
    };
    vector<int> &arr;
    vector<SegTreeNode> tree;

public:
    SegTree(vector<int>& _arr, int n) : arr(_arr) {
        tree = vector<SegTreeNode>(n << 2);
        build(1, 1, n);
    }

public:
    void pushUp(int root) {
        tree[root].val = tree[ls].val + tree[rs].val;
    }

    void pushDown(int root, int left, int right) {
        int mid = (right - left) / 2 + left;
        int leftLen = mid - left + 1;
        int rightLen = right - mid;
        if (tree[root].lazy) {
            tree[ls].lazy ^= 1;
            tree[ls].val = leftLen - tree[ls].val;

            tree[rs].lazy ^= 1;
            tree[rs].val = rightLen - tree[rs].val;

            tree[root].lazy = 0;
        }
    }

    void build(int root, int left, int right) {
        tree[root].val = tree[root].lazy = 0;
        if (left == right) {
            tree[root].val = arr[left];
            return ;
        }

        int mid = (right - left) / 2 + left;
        build(ls, left, mid);
        build(rs, mid + 1, right);
        pushUp(root);
    }

    // 本题的val没有用到
    void update(int root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return ;
        }
        if (from <= left && right <= to) {
            int len = right - left + 1;
            tree[root].val = len - tree[root].val;
            tree[root].lazy ^= 1;
            return ;
        }
        
        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return 0;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root, left, right);
        int mid = (right - left) / 2 + left;
        return 
            query(ls, left, mid, from, to) + 
            query(rs, mid + 1, right, from, to);
    }
};

class Solution {
public:
    vector<long long> handleQuery(vector<int>& nums1, vector<int>& nums2, vector<vector<int>>& queries) {
        const int n = nums1.size();
        // [0, n - 1] => [1, n]
        nums1.insert(nums1.begin(), 0);
        SegTree tree(nums1, n);

        vector<long long> ans;
        // 本质上与num2数组没关系
        long long sum = reduce(nums2.begin(), nums2.end(), 0LL);
        for (auto&& query : queries) {
            const int type = query[0];
            if (type == 1) {
                int left = query[1] + 1;
                int right = query[2] + 1;
                // 最后一个val参数没用到,占位一下
                tree.update(1, 1, n, left, right, 1);
            } else if (type == 2) {
                // 根据题意维护sum
                sum += 1LL * tree.query(1, 1, n, 1, n) * query[1];
            } else {
                ans.push_back(sum);
            }
        }

        return ans;
    }
};

🏷️2736. 最大和查询

⭐二维偏序模板题

⭐区间维护最值

题意与思路

给你两个长度为 n 、下标从 0 开始的整数数组 nums1nums2 ,另给你一个下标从 1 开始的二维数组 queries ,其中 queries[i] = [xi, yi]

对于第 i 个查询,在所有满足 nums1[j] >= xinums2[j] >= yi 的下标 j (0 <= j < n) 中,找出 nums1[j] + nums2[j]最大值 ,如果不存在满足条件的 j 则返回 -1

返回数组 answer *,*其中 answer[i] 是第 i 个查询的答案。

简单说就是两个维度都要达到要求,并获取达到要求的最值


本题的重点已经不是线段树了。

一个简单的模型是,作为一个二维平面:问query的xy,有多少val的xy的右上方。

我们先确定一个维度,比如拿x排序,这样可以在x的遍历中不用考虑相对关系。然后只要维护关于离散的y的状态即可。

注意本题是维护最值,且无效返回-1。因此节点的默认值和query的默认值设定为-1。不要无脑都写0。

Code

#define ls (root << 1)
#define rs (root << 1 | 1)

// 区间维护最值
class SegTree {
private:
    struct Node {
        int val  = -1;
        int lazy = 0;
    };

    int n = 1e5;
    vector<Node> tree;

public:
    SegTree(int n) : n(n), tree(n << 2) {}

    // 维护最值
    void pushUp(int root) {
        tree[root].val = max(tree[ls].val, tree[rs].val);
    }

    void pushDown(int root) {
        if (tree[root].lazy != 0) {
            tree[ls].val  = max(tree[ls].val, tree[root].val);
            tree[ls].lazy = max(tree[ls].lazy, tree[root].lazy);
            tree[rs].val  = max(tree[rs].val, tree[root].val);
            tree[rs].lazy = max(tree[rs].lazy, tree[root].lazy);

            tree[root].lazy = 0;
        }
    }

    void update(int root, int left, int right, int from, int to, int val) {
        if (from > right || to < left) {
            return;
        }
        if (from <= left && right <= to) {
            tree[root].val  = max(tree[root].val, val);
            tree[root].lazy = max(tree[root].lazy, val);
            return;
        }

        pushDown(root);
        int mid = (right - left) / 2 + left;
        update(ls, left, mid, from, to, val);
        update(rs, mid + 1, right, from, to, val);
        pushUp(root);
    }

    int query(int root, int left, int right, int from, int to) {
        if (from > right || to < left) {
            return -1;
        }
        if (from <= left && right <= to) {
            return tree[root].val;
        }

        pushDown(root);
        int mid = (right - left) / 2 + left;
        return max(query(ls, left, mid, from, to),
            query(rs, mid + 1, right, from, to));
    }
};

class Solution {
public:
    vector<int> maximumSumQueries(vector<int>& nums1, vector<int>& nums2, vector<vector<int>>& queries) {
        const int n = nums1.size();
        const int q = queries.size();

        //! [每个idx位置的和]
        vector<int> val(n);
        for (int i = 0; i < n; i += 1) {
            val[i] = nums1[i] + nums2[i];
        }
        //! [每个idx位置的和]

        //! [离散化]
        // map自带去重和排序功能
        map<int, int> mp;
        for (int i = 0; i < n ; i += 1) {
            mp[nums1[i]] = 0;
            mp[nums2[i]] = 0;
        }
        for (int i = 0; i < q; i += 1) {
            mp[queries[i][0]] = 0;
            mp[queries[i][1]] = 0;
        }
        // mp默认递增,范围[1, n]
        for (int idx = 1; auto&& [k, v] : mp) {
            v = idx ++;
        }
        for (int i = 0; i < n; i += 1) {
            nums1[i] = mp[nums1[i]];
            nums2[i] = mp[nums2[i]];
        }
        for (int i = 0; i < q; i += 1) {
            queries[i][0] = mp[queries[i][0]];
            queries[i][1] = mp[queries[i][1]];
        }
        //! [离散化]

        //! [组合数据]
        // 作为一个二维平面:问query的xy,有多少val的xy的右上方
        // {x, y, 属性}
        struct Node {
            int x;
            int y;
            int k;
            bool operator < (const Node& rhs) const {
                // 优先使x轴逆序
                if (x != rhs.x) {
                    return x > rhs.x;
                }
                // 数值在查询前先处理
                return k < rhs.k;
            }
        };
        vector<Node> arr;
        // 对于点,属性为  :val
        for (int i = 0; i < n; i += 1) {
            // 这里将val作为负数,使得val点排序在前
            arr.push_back({nums1[i], nums2[i], -val[i]});
        }
        // 对于查询,属性为:第几号查询(离线算法)
        for (int i = 0; i < q; i += 1) {
            arr.push_back({queries[i][0], queries[i][1], i});
        }
        sort(arr.begin(), arr.end());
        //! [组合数据]

        //! [离线查询]      
        // 所有范围内的点都可能缓存
        const int N = mp.size();
        // 注意,本题的线段树的默认值,和query的非法值是-1
        SegTree tree(N);
        vector<int> ans(q, -1);
        // 已经按照x轴递减了
        for (auto node : arr) {
            // 是数值点,缓存
            if (node.k < 0) {
                tree.update(1, 1, N, node.y, node.y, -node.k);
            } 
            // 是查询点,记录答案
            // 查询比当前查询点大的点
            else {
                ans[node.k] = tree.query(1, 1, N, node.y, N);
            }
        }
        //! [离线查询]

        return ans;
    }
};

END

🔗ref

评论 (5)