题解:AtCoder AT_awc0087_e Change of Assigned Interval

【题目来源】

AtCoder:E - Change of Assigned Interval

【题目描述】

Takahashi and Aoki manage \(N\) tasks numbered from \(1\) to \(N\).

For the \(i\)-th task, a symbol \(S_i\) representing the person in charge and a work cost \(P_i\) are recorded. When \(S_i\) is T, the person in charge is Takahashi; when it is A, the person in charge is Aoki.

Here, flipping the assignment means changing T to A and A to T.

\(Q\) queries are given.

For the \(j\)-th query, you may perform the following operation at most once. It is also permitted to not perform the operation.

Operation: Choose a pair of integers \((l, r)\) satisfying \(L_j \leq l \leq r \leq R_j\), and flip the assignment of all tasks from the \(l\)-th to the \(r\)-th.

Consider the total work cost of tasks assigned to Aoki (A) among the tasks from the \(L_j\)-th to the \(R_j\)-th after the operation (or if no operation is performed). Find the minimum value of this total when the operation is chosen optimally.

Each query is independent. An operation in one query does not affect the original assignment/cost information or other queries.

高桥和青木管理着编号从 \(1\)\(N\)\(N\) 个任务。

对于第 \(i\) 个任务,记录了表示负责人符号 \(S_i\) 和工作成本 \(P_i\)。当 \(S_i\)T 时,负责人是高桥;当 \(S_i\)A 时,负责人是青木。

这里,翻转分配意味着将 T 改为 A,将 A 改为 T

给定 \(Q\) 个查询。

对于第 \(j\) 个查询,你最多可以执行一次以下操作。也可以选择不执行操作。

操作: 选择一对满足 \(L_j \leq l \leq r \leq R_j\) 的整数 \((l, r)\),并翻转从第 \(l\) 个到第 \(r\) 个所有任务的分配。

考虑操作后(或不执行操作时),从第 \(L_j\) 个到第 \(R_j\) 个任务中分配给青木(A)的任务的总工作成本。求当操作选择最优时,这个总和的最小值。

每个查询是独立的。一个查询中的操作不会影响原始的分配/成本信息或其他查询。

【输入】

\(N\) \(Q\)
\(S_1\) \(P_1\)
\(S_2\) \(P_2\)
\(\vdots\)
\(S_N\) \(P_N\)
\(L_1\) \(R_1\)
\(L_2\) \(R_2\)
\(\vdots\)
\(L_Q\) \(R_Q\)

  • The first line contains \(N\), the number of tasks, and \(Q\), the number of queries, separated by a space.
  • The following \(N\) lines give the information of each task.
  • The \(i\)-th of these lines contains \(S_i\), representing the person in charge of the \(i\)-th task, and \(P_i\), representing the work cost of that task, separated by a space.
  • \(S_i\) is T or A.
  • The following \(Q\) lines give the information of the queries.
  • The \(j\)-th of these lines contains the left endpoint \(L_j\) and right endpoint \(R_j\) of the range for the \(j\)-th query, separated by a space.

【输出】

Print \(Q\) lines.

On the \(j\)-th line, print the answer to the \(j\)-th query as an integer.

【输入样例】

5 4
A 3
T 5
A 2
T 4
A 6
1 5
2 4
3 3
4 5

【输出样例】

5
0
0
0

