本文主要记录力扣中出现的能够用线段树处理的题目。
线段树是一种非常强大的数据结构。但是很多用线段树能处理的题也会有别的解法。
线段树的核心难点在于如何对区间的性质状态进行维护和转移,这点和动态规划很像。
注意:本文不适合连一点线段树概念都没有的小白观看。
由于力扣一直将我的帖子不过审核,因此这里就不贴关于线段树的其他学习内容了。
一般线段树可能需要的状态标记不止一个,因此推荐使用结构体来封装。因为在结构体中加一个变量非常简单,重新多开一个数组会比较麻烦。
关于动态开点的线段树,通常以下几种方式
借助语言的数据结构自动开点
指针new节点
提前估点,用数组下标指向
其实基础好的话这两种方式可以很快掌握。因此这绝对不是你不会处理线段树题目的绊脚石。因为线段树的核心就在状态维护上,而不是怎么开点。不要把这个作为学不好的借口。
由于线段树的一些接口的部分参数是固定的,其实可以在做一个间接层的封装。而且在动态开点的题目中,可以隔离内部使用的实际数据结构。
大多数线段树的code都非常适合抽离出来作为模板。基于面向对象语言的话,完全可以写成一个类。
一方面避免与题目或者说业务代码有融合,另一方面可以直接cv掉这个class并更清晰的进行修改。
线段树的整体的框架比较固定,本文中出现的code均为楼主本人个人的模板和风格。读者应该将重点放在线段树的状态维护上!
除了一些比较裸的题,比较难的线段树的题是会配合另一种算法,做一些数值状态维护操作的题。可见在很多难题中线段树只是作为一种数据维护的辅助方式。
在本文中会看到一些类似甚至相同的维护操作。可能是调用主题函数需要不同,一方面是可以多验证下模板,另一方面是我已经写好了舍不得删掉了。
有关联的题有很多,这里放一些比较有代表性质的题。
| 需求 | 典型例题 |
|---|---|
| 单点操作 | 🏷️307. 区域和检索 - 数组可修改 |
| 加权|覆盖 | 🏷️307. 区域和检索 - 数组可修改 |
| 动态开点 | 🏷️699. 掉落的方块 |
| 区间合并 | 🏷️918. 环形子数组的最大和 🏷️2213. 由单个字符重复的最长子字符串 |
| 可持久化线段树 | 🏷️1146. 快照数组 |
🔗完善版:🏷️918. 环形子数组的最大和
更完善代码见918题,但是918也只有build和query,没有修改的update操作。
本题是一道好题,此处给出多种解法方案的具体代码。
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
这是一个很经典的模板例题,区间求最值。
但是这里没有update操作,因此不用lazy和pushDown。
最后注意,这种与最值相关的题目,返回的默认值都需要注意,不是无脑的0,也不是无脑的不是0。
#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;
}
};
本题是一道好题,此处给出多种解法方案的具体代码。
给你一个数组 nums ,请你完成两类查询。
nums 下标对应的值nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 ,其中 left <= right典型的单点操作型。单点操作可以不用lazy和pushDown,相对比较简单。
但是此处还是给出区间操作的代码。因为单点就是一个长度为1的区间。
同时本题的题面是覆盖,但是实际操作可以使用加权的方式,此处也会给出。
加权式
// 单点加权,区间求和
#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);
}
};给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
只读一遍可以说题意非常难懂,这里简答描述下,并举例:
输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。能构成的所有区间,且在[lower, upper]范围内的是:
因此,说白了就是要枚举所有区间的和,然后判断是否在范围内
对于快速求和,很容易想到前缀和来处理。
目的是统计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)这是一种比较常见的模型,固定遍历一个序列,并查询历史缓存,统计结束后在将当前记录到缓存中。经典的两数之和也是这中思路,但是要想到这种转换在不同的复杂题中并不简单。
最后注意,本题的数据量很大,因此需要离散化。
#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;
}
};给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
nums 中的最大值。返回 nums 构建的 *最大二叉树* 。
本题讲的就是将数组创建一个二叉树的过程,属于分治的思路。
主框架的代码还是比较简单的。
而题意是求区间的最大值,很容易想到用线段树来处理。
但这里题目还需要一个信息是下标,由于本题是所有数值不同的,因此可以使用一个hash来记录数值的下标。
但是此处给出直接维护下标的线段树代码。
#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);
}
};
直接说需求,不断落入方块,并会垂直的叠加。
每落一个统计全局最高值。
标准的线段树动态开点的模板题。
方法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个大小,用完再申请一块空间。
效果对比,注意这个效果对比仅做参考:
| 用时 | 内存 | |
|---|---|---|
| map法 | 528ms | 174.8 MB |
| 指针法 | 176ms | 145.4 MB |
| 数组法 | 68ms | 48.8 MB |
| 指针 & 数组 | 44ms | 67.9 MB |
#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;
}
};区间全覆盖为true|false,然后区间计数有多少个true
如果能思考出区间覆盖为bool,然后统计长度,那思维上还是比较容易的。
但是本题对C++极度不友好,甚至这个调用量是1e4次。必须得估点。
这里放几个提交
// 手写递增分配器
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);
}
};日程安排表123都是这个模板
课程表2要求计次不能超过1,每次计次累计1点单位。
本题的查询次数比较少,因此直接使用了map的自动开点方式。
请注意本题的左端点是0。并且根节点不能随意定,具体规律未知,因此还是推荐使用传统的指针和数组的动态开点法。
#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;
}
}
};
获取一个环形数组的最大子数组和。
如果不是环形数组,那就是超级经典的53. 最大子数组和
而这里时环形,那么最直接的一种方式就是破换成链,然后固定区间的去获取最大值。
力扣的区间合并的例子不多,且这题没有修改的修改,只有查询,因此就不写update了。
一般一个节点需要至少记录一个区间的左连续状态,右连续状态,整体状态。
放在本题,子数组指的是连续单位内元素的数值和。
这里存储4个状态:
合并时主要有三个松弛因素,其中合并分两种:
而左右的连续状态,也有合并的情况,具体的可以参见下方代码。
#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;
}
};要求在每个时间点上做一个标记,再统计区间[t - 3000, t]之间的计数。
很明显的的区间维护型问题。
但是本题限定了t的访问时递增的,因此可以用队列来处理。
但是本文是线段树专题,还是会用线段树来处理,并且下面的线段树是可以处理非递增和重复的访问的。
// 区间累计
// 区间求和
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);
}
};这是一道标准的差分题目,也可以用线段树来处理
本题属于区间累计,区间求和
注意本题要开longlong
#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;
}
};
实现支持下列接口的「快照数组」- 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(可持久化数组)
如果连可持久化线段树这个词都没听过,想必本代码是很难写出来的。
还有就是处理可持久化线段树,必须得学会动态开点。
当然题解和评论区有很多别的解法可以去参考。
这里对于可持久化线段树,仅对空间方面做一些解释:
空间复杂度:
定义:
推导:
单次操作开点数量为log(N)
若存在build操作 则为 N*log(N)
考虑操作次数 M*log(N)
因此开点量为 max(N, M)*log(N)
#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);
}
};请你实现三个 API append,addAll 和 multAll 来实现奇妙序列。
请实现 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
#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);
}
};
又是一道差分的题目,但这里的需求是是否覆盖到即可
而最后统计答案是,判断覆盖的总量是否和区间长度一致。
注意区间覆盖的lazy表示是非0/1
#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);
}
};请直接看原题题意。多数语言的数据结构中因该也有类似的数据结构。
std::bitset - cppreference.com
说白了就是对区间内的各个点,范围的true|false的维护。
首先,下方链接的代码是ac的
但是笔者的目的是写一个区间操作版本,但是本题中各个api要么是对单个点的操作,要么是对区间整体的操作。
无法验证写的区间操作是否正确,其实有经验的可以看出这段代码是明显不能争取处理区间操作的。
自己也简单验证过确实是错的,但是不知道怎么改动。
在此希望能有看到的大佬指点一下。
目标是求出区间内的最长连续字符。允许单点修改。
在本题中对区间合并写的比较详细:🔗🏷️918. 环形子数组的最大和
由于区间合并的线段树本身就写起来比较复杂了,这里就使用了单点修改的模式。
就是不用lazy,不用pushDown。
注意本题中合并的要求是所有连接的字符相等。
因此要在区间节点中记录好左右字符。
#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;
}
};给你一个整数数组 nums 和一个整数 k 。
找到 nums 中满足以下要求的最长子序列:
k 。请你返回满足上述要求的 最长子序列 的长度。
具体讲解可以看灵神的讲解。
想要处理该题,需要先掌握普通最长递增子序列中的根据确定值域线段树处理方法,该方法复杂度为。
具体到该题,我们需要在查询时把k作为一个影响查询范围因子即可。
这里主要说一个很坑的地方:
就是很多线段树的使用都是搜索范围
[1, n]但LIS的优化是基于前驱状态的缓存维护
比如要查询维护5时,需要先查询
5-1=4的状态,再+1而当要查询1时,
1-1=0,但又要限定到线段树的搜寻范围[1, n],会出现max(1, 1-1)=1就是说我们在查询1的前驱时会出现查到缓存的1。
两种解决方案
- 特判1的情况
- 修改查询范围从0开始
#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);
}
};其实就是🔗🏷️1893. 检查是否区域内所有整数都被覆盖 的增强版。
增强在需要动态开点。
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);
}
};给定一个n*m的矩阵,即座位。
开门见山,本题很难。包含知识点众多,细节众多,技巧众多。
多状态维护的线段树练习题:P3373 【模板】线段树 2
首先多状态维护,一般会分别写对应的update和query。并且注意pushup和pushdown的时候都要维护,并注意相互间的影响。
然后关于二分的问题,是在query上根据左右子树的状态进行判断,选择一边进行递归。这就和普通的二分形式非常像。
本代码参考蛙佬的解法: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;
}
};给你两个下标从 0 开始的数组 nums1 和 nums2 ,和一个二维数组 queries 表示一些操作。总共有 3 种类型的操作:
queries[i] = [1, l, r] 。你需要将 nums1 从下标 l 到下标 r 的所有 0 反转成 1 并且所有 1 反转成 0 。l 和 r 下标都从 0 开始。queries[i] = [2, p, 0] 。对于 0 <= i < n 中的所有下标,令 nums2[i] = nums2[i] + nums1[i] * p 。queries[i] = [3, 0, 0] 。求 nums2 中所有元素的和。请你返回一个 数组,包含 所有第三种操作类型 的答案。
非常典型的acm题型的提问方式。
注意本题只与nums1有关,nums2本质没什么用。
想要反转true|false。可以利用异或^的性质。
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;
}
};给你两个长度为 n 、下标从 0 开始的整数数组 nums1 和 nums2 ,另给你一个下标从 1 开始的二维数组 queries ,其中 queries[i] = [xi, yi] 。
对于第 i 个查询,在所有满足 nums1[j] >= xi 且 nums2[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。
#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;
}
};