题解:AcWing 245 你能回答这些问题吗

【题目来源】

AcWing:245. 你能回答这些问题吗 - AcWing题库

【题目描述】

给定长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:

  1. 1 x y,查询区间 \([x,y]\) 中的最大连续子段和,即 \(\max\limits_{x\le l\le r\le y}\{\sum\limits_{i=l}^{r}A[i]\}\)
  2. 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

【核心思想】

  1. 问题分析:给定长度为 \(N\) 的数列 \(A\)\(M\) 条指令,指令分为两类:查询区间 \([x,y]\) 中的最大连续子段和,或将 \(A[x]\) 修改为 \(y\)。这是一个线段树维护最大子段和问题,关键在于设计能支持区间合并的节点信息。

  2. 算法选择

    • 线段树(Segment Tree):支持 \(O(\log N)\) 时间复杂度的单点修改和区间查询
    • 节点信息设计:每个节点维护四个值——区间总和 \(sum\)、最大前缀和 \(pre\)、最大后缀和 \(suf\)、最大连续子段和 \(best\)
  3. 关键步骤

    • 建树\(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 信息
  4. 时间/空间复杂度

    • 时间复杂度:建树 \(O(N)\),单次修改 \(O(\log N)\),单次查询 \(O(\log N)\),总复杂度 \(O(N + M \log N)\)
    • 空间复杂度:\(O(N)\),线段树开 \(4N\) 空间
  5. 线段树维护最大子段和的核心思想

    • 节点信息设计:最大子段和无法直接由左右子树的最大子段和推导,必须额外维护最大前缀和与最大后缀和,才能处理跨过中点的情况
    • 跨区间合并:区间 \([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
posted @ 2026-06-23 11:23  团爸讲算法  阅读(9)  评论(0)    收藏  举报