【核心思想】

  1. 问题分析:给定 \(N\) 个任务,每个任务有负责人 \(S_i\)TA)和成本 \(P_i\)。对于每个查询 \([L_j, R_j]\),可以选择一个子区间 \([l, r]\)(满足 \(L_j \leq l \leq r \leq R_j\))进行翻转(TAAT),使得该区间内分配给青木(A)的任务总成本最小。这是一个线段树维护最大子段和问题,关键在于将翻转操作转化为最大子段和问题。

  2. 算法选择

    • 前缀和(Prefix Sum)sa[i] 记录前 \(i\) 个任务中分配给青木(A)的总成本
    • 值转换:将任务值转换为 \(v_i = P_i\)(若 $S_i = $ A)或 \(v_i = -P_i\)(若 $S_i = $ T
    • 最大子段和(Maximum Subarray Sum):翻转区间 \([l, r]\) 的效果等价于从基础值中减去该区间内的最大子段和
    • 线段树(Segment Tree):维护区间和、最大前缀和、最大后缀和、最大子段和,支持 \(O(\log N)\) 查询
  3. 关键步骤

    • 初始化
      • 读取 \(N\)(任务数量)、\(Q\)(查询次数)
      • 对于每个任务 \(i\)
        • 若 $S_i = $ Asa[i] = sa[i-1] + P_iv[i] = P_i
        • 若 $S_i = $ Tsa[i] = sa[i-1]v[i] = -P_i
      • 基于数组 \(v\) 构建线段树
    • 处理查询(对于每个查询 \([L_j, R_j]\)):
      • 计算基础值base = sa[R_j] - sa[L_j-1],表示不翻转时青木的总成本
      • 查询最大子段和res = query(1, L_j, R_j),获取区间内的最大子段和 res.best
      • 计算最大可减少值maxReduce = max(0, res.best),若最大子段和为负则不翻转更优
      • 输出答案base - maxReduce,即翻转后的最小成本
  4. 时间/空间复杂度

    • 时间复杂度:\(O((N + Q) \log N)\),建树 \(O(N \log N)\),每次查询 \(O(\log N)\)
    • 空间复杂度:\(O(N)\),线段树需要 \(4N\) 个节点空间
  5. 最大子段和与线段树的核心思想

    • 翻转操作的转化:翻转区间 \([l, r]\) 会将该区间内所有 A 变为 T(减少成本 \(P_i\)),所有 T 变为 A(增加成本 \(P_i\))。净效果为:原 A 的成本被移除,原 T 的成本被加入。若将 A 设为 \(+P_i\)T 设为 \(-P_i\),则翻转效果等于基础值减去该区间内的子段和
    • 最大子段和的意义:选择最大子段和进行翻转,可以使青木的总成本减少最多(或增加最少)
    • 线段树维护四个值
      • sum:区间总和
      • pre:最大前缀和(从左端点开始的最大子段和)
      • suf:最大后缀和(以右端点结束的最大子段和)
      • best:最大子段和
    • 合并策略:最大子段和可能完全在左子区间、完全在右子区间,或跨越中点(左后缀 + 右前缀)
    • 适用于区间翻转、成本优化、最大子段和查询类问题

【算法标签】

线段树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100005;  // 定义最大数组长度
int n, q;  // 数组长度n,查询次数q
int sa[N], v[N];  // 前缀和数组sa,线段树基础数组v
struct Node
{
    int l, r;  // 区间左右端点
    int sum, pre, suf, best;  // 区间和、最大前缀和、最大后缀和、最大子段和
}tr[N*4];  // 线段树数组

void pushup(int u)  // 向上更新节点信息
{
    tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum;  // 区间和
    tr[u].pre = max(tr[u<<1].pre, tr[u<<1].sum + tr[u<<1|1].pre);  // 最大前缀和
    tr[u].suf = max(tr[u<<1|1].suf, tr[u<<1|1].sum + tr[u<<1].suf);  // 最大后缀和
    tr[u].best = max({tr[u<<1].best, tr[u<<1|1].best, tr[u<<1].suf + tr[u<<1|1].pre});  // 最大子段和
}

void build(int u, int l, int r)  // 建立线段树
{
    if (l==r)  // 叶子节点
        tr[u] = {l, r, v[l], v[l], v[l], v[l]};  // 初始化
    else  // 非叶子节点
    {
        tr[u] = {l, r};  // 初始化区间
        int mid = l + r >> 1;  // 计算中点
        build (u<<1, l, mid), build(u<<1|1, mid+1, r);  // 递归建树
        pushup(u);  // 更新节点信息
    }
}

Node query(int u, int l, int r)  // 区间查询
{
    if (tr[u].l>=l && tr[u].r<=r)  // 完全包含
        return tr[u];  // 直接返回
    else  // 不完全包含
    {
        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()  // 主函数
{
    cin >> n >> q;  // 输入数组长度和查询次数
    for (int i=1; i<=n; i++)  // 读取数组元素
    {
        char s;  // 操作类型
        int p;  // 操作数值
        cin >> s >> p;  // 输入操作类型和数值
        sa[i] = sa[i-1] + (s=='A' ? p : 0);  // 计算前缀和(只累加A类型的值)
        v[i] = (s=='A' ? p : -p);  // 设置线段树基础数组值
    }
    build (1, 1, n);  // 建立线段树
    while (q--)  // 处理每个查询
    {
        int l, r;  // 查询区间
        cin >> l >> r;  // 输入查询区间
        int base = sa[r] - sa[l-1];  // 计算基础值(区间内A类型值的和)
        Node res = query(1, l, r);  // 查询区间信息
        int maxReduce = max(0LL, res.best);  // 最大可减少值
        cout << base - maxReduce << endl;  // 输出结果
    }
    return 0;  // 程序正常结束
}

【运行结果】

5 4
A 3
T 5
A 2
T 4
A 6
1 5
5
2 4
0
3 3
0
4 5
0
posted @ 2026-06-15 09:50  团爸讲算法  阅读(8)  评论(0)    收藏  举报