题目背景:
1.该题来自Leetcode--2569. 困难题。
2.该题对于没有系统学习的同志来讲确实是一道困难题。但如果系统了解过一些数据结构。那么这道题难度急转直下,是一道简单的模板题(稍加变形)。
闲言少叙,书归正传!
题目描述:
我们此前说过这道题在经过系统地了解数据结构后,我们会很快得到需要使用“线段树”来进行设计。
在这里,我们借此机会来系统了解(记录)“线段树”的相关知识。
前置知识
线段树
线段树的概念
线段树擅长处理区间问题。形如下图,线段树是一个完美二叉树,换言之可以使用数组存放相关数组。树上的每一个节点维护一个区间,而根维护整个区间。一般,我们的子节点维护父节点二分后的其中一个区间。对此我们可以在O(logN)的时间内维护区间。
基于线段树的RMQ问题
为了更好地理解线段树的概念和操作,我们引入经典的题目来讲解相关操作。
基于线段树的RMQ查询
像这样,就算要查询比较大的区间,也会因为较为靠上的节点范围较大,而只需要查找较小的节点个数就可以获取最后的答案。
PS:注意在没有交集时,返回不影响答案的值。例如:在求和中返回0,求积时返回1。
基于线段树的RMQ更新
如上图,我们将a0值进行更改,我们只需要访问4个节点。也就是子区间访问父区间的过程。
基于线段树的RMQ的复杂度分析
代码实现
const int MAX_N = 1 << 17;
//线段树
int dat[2 * MAX_N - 1];
void init(int _n) {
int n = 1;
while(n < _n) n *= 2;//为了方便将_n扩展到2的幂
for(int i = 0; i <= 2 * n - 1; ++i) dat[i] = INT_MAX;
return ;
}
//创建线段树--编号从1开始,区间设计[l, r)
void build(int id, int l, int r, int nums[]) {
if (l - r == 1) {
dat[id] = nums[l];
return ;
}
int mid = (l + r) / 2; //部分版本int mid = (l + r) >> 1;
build(2 * id, l, mid, nums);
bulid(2 * id, mid, r, nums);
dat[id] = std::min(dat[2 * id], dat[2 * id + 1]);
return ;
}
//更新第k个值--可以计算得到 id = k + n;
void updata(int id, int l, int r, int k, int x) {
if(l == k && k + 1 == r) {
dat[id] = std::min(dat[id], x);
return;
}
if(k < l || k >= r) return INT_MAX;
if(l <= k && k < r) {
int mid = (l +r) / 2;
updata(2 * id, l, mid, k, x);
updata(2 * id + 1, mid, r, k, x);
dat[id] = std::min(dat[2 * id], dat[2 * id + 1]);
}
}
//从编号从0起
void updata2(int k, int x) {
int id = k + n - 1;
dat[id] = x;
while(id > 1) {
id = id / 2;
dat[id] = std::min(dat[2 * id], dat[2 * id + 1]);
}
}
//在编号区间id中查询[q_l, q_r)
int query(int id, int l, r, int q_l, int q_r) {
if(l <= q_l && q_r <= r) {//查询区间包含编号区间
return dat[id];
}
if(q_l >= r || q_r <= l) {
return INT_MAX;
}
int mid = (l + r) / 2;
int vl = query(2 * id, l, mid, q_l, q_r);
int v2 = query(2 * id + 1, mid, r, q_l, q_r);
retuen dat[id] = std::min(v1, v2);
}
特此说明:
在updata函数中,我们说明过,如果更新第k个值可以直接计算得到对应的编号区间id = k + n - 1;
至于为何成立这涉及到在线段树中,所有节点和为2*n - 1;其中n为偶数,最后一行会有n个节点,那么前面就有n-1个节点。也就是说在偶数情况下,无论编号从0开始还是1开始都是正确的。但是一旦变成奇数,那么编号从1开始就会产生错误。
二叉索引树
二叉索引树看似高大上,其实它一点也不高级。它有另外一个名字--树状数组。说白了,也就是线段树的空间优化。
我们以和为例子来进行讲解。
我们将这些不要的灰色点去掉,就得到了树状数组。
同时我们给出树状数组每一个节点的编号的二进制编码。
BIT求和
BIT更新
代码实现
int sum(int i) {
int sum = 0;
while(i) {
sum += dat[i];
i -= i & (-i);
}
rteurn sum;
}
//在第i个值上加x
void add(int i, int x) {
while(i <= n) {
dat[i] += x;
i += i & (-i);
}
return ;
}
总结
讲解完我们的前置知识,我们先来浅谈一下线段树和树状数组。
树状数组擅长处理前缀,而线段树擅长处理区间。而值得一提的是,我们可以使用presum[i] - presum[j]来求取[i, j]的区间和。所以,在一般情况下树状数组也擅长处理区间。
那么,我们不禁发问!树状数组就一定何时何地都比线段树优秀吗?
答案是否定的,树状数组的优秀有一个前提条件:对一个值进行更迭。
但是,如果我们需要对区间内的多个值进行更迭。那么树状数组就需要一个个的遍历更新,而每次更新需要O(logN)的时间。所以最终的更新时间会降至O(NlogN)。那么,为了保证树状数组的高效。我们需要转求与线段树。
那么为了保证线段树的高效性,我们可以给出两个策略。
如果对某一区间内的所有值都进行统一的更迭操作,那么我们可以使用另外的数组来存放某一区间共同加上的值。
如果对某一区间内的部分值更迭,那么我们可以采用“缓式评估”策略。
视野收束
那么我们回到此道题目上,我们看到我们需要处理的是区间问题。同时呢,他是对我们的某一区间的部分值进行更迭,所以我们在此选择使用线段树来进行维护关系。
同时,我们选择使用了“缓式评估”策略。说得通俗一点就是“摆烂行为”。
AC代码
class Solution {
private:
struct SegNode {
int l, r, sum;
bool lazytag;
SegNode() {
this->l = 0;
this->r = 0;
this->sum = 0;
this->lazytag = false;
}
};
//线段树区间设计[)
class SegmentTree {
private:
vector<SegNode> arr;
public:
SegmentTree(vector<int>& nums) {
int n = nums.size();
arr.resize(4 * n + 1);
build(1, 0, n, nums);
}
/*构建线段树*/
void build(int id, int l, int r, vector<int>& nums) {
arr[id].l = l;
arr[id].r = r;
arr[id].lazytag = false;
if (r - l == 1) {
arr[id].sum = nums[l];
return;
}
int mid = (l + r) >> 1;
build(2 * id, l, mid, nums);
build(2 * id + 1, mid, r, nums);
arr[id].sum = arr[2 * id].sum + arr[2 * id + 1].sum;
return;
}
/*区间求和 [)*/
int sumRange(int l, int r) {
return query(1, l, r);
}
int query(int id, int l, int r) {
if (l <= arr[id].l && arr[id].r <= r) {//包含编号区间
return arr[id].sum;
}
else if (arr[id].l > r || arr[id].r <= l) {//不包含编号区间
return 0;
}
//部分包含
int res = 0;
if (arr[2 * id].r > l) {
res += query(2 * id, l, r);
}
if (arr[2 * id + 1].l < r) {
res += query(2 * id + 1, l, r);
}
return res;
}
//反转区间值,按照题意传入[l, r],而modify区间设计为[)
void reverseRange(int l, int r) {
modify(1, l, r + 1);//注意,需要变化操作,原因已标注
return;
}
//修改区间,区间设计[)
void modify(int id, int l, int r) {
if (l <= arr[id].l && arr[id].r <= r) {
arr[id].sum = arr[id].r - arr[id].l - arr[id].sum;
arr[id].lazytag = !arr[id].lazytag;
return;
}
pushdown(id);//向下更新--缓式评估
int mid = (arr[id].l + arr[id].r) >> 1;
if (arr[2 * id].r > l) {
modify(2 * id, l, r);
}
if (arr[2 * id + 1].l < r) {
modify(2 * id + 1, l, r);
}
arr[id].sum = arr[2 * id].sum + arr[2 * id + 1].sum;
return;
}
void pushdown(int id) {
if (arr[id].lazytag) {
int que_l = 2 * id;
int que_r = que_l + 1;
arr[que_l].lazytag = !arr[que_l].lazytag;
arr[que_l].sum = arr[que_l].r - arr[que_l].l - arr[que_l].sum;
arr[que_r].lazytag = !arr[que_r].lazytag;
arr[que_r].sum = arr[que_r].r - arr[que_r].l - arr[que_r].sum;
arr[id].lazytag = false;
}
}
};
public:
vector<long long> handleQuery(vector<int>& nums1, vector<int>& nums2, vector<vector<int>>& queries) {
int n = nums1.size();
int m = queries.size();
SegmentTree tree(nums1);
vector<long long> ans;
long long sum = 0;
for (int i = 0; i < nums2.size(); ++i) {
sum += (long long)nums2[i];
}
for (int i = 0; i < m; ++i) {
if (queries[i][0] == 1) {
tree.reverseRange(queries[i][1], queries[i][2]);
}
else if (queries[i][0] == 2) {
sum += (long long)tree.sumRange(0, n) * queries[i][1];
}
else {
ans.emplace_back(sum);
}
}
return ans;
}
};