三队列维护「未切 / 小段 / 大段」+ 全局偏移量消化集体加 q:每秒 O(1) 取出最长蚯蚓,把 O(m(n+m)) 的模拟压到 O((n+m)log(n+m))。
OJ: roj
题目 ID: 3035
难度:提高+/省选-
标签:队列模拟
创建: 2026-10-01 11:18
更新: 2026-10-07 13:50
形式化题目
给定
- 取当前所有数中的最大值
并记录(并列时任取一个); - 把
替换成两个新数 和 ; - 除这两个新数外,其余所有数增加
。
要求输出两组数:
- 第
轮记录下的 (共 个,按时间顺序); 轮结束后全部 个数从大到小排序中,排名 的数。
两行各自输出,即使一行没有数也要输出一个空行。
正解
思路
朴素做法与两个瓶颈。 直接按题意模拟:每秒扫描全部蚯蚓找最大值切开,再给其余蚯蚓集体加
第一步:用全局偏移量消灭"集体加 q"。 注意到"其余蚯蚓每秒加
第二步:证明切出序列单调,用三个队列取代堆。 把蚯蚓分成三类,各放一个降序队列:
| 队列 | 装什么 | 初始内容 |
|---|---|---|
| 从未被切过的蚯蚓 | 初始 |
|
| 历次切出的小段 |
空 | |
| 历次切出的大段 |
空 |
设第
:设第 秒入队的小段存储值为 。由 得 ( ),两边取下取整得 ,于是 ; :大段存储值 ,则 ; :初始排序一次后自然降序。
于是
三个队首就是每秒的全部候选,取最大后 popleft,切成的两段 append 回
主循环每秒的工作量是常数,最后把三个队列展开、加
用样例
| 秒 | 取出并切开 | ||||
|---|---|---|---|---|---|
| 初始 | — | 3, 3, 2 | 空 | 空 | 0 |
| 1 | 4, 3 | 1 | 2 | 1 | |
| 2 | 4, 3 | 1, 1 | 2, 3 | 2 | |
| 3 | 3 | 1, 1, 1 | 2, 3, 3 | 3 |
每个队列从左到右都保持降序,所以每一秒的最长蚯蚓只会出现在三队首:第 2 秒候选是
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 11:22
# update_at: 2026-10-01 11:22
import sys
from collections import deque
NEG = -1 << 62 # 空队哨兵:保证含哨兵的 max 一定落在非空队列上
def pop_max(queues: list[deque]) -> int:
"""从三个降序单调队列的队首中取出当前最长的蚯蚓长度。"""
candidates = [q[0] if q else NEG for q in queues]
pick = candidates.index(max(candidates))
return queues[pick].popleft()
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n, m, q = int(next(data)), int(next(data)), int(next(data))
u, v, t = int(next(data)), int(next(data)), int(next(data))
# 三个队列分别装:未切过的蚯蚓、切出的 ⌊px⌋ 段、切出的 x-⌊px⌋ 段。
# 队首存真实值(含此后累积的 q),单调递减;队尾存"没长过"的历史值。
initial = sorted((int(next(data)) for _ in range(n)), reverse=True)
queues = [deque(initial), deque(), deque()]
cut_lengths: list[int] = [] # 每一秒被切断蚯蚓(切断前)的长度
offset = 0 # 全局偏移:除最近切出的两只外,每只蚯蚓每秒都长 q
for second in range(1, m + 1):
x = pop_max(queues) + offset
left = x * u // v # ⌊px⌋,历史值(含累积的 q)
right = x - left
# 新蚯蚓从下一秒开始长:历史值要倒扣本秒之后的 q(含即将累加的这一次)
queues[1].append(left - offset - q)
queues[2].append(right - offset - q)
offset += q
cut_lengths.append(x)
final = sorted(
(w + offset for qu in queues for w in qu),
reverse=True,
) # m 秒后所有蚯蚓的真实长度:三个降序队列拼接后归并,从大到小
# 两行各占一行输出,没有数的行也要输出空行
out = [cut_lengths[i] for i in range(t - 1, len(cut_lengths), t)]
print(' '.join(map(str, out)))
out = [final[i] for i in range(t - 1, len(final), t)]
print(' '.join(map(str, out)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 15:16
* update_at: 2026-10-06 15:16
*/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long ll;
// 蚯蚓(NOIP2016 / roj 3035):三队列 + 全局偏移量,O((n+m)log(n+m))
// 队列存"历史值",真实长度 = 存储值 + 全局偏移 delta,
// 这样"其余蚯蚓集体加 q"就折叠成一次 delta += q。
const int MAXN = 100005; // 初始蚯蚓数上限 n <= 1e5
const int MAXM = 7000005; // m <= 7e6,q1/q2 每秒最多各进 1 只
// 三个降序队列(证明见题解):队首即每秒全局最长的三个候选
int q0[MAXN]; // 从未被切过的蚯蚓,初始从大到小排序
int q1[MAXM]; // 历次切出的小段 floor(p*x),入队次序保证存储值不增
int q2[MAXM]; // 历次切出的大段 x - floor(p*x),同理不增
int head0 = 0, tail0 = 0; // q0 队首/队尾下标
int head1 = 0, tail1 = 0;
int head2 = 0, tail2 = 0;
// 快速输出:t=1 时两行合计可达约 1.4e7 个数,printf 太慢
char obuf[1 << 16];
int opos = 0;
void flush_out() { fwrite(obuf, 1, opos, stdout); opos = 0; }
void write_char(char c) {
if (opos == (int)sizeof(obuf)) flush_out();
obuf[opos++] = c;
}
void write_ll(ll x) { // 本题长度非负,按非负处理
if (opos > (int)sizeof(obuf) - 16) flush_out();
char tmp[24];
int len = 0;
do { tmp[len++] = '0' + x % 10; x /= 10; } while (x);
while (len) obuf[opos++] = tmp[--len];
}
int main() {
ll n, m, q, u, v, t;
scanf("%lld %lld %lld %lld %lld %lld", &n, &m, &q, &u, &v, &t);
for (int i = 0; i < n; i++) scanf("%d", &q0[i]);
sort(q0, q0 + n, greater<int>()); // q0 保持降序
tail0 = n;
ll delta = 0; // 全局偏移量:真实长度 = 存储值 + delta
bool first = true; // 第一行控制数字间空格
for (ll second = 1; second <= m; second++) {
// 三个队首候选,空队列用哨兵 INT_MIN,保证选到非空队列
int c0 = (head0 < tail0) ? q0[head0] : INT_MIN;
int c1 = (head1 < tail1) ? q1[head1] : INT_MIN;
int c2 = (head2 < tail2) ? q2[head2] : INT_MIN;
ll x; // 本秒被切蚯蚓的真实长度
if (c0 >= c1 && c0 >= c2) { x = c0 + delta; head0++; }
else if (c1 >= c2) { x = c1 + delta; head1++; }
else { x = c2 + delta; head2++; }
ll left = x * u / v; // 小段 floor(p*x),x*u 最大约 1.5e18,用 ll
ll right = x - left; // 大段
// 新切的两只不参与"本秒"加 q:历史值先倒扣 q,随后 delta 自增
q1[tail1++] = left - delta - q;
q2[tail2++] = right - delta - q;
delta += q;
if (second % t == 0) { // 第一行:第 t, 2t, ... 秒被切前的长度
if (!first) write_char(' ');
first = false;
write_ll(x);
}
}
write_char('\n'); // 没有数也要输出空行
// 第二行:m 秒后共 n+m 只,三个降序队列归并,输出排名 t, 2t, ... 的真实长度
first = true;
int i0 = head0, i1 = head1, i2 = head2;
for (ll rank = 1; rank <= n + m; rank++) {
int c0 = (i0 < tail0) ? q0[i0] : INT_MIN;
int c1 = (i1 < tail1) ? q1[i1] : INT_MIN;
int c2 = (i2 < tail2) ? q2[i2] : INT_MIN;
ll val;
if (c0 >= c1 && c0 >= c2) { val = c0 + delta; i0++; }
else if (c1 >= c2) { val = c1 + delta; i1++; }
else { val = c2 + delta; i2++; }
if (rank % t == 0) {
if (!first) write_char(' ');
first = false;
write_ll(val);
}
}
write_char('\n');
flush_out();
return 0;
}复杂度
- 时间复杂度:主循环
轮、每轮 ,加上末尾一次排序,总计 ;本题 。 - 空间复杂度:三个队列共存
个长度值, 。
总结
这道题的正解由两个观察拼成:集体加
实现上有两个容易踩的坑:
- 输出两行必须各占一行,没有数的行也要输出一个空行(直接省略会 WA);
- 新切出的两段不参与"本秒"加
,入队的历史值要倒扣 ;随后 自增,语义保持"真实长度 = 存储值 + "。
本题也是"生成序列有序"思想的代表题:新产生的元素不大于之前产生的元素,队列就比堆更合适。
图示解析
以样例(
| 秒 | 三队首候选( |
被切长度 | 切成 | 全局最长落在 |
|---|---|---|---|---|
| 1 | 3 | 1, 2 | ||
| 2 | 4 | 1, 3 | ||
| 3 | 4 | 1, 3 | ||
| 4 | 4 | 1, 3 | ||
| 5 | 5 | 1, 4 | ||
| 6 | 5 | 1, 4 | ||
| 7 | 6 | 2, 4 |
可以看到初始蚯蚓被