\(AcWing\) \(245\). 你能回答这些问题吗
一、题目描述
给定长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:
1 x y
,查询区间 \([x,y]\) 中的 最大连续子段和,即 \(\displaystyle \max_{x≤l≤r≤y}{\sum_{i=l}^rA[i]}\)。
2 x y
,把 \(A[x]\) 改成 \(y\)。
对于每个查询指令,输出一个整数表示答案。
输入格式
第一行两个整数 \(N,M\)。
第二行 \(N\) 个整数 \(A[i]\)。
接下来 \(M\) 行每行 \(3\) 个整数 \(k,x,y,k=1\) 表示查询(此时如果 \(x>y\),请交换 \(x,y\)),\(k=2\) 表示修改。
输出格式
对于每个查询指令输出一个整数表示答案。
每个答案占一行。
数据范围
\(N≤500000,M≤100000,
−1000≤A[i]≤1000\)
二、题目解析
单点修改+带跨区间查询:最长连续子段和 无懒标
本题依据题意需要进行 单点修改,因此无需懒标记 \(pushdown\) 操作,只需要 \(pushup\) 操作即可。
对于查询区间内部的最大子段和,我们需要想想每个节点内部需要存储哪些信息 才能保证儿子节点向父亲节点顺利pushup
传递信息。(这是我们面对线段树题目时的一个很重要的思考点)
设线段树中的节点为\(node\),我们显然最先应该存储 区间的左、右端点:\(l、r\),因为线段树本质上是一棵由区间作为节点的二叉树。
同时,依据题意 还应该存区间内部的最大连续子段和,我们用total_max
表示,简写为tmax
。
我们来想一下,只存这些信息就够了吗?只利用当前存储的信息,能够实现 父节点的最大连续子段和tmax
由 两个儿子节点的最大连续子段和 求出吗?
显然是不够的。
原因:两个 儿子节点 各自tmax
可能会出现完全处于区间内部的情况,且不包含边界,然而父亲节点的tmax
可能会出现 横跨两个区间的情况 。就好比下图所示(红色括号所包括的范围表示节点的tmax
):
对于像上图的这种 父节点tmax
横跨两个区间 的这种情况,我们其实需要再 额外存储两个信息:
-
① 以左儿子区间右端点为起点 向左 的最大后缀和
-
② 以右儿子区间左端点为起点 向右 的最大前缀和
小结一下,也就是说每个节点还需要存储它的 最大前缀和 和 最大后缀和,这样, 父节点 横跨两区间 的最大连续子段和 = 左儿子的最大后缀和 + 右儿子的最大前缀和。
(左右儿子区间完全独立,两者没有任何关系,没有限制,左右两区间取max
即为父节点tmax
)
我们设节点最大前缀和为lmax
,最大后缀和为rmax
。
我们有下面三种情况:
父节点 tmax
没有横跨区间的情况包含两种:
- ① 完全在左儿子区间内部:
tmax = 左儿子tmax
- ② 完全在右儿子区间内部:
tmax = 右儿子tmax
父节点tmax
横跨两区间 情况为一种:
- ③
父节点u.tmax = 左儿子rmax + 右儿子lmax
综合一下得出表达式:
至此,我们已有方法计算出每个节点内部的\(tmax\)了。
新的问题
不过我们还需要想一下新加的两个变量:\(lmax\)(最大前缀和) 和 \(rmax\)(最大后缀和)如何得到。
和之前的思考方式类似,我们分情况来讨论:
对于一个父节点的\(lmax\),我们也可以分为两种情况:
- ① 没有跨过分界点,如下图,\(父节点lmax = 左儿子lmax\)
- ② 跨过了分界点,如下图
对于上方的情况 ②,我们发现运用现有的条件是无法得到的:
父节点最大前缀和\(u.lmax\) = 左儿子\(L_{son}.\)区间总和 + 右儿子\(R_{son}.lmax\)
同理:
父节点最大后缀和\(u.rmax\) = 右儿子\(R_{son}.\)区间总和 + 左儿子\(L_{son}.rmax\)
所以说,我们的节点最后还需要一个新的信息:区间和(设为\(sum\))
而对于 父节点\(u.sum\)也是可以计算出来的,我们可以由左儿子\(L_{son}.sum\) + 右儿子\(R_{son}.sum\)
表达式:
我们综合一下得出两个最大前后缀和的表达式:
① 父节点\(u.lmax = max(L\_son.lmax, L\_son.sum + R\_son.lmax)\)
② 父节点\(u.rmax = max(R\_son.rmax, R\_son.sum + L\_son.rmax)\)
至此,我们已经能够确定好存储线段树的结构体包含了哪些变量,进而可以确定\(pushup\)函数的编写,整个编码的大体框架不变,由四个函数构成:
序号 | 功能 | 函数名 |
---|---|---|
① | \(build\) | 建立 |
② | \(pushup\) | 将自己子孙后代的变化向上级领导汇报 |
③ | \(modify\) | 修改 |
④ | \(query\) | 查询 |
本题对于\(query\)函数另有分类等细节处理,详见代码。
时间复杂度
\(O(mlogn)(m<=1e5,n<=5e5)\)
三、实现代码
include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int n, m;
int a[N]; //临时数组,用于装一下输入的数字
struct Node {
int l, r;
int sum; // 区间和
int lmax; // 左后缀最大和
int rmax; // 右前缀最大和
int tmax; // 整体最大和
} tr[N << 2];
//由于左右儿子有变动,所以,作为父亲的节点,有责任有义务向 调用者(上级领导)汇报自己家庭的变更情况,不瞒报,不漏报~
//这里Node是用的地址符,即按地址传递参数,函数内修改的是原来的变量
//因为pushup需要在很多地方用,所以这里多写一层
void pushup(Node &u, Node &l, Node &r) {
u.sum = l.sum + r.sum; //区间和
u.lmax = max(l.lmax, l.sum + r.lmax); //左端区间和+右端前缀最大和
u.rmax = max(r.rmax, r.sum + l.rmax); //右端区间和+左端后缀最大和
u.tmax = max({l.tmax, r.tmax, l.rmax + r.lmax}); //三者取max
}
void pushup(int u) { //函数重载,这样主要是方便写起来方便
pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}
//构建
void build(int u, int l, int r) {
if (l == r) {
// lmax:因为只有一个,所以是a[l]
// rmax:因为只有一个,所以是a[l]
// mx:因为只有一个,所以是a[l]
// sum:区间总和是a[l]
tr[u] = {l, r, a[l], a[l], a[l], a[l]};
return;
}
tr[u] = {l, r}; //构建最重要的就是设置好范围
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
//由子节点信息算一下父节点信息
pushup(u);
}
//在以u节点为根的子树中,将位置x的值修改为v
void modify(int u, int x, int v) {
if (tr[u].l == x && tr[u].r == x) //如果已经到了叶节点
tr[u] = {x, x, v, v, v, v}; // l=r=x:因为只有老哥一个 sum=v lmax=v,rmax=v,mx=v
//注意:叶子节点的更改,是不需要pushup(u)的!!可以理解为此时u没有子节点了
else {
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) //那么x一定在左半边
modify(u << 1, x, v);
else // x一定在右半边
modify(u << 1 | 1, x, v);
//由于更新了左半边或者右半边中的某一个数据,所以需要再次由子节点信息算一下父节点信息
pushup(u); //有点类似于后序遍历输出
}
}
//查询的时候也需要算那四个数的,因为查的时候也可能涉及到区间合并的
Node query(int u, int l, int r) {
//要查找的区间tr[u].l ~ tr[u].r 包含了[l,r]这个区间
if (tr[u].l >= l && tr[u].r <= r) return tr[u];
int mid = tr[u].l + tr[u].r >> 1;
if (r <= mid) return query(u << 1, l, r); //返回左半边
if (l > mid) return query(u << 1 | 1, l, r); //返回右半边
//左右各半
Node ls = query(u << 1, l, r);
Node rs = query(u << 1 | 1, l, r);
//将左右儿子送回来的信息进行合并
Node res; //这里的res其实并不是一个真正在线段树中存在的节点,而是一个临时变量,方便利用pushup
//函数的计算逻辑,计算出mx,这么写确实是可以复用代码,就是理解起来麻烦了
pushup(res, ls, rs);
return res;
}
int main() {
//加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
//构建树,root=1,范围[1,n]
build(1, 1, n);
int k, x, y;
while (m--) {
cin >> k >> x >> y;
if (k == 1) {
if (x > y) swap(x, y);
printf("%d\n", query(1, x, y).tmax);
} else
modify(1, x, y);
}
return 0;
}