洛谷-P2629 好消息,坏消息 题解

题目链接:https://www.luogu.com.cn/problem/P2629

题目分析

Uim 需要向老板报告 n 条消息,每条消息有好坏度 Ai​。老板初始心情为 0,报告每条消息后心情增加 Ai​。若心情低于 0,Uim 被解雇。Uim 可以选择一个起始点 k(1≤k≤n),先报告第 k 到第 n 条消息,再报告第 1 到第 k−1 条消息。求有多少个 k 使得整个报告过程中心情始终 ≥0。

通过观察可以发现以下几点

  1. 总和约束:所有消息的总和 S 必须 ≥0。若 S<0,报告完所有消息后心情为负,直接无解。
  2. 报告过程拆分:

设s为分割点:

  • 右半部分:消息 [s,n−1],心情从 0 开始。要求该部分所有前缀和 ≥0。
  • 左半部分:消息 [0,s−1],心情从右半部分总和 R 开始。要求 R+左半部分任意前缀和≥0。

3.前缀和优化:

定义前缀和数组 presum[i]​:用于快速求左右部分的和。

算法设计

  1. 前缀和与总和:计算前缀和数组 presum 和总和 S。若 S<0,直接输出 0。
  2. 检查 k=1(原顺序):若所有前缀和 presum[i]≥0(0≤i<n),则 k=1 可行,初始化 ans=1;否则 ans=0。
  3. 单调队列维护极值:右半部分最小值:用单调递增队列 rminQ 存储 presum 索引,队首是 [i+1,n−1] 的最小值。左半部分最小值:用单调递增队列 lminQ 动态维护 [0,i] 的最小值。
  4. 枚举分割点:遍历 i 从 0 到 n−2:更新 lminQ(加入 i)和 rminQ(移除 ≤i 的索引)。计算左半部分和 L=presum[i],右半部分和 R=S−L。检查两个条件:右半部分有效:presum[rminQ.front()]−L≥0(右半部分所有前缀和 ≥0)。左半部分有效:R+presum[lminQ.front()]≥0(左半部分所有心情 ≥0)。若均满足,ans 增加 1。

复杂度分析

  • 时间复杂度:O(n)。每个元素最多入队和出队一次。
  • 空间复杂度:O(n)。存储前缀和与队列。

代码解析

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> presum(1000001); // 前缀和数组
    vector<int> lminQ(1000001), rminQ(1000001); // 单调队列
    int ll = 0, lr = 0, rl = 0, rr = 0; // 队列指针
    int ans = 1, sum = 0; // ans 初始化为 k=1 的情况

    // 输入并计算前缀和,初始化右队列
    for (int i = 0; i < n; i++) {
        cin >> presum[i];
        sum += presum[i];
        // 检查 k=1:若当前前缀和<0,则 k=1 不可行
        if (sum < 0) ans = 0;
        // 维护右半部分的单调递增队列(整个数组)
        while (rl < rr && presum[rminQ[rr - 1]] >= presum[i])
            rr--;
        rminQ[rr++] = i;
    }

    // 总和<0,无解
    if (sum < 0) {
        cout << 0;
        return 0;
    }

    // 枚举分割点 i(左半部分 [0,i])
    int lsum = 0, rsum;
    for (int i = 0; i < n - 1; i++) {
        // 维护左半部分 [0,i] 的单调递增队列
        while (ll < lr && presum[lminQ[lr - 1]] >= presum[i])
            lr--;
        lminQ[lr++] = i;

        // 移除右队列中索引 <= i 的元素(右半部分从 i+1 开始)
        if (rl < rr && i == rminQ[rl])
            rl++;

        lsum = presum[i]; // 左半部分和
        rsum = sum - lsum; // 右半部分和

        // 检查两个条件
        if (presum[rminQ[rl]] - lsum >= 0 && rsum + presum[lminQ[ll]] >= 0)
            ans++;
    }

    cout << ans;
    return 0;
}

正确性说明

  • 总和检查:S<0 时直接退出,确保最终心情 ≥0。
  • k=1 处理:在输入时检查所有前缀和,若全 ≥0 则 ans=1,否则 0。
  • 单调队列:rminQ 初始化为整个数组的单调递增队列,循环中弹出 ≤i 的索引,保证队首是 [i+1,n−1] 的最小值。lminQ 动态维护 [0,i] 的最小值。
  • 条件验证:presum[rmin​]−L≥0 确保右半部分所有前缀和 ≥0。R+presum[lmin​]≥0 确保左半部分所有心情 ≥0。

总结

本题通过前缀和将问题转化为区间极值查询,利用单调队列高效维护滑动窗口最小值。算法在 O(n) 时间内处理 n≤106 的数据规模,核心在于:

  1. 总和约束快速剪枝。
  2. 单调队列维护左右部分的最小前缀和。
  3. 两个条件验证确保报告过程中心情始终非负。
全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务