文章

【GESP】C++六级/五级练习题 luogu-P2629 好消息,坏消息

GESP C++ 六级/五级练习题,前缀和思想和队列数据结构考点应用,五六级考生均可以练习。题目难度⭐⭐⭐☆☆,洛谷难度等级普及/提高−

luogu-P2629 好消息,坏消息

题目要求

题目描述

Uim 在公司里面当秘书,现在有 $n$ 条消息要告知老板。每条消息有一个好坏度,这会影响老板的心情。告知完一条消息后,老板的心情等于老板之前的心情加上这条消息的好坏度。最开始老板的心情是 $0$,一旦老板心情到了 $0$ 以下就会勃然大怒,炒了 Uim 的鱿鱼。

Uim 为了不被炒,提前知道了这些消息(已经按时间的发生顺序进行了排列)的好坏度,希望知道如何才能不让老板发怒。

Uim 可以使用一种叫 “倒叙” 的手法,例如有 $n$ 条消息,Uim 可以任取一个整数 $k$($1 \leq k \leq n$),先从 $k$ 事件通报到 $n$ 事件,再从 $1$ 事件通报到 $k-1$ 事件。特别的,当 $k=1$ 时按照原顺序通报。

他希望知道,有多少个这样的 $k$ 可以让老板不发怒。

输入格式

第一行一个整数 $n$($1 \le n \le10^6$),表示有 $n$ 个消息。

第二行 $n$ 个整数,按时间顺序给出第 $i$ 条消息的好坏度 $A_i$($-10^3\le A_i \le 10^3$)。

输出格式

一行一个整数,表示可行的方案个数。

输入输出样例 #1

输入 #1
1
2
4
-3 5 1 2
输出 #1
1
2

说明/提示

【样例解释】

通报事件的可行顺序(用编号表示)为 $2\rightarrow3\rightarrow4\rightarrow1$ 或 $3\rightarrow4\rightarrow1\rightarrow2$(分别对应 $k=2$ 和 $k=3$)

通报事件的可行顺序(用好坏度表示)为 $5\rightarrow1\rightarrow2\rightarrow(-3)$ 或 $1\rightarrow2\rightarrow(-3)\rightarrow5$

【数据范围】

对于 $25\%$ 的数据,$n\le10^3$;
对于 $75\%$ 的数据,$n\le10^4$;
对于 $100\%$ 的数据,$1 \le n\le 10^6$。


题目分析

这道题目要求我们在一个环形序列中找到合适的起始位置 $k$,使得从 $k$ 开始遍历一圈的过程中,累加和始终保持非负。

1. 断环成链

处理环形问题的一个常用技巧是“断环成链”。 原序列是 $A_1, A_2, \dots, A_n$。 我们将原序列复制一份接在末尾,形成长度为 $2n$ 的新序列:$A_1, A_2, \dots, A_n, A_1, A_2, \dots, A_n$。 这样,原序列中任意一种“倒叙”手法(即从 $k$ 开始的循环移位),都对应新序列中一段长度为 $n$ 的连续子区间 $[k, k+n-1]$。

2. 前缀和

为了快速计算区间和,我们需要使用前缀和。 设 pre[i] 表示新序列前 $i$ 项的和,即 pre[i] = A[1] + ... + A[i]。 那么子区间 $[k, j]$ 的和可以表示为 pre[j] - pre[k-1]

题目要求老板的心情始终不能小于 0。 这意味着,对于确定的起始位置 $k$,在区间 $[k, k+n-1]$ 内的任意位置 $j$ ($k \le j \le k+n-1$),都要满足: \(\text{当前心情} = \text{pre}[j] - \text{pre}[k-1] \ge 0\) 即: \(\text{pre}[j] \ge \text{pre}[k-1]\)

要让上述不等式对区间内所有的 $j$ 都成立,只需让区间内的最小值满足即可: \(\min_{k \le j \le k+n-1} \{ \text{pre}[j] \} \ge \text{pre}[k-1]\)

3. 单调队列优化

现在问题转化成了:对于每一个可能的起点 $k$ ($1 \le k \le n$),我们需要查询长度为 $n$ 的区间 $[k, k+n-1]$ 内 pre 数组的最小值。 这是一个典型的滑动窗口最小值问题。

