题解 P2629 【好消息,坏消息】(断环成链 + 单调队列)
1. 题目简述
Uim 有
数据范围:
2. 思路分析
难点一:环形处理(断环成链)
题目中可以选择任意起点
难点二:转化条件(前缀和)
设
难点三:滑动窗口最小值(单调队列)
我们需要枚举每一个可能的
这正是 单调队列(滑动窗口最小值) 的标准模型!
算法流程:
- 构造长度为
的数组,计算前缀和 。 - 使用一个单调递增的队列(存下标),维护当前窗口内的
值。 - 遍历
从 到 : - 入队:如果
比队尾小,队尾就没用了,弹出(维护单调性)。 - 出队:如果队头下标
,说明滑出窗口了,弹出。 - 判断:当
时,说明窗口长度已达到 。此时对应的起点是 。 检查队头(即窗口内最小值):如果 ,则该起点 合法,答案 。
- 入队:如果
时间复杂度:
3. 样例推演
输入:4,数组:-3 5 1 2
断环成链后的数组:-3 5 1 2 -3 5 1 2
前缀和 -3, 2, 3, 5, 2, 7, 8, 10,且
我们需要滑动窗口大小为 4。
| i | 当前 S[i] | 队列状态 (存下标) | 窗口范围 (对应起点 k) | 窗口最小值 (S[head]) | 基准值 S[k-1] | 判断 S[head] >= S[k-1] |
|---|---|---|---|---|---|---|
| 1 | -3 | {1} |
- | - | - | - |
| 2 | 2 | {1, 2} |
- | - | - | - |
| 3 | 3 | {1, 2, 3} |
- | - | - | - |
| 4 | 5 | {1, 2, 3, 4} |
||||
| 5 | 2 | 队列调整: 2<5(弹4), 2<3(弹3), 2<2(不弹) {1, 2, 5}队头1过期(1 < 5-4+1=2) {2, 5} |
||||
| 6 | 7 | {2, 5, 6} |
||||
| 7 | 8 | {2, 5, 6, 7}队头2过期 {5, 6, 7} |
统计 ✅ 数量:2。输出 2。
4. AC 代码
cpp
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
// 数据范围 N <= 10^6,开大一点
const int MAXN = 2000005;
int a[MAXN];
long long s[MAXN]; // 前缀和用 long long 防止溢出(虽然这题int大概率够用)
int main() {
// 开启 IO 加速
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
if (!(cin >> n)) return 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
// 断环成链:复制一份到后面
a[i + n] = a[i];
}
// 计算前缀和,计算范围到 2*n - 1 即可覆盖所有可能的长度为 n 的窗口
s[0] = 0;
for (int i = 1; i < 2 * n; ++i) {
s[i] = s[i - 1] + a[i];
}
deque<int> q;
int ans = 0;
// 遍历前缀和数组
for (int i = 1; i < 2 * n; ++i) {
// 1. 维护单调递增队列 (找最小值)
// 如果当前 s[i] 比队尾小,队尾显然不是最小值的候选了
while (!q.empty() && s[q.back()] >= s[i]) {
q.pop_back();
}
q.push_back(i);
// 2. 窗口形成判断 (i >= n)
if (i >= n) {
// 当前窗口对应的起始位置 k
// 窗口范围是 [i - n + 1, i]
int k = i - n + 1;
// 3. 移除过期元素
// 如果队头下标小于窗口左边界,弹出
if (!q.empty() && q.front() < k) {
q.pop_front();
}
// 4. 判断条件
// 区间内最小的前缀和必须 >= 之前的基准前缀和 s[k-1]
if (s[q.front()] - s[k - 1] >= 0) {
ans++;
}
}
}
cout << ans << endl;
return 0;
}
5. 总结
- 断环成链:解决循环数组问题的首选方法。
- 问题转化:将“全程非负”转化为“区间内最小前缀和
初始前缀和”。 - 单调队列:在
时间内解决滑动窗口最值问题。 - 注意细节:
的计算范围,以及 与 的下标对应关系。