题解:AcWing 245 你能回答这些问题吗
【题目来源】
AcWing:245. 你能回答这些问题吗 - AcWing题库
【题目描述】
给定长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:
1 x y,查询区间 \([x,y]\) 中的最大连续子段和,即 \(\max\limits_{x\le l\le r\le y}\{\sum\limits_{i=l}^{r}A[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\) 表示修改。
【输出】
对于每个查询指令输出一个整数表示答案。
每个答案占一行。
【输入样例】
5 3
1 2 -3 4 5
1 2 3
2 2 -1
1 3 2
【输出样例】
2
-1
【核心思想】
-
问题分析:给定长度为 \(N\) 的数列 \(A\) 和 \(M\) 条指令,指令分为两类:查询区间 \([x,y]\) 中的最大连续子段和,或将 \(A[x]\) 修改为 \(y\)。这是一个线段树维护最大子段和问题,关键在于设计能支持区间合并的节点信息。
-
算法选择:
- 线段树(Segment Tree):支持 \(O(\log N)\) 时间复杂度的单点修改和区间查询
- 节点信息设计:每个节点维护四个值——区间总和 \(sum\)、最大前缀和 \(pre\)、最大后缀和 \(suf\)、最大连续子段和 \(best\)
-
关键步骤:
- 建树(\(O(N)\)):递归建立线段树,叶子节点初始化 \(sum = pre = suf = best = A[i]\)
- 向上合并 pushup(\(O(1)\)):
- \(sum = l.sum + r.sum\)
- \(pre = \max(l.pre,\ l.sum + r.pre)\)
- \(suf = \max(r.suf,\ r.sum + l.suf)\)
- \(best = \max(l.best,\ r.best,\ l.suf + r.pre)\)
- 单点修改 update(\(O(\log N)\)):定位到叶子节点修改值,然后自底向上 pushup 更新
- 区间查询 query(\(O(\log N)\)):若查询区间横跨左右子树,需合并左右结果并返回完整 Node 信息
-
时间/空间复杂度:
- 时间复杂度:建树 \(O(N)\),单次修改 \(O(\log N)\),单次查询 \(O(\log N)\),总复杂度 \(O(N + M \log N)\)
- 空间复杂度:\(O(N)\),线段树开 \(4N\) 空间
-
线段树维护最大子段和的核心思想:
- 节点信息设计:最大子段和无法直接由左右子树的最大子段和推导,必须额外维护最大前缀和与最大后缀和,才能处理跨过中点的情况
- 跨区间合并:区间 \([l,r]\) 的最大子段和有三种来源——完全在左子树、完全在右子树、跨过中点(左后缀和 + 右前缀和)
- 查询返回结构体:与普通线段树查询返回数值不同,本题查询需要返回完整的 Node 结构,以支持上层合并
- 适用于带修改的最大子段和、最大子矩阵和等动态区间问题
【算法标签】
线段树
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将 int 定义为 long long,防止数值溢出
const int N = 500005; // 最大数组长度
int n, m, v[N], sa[N]; // n: 数组长度, m: 指令数, v: 原数组, sa: 前缀和数组(未使用)
// 线段树节点结构
struct Node
{
int l, r; // 区间左右端点
int sum; // 区间总和
int pre; // 最大前缀和(从左端点开始的最大连续子段和)
int suf; // 最大后缀和(以右端点结束的最大连续子段和)
int best; // 区间内的最大连续子段和
} tr[N * 4]; // 线段树数组(4倍空间)
// 向上更新:根据左右子节点信息更新父节点
void pushup(int u)
{
auto &root = tr[u], &l = tr[u << 1], &r = tr[u << 1 | 1];
root.sum = l.sum + r.sum; // 区间总和 = 左子树和 + 右子树和
root.pre = max(l.pre, l.sum + r.pre); // 最大前缀和:要么在左子树,要么跨过中点
root.suf = max(r.suf, r.sum + l.suf); // 最大后缀和:要么在右子树,要么跨过中点
root.best = max({l.best, r.best, l.suf + r.pre}); // 最大子段和:左子树、右子树、或跨过中点
}
// 建立线段树
void build(int u, int l, int r)
{
if (l == r) // 叶子节点
tr[u] = {l, r, v[l], v[l], v[l], v[l]}; // 初始化:sum=pre=suf=best=v[l]
else
{
tr[u] = {l, r}; // 初始化当前节点的区间范围
int mid = l + r >> 1; // 取中点(等价于 (l+r)/2)
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 递归建立左右子树
pushup(u); // 向上更新当前节点
}
}
// 单点修改:将位置 pos 的值改为 d
void update(int u, int pos, int d)
{
if (tr[u].l == tr[u].r) // 叶子节点
{
tr[u] = {pos, pos, d, d, d, d}; // 直接替换为新的值
return;
}
int mid = tr[u].l + tr[u].r >> 1; // 取中点
if (pos <= mid) update(u << 1, pos, d); // 在左子树
else update(u << 1 | 1, pos, d); // 在右子树
pushup(u); // 修改后向上更新
}
// 区间查询:返回区间 [l, r] 的 Node 信息
Node query(int u, int l, int 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 left = query(u << 1, l, r); // 查询左子树
Node right = query(u << 1 | 1, l, r); // 查询右子树
Node res;
res.sum = left.sum + right.sum;
res.pre = max(left.pre, left.sum + right.pre);
res.suf = max(right.suf, right.sum + left.suf);
res.best = max({left.best, right.best, left.suf + right.pre});
return res;
}
signed main() // 使用 signed 替代 int,因为 #define int long long
{
cin >> n >> m; // 读入数组长度和指令数
for (int i = 1; i <= n; i++)
{
cin >> v[i]; // 读入原数组
}
build(1, 1, n); // 建立线段树,根节点为1,覆盖区间[1,n]
while (m--) // 依次处理每条指令
{
int k, x, y;
cin >> k >> x >> y; // 读入指令类型和参数
if (k == 1) // 查询指令
{
if (x > y) swap(x, y); // 保证 x <= y
Node res = query(1, x, y); // 查询区间最大子段和
cout << res.best << endl; // 输出最大子段和
}
else // 修改指令
update(1, x, y); // 将 A[x] 修改为 y
}
return 0;
}
【运行结果】
5 3
1 2 -3 4 5
1 2 3
2
2 2 -1
1 3 2
-1
浙公网安备 33010602011771号