如果我们暴力遍历寻找最小值,总时间复杂度是 $O(n^2)$。考虑到 $n \le 10^6$,这会超时。 我们可以使用单调队列(双端队列 std::deque)来优化,将时间复杂度降低到 $O(n)$。

  • 维护单调递增队列:队列中存储数组下标,对应的 pre 值单调递增。
  • 入队:当遍历到一个新元素 pre[i] 时,如果它比队尾元素对应的 pre 值小(或相等),说明队尾元素不可能是未来的最小值了(因为 pre[i] 更小且更靠后),将队尾元素弹出。然后将 $i$ 入队。
  • 出队(滑出窗口):如果队头元素对应的下标已经滑出了当前窗口范围(即下标 $< k$),则将队头弹出。
  • 查询:队头元素对应的 pre 值即为当前窗口的最小值。

总结

  1. 读取输入 $n$ 和数组 $A$,并将 $A$ 复制一份接在后面。
  2. 计算前缀和数组 pre
  3. 利用单调队列维护长度为 $n$ 的滑动窗口内的 pre 最小值。
  4. 枚举起点 $k$ 从 $1$ 到 $n$,判断窗口最小值是否大于等于 pre[k-1],统计符合条件的 $k$ 的个数。

时间复杂度:$O(n)$。 空间复杂度:$O(n)$。


示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <deque>
#include <iostream>

// 数组大小设置为 2*10^6,因为 n 最大为 10^6,且需要断环成链开两倍空间
int a[2000005];
int pre[2000005];  // 前缀和数组

int main() {
    int n;
    std::cin >> n;
    for (int i = 1; i <= n; i++) {
        std::cin >> a[i];
        // 断环成链:将原数组复制一份接在后面,模拟环状结构
        // 这样处理后,从任意 k 开始的长度为 n 的序列在数组中就是连续的一段
        a[i + n] = a[i];
    }

    // 计算前缀和
    // pre[i] 表示序列前 i 项的和。
    // 注意这里计算到了 2*n - 1,覆盖了所有可能的起始位置延展出的 n 长度序列
    for (int i = 1; i < 2 * n; i++) {
        pre[i] = pre[i - 1] + a[i];
    }

    int count = 0;
    std::deque<int> q;  // 单调队列,用于维护当前滑动窗口内的最小前缀和的下标

    // 遍历所有可能的结束位置 i
    for (int i = 1; i < 2 * n; i++) {
        // 维护单调递增队列:
        // 如果新元素 pre[i] 比队尾元素更小(或相等),
        // 那么队尾元素就不可能是当前或未来窗口的最小值了,可以直接弹出
        while (!q.empty() && pre[q.back()] >= pre[i]) {
            q.pop_back();
        }
        q.push_back(i);

        // 如果 i >= n,说明窗口大小已经达到了 n,可以开始判断是否有解
        // 此时窗口对应的原序列起始位置 k = i - n + 1
        if (i >= n) {
            int k = i - n + 1;

            // 检查队头元素是否已滑出窗口 [k, i]
            // 如果队头下标小于 k,说明该最小值不在当前考虑的区间内,弹出
            if (!q.empty() && q.front() < k) {
                q.pop_front();
            }

            // 我们只关心 k 在 1 到 n 之间的情况
            if (k <= n) {
                // 核心判断逻辑:
                // 如果从 k 开始每一步累加都不小于 0,等价于
                // 区间 [k, k+n-1] 内的所有前缀和 pre[j] 减去 pre[k-1] 都 >= 0
                // 即:min(pre[k...i]) - pre[k-1] >= 0
                // q.front() 保存了区间 [k, i] 内最小前缀和的下标
                if (pre[q.front()] - pre[k - 1] >= 0) {
                    count++;
                }
            }
        }
    }

    std::cout << count;
    return 0;
}

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

GESP 学习专题站:GESP WIKI

luogu-”系列题目可在洛谷题库进行在线评测。

bcqm-”系列题目可在编程启蒙题库进行在线评测。

欢迎加入Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答

欢迎加入C++ GESP/CSP认证学习QQ频道,考试资源总结汇总

欢迎加入C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
本文由作者按照 CC BY-NC-SA 4.0 进行授权