全局偏移量把每轮全体 +q 记成一个变量,三段存储值天然降序、只比队头每轮 O(1) 取最大,模拟压到排序 + O(m)。
OJ: roj
题目 ID: 3642
难度:提高
标签:队列模拟NOIPpython
创建: 2026-10-02 12:26
更新: 2026-10-10 11:30
形式化题目
给定
- 取出最大值
(并列时任取一个); - 把
替换为 与 两个数(等于 的段也保留); - 除这两个新生数外,其余所有数同时加
(新生段不参加本轮的 )。
需要输出两个抽样序列:
- 每轮取出的
中,第 轮对应的那些(共 个); 轮结束后全部 个数按从大到小排序,第 名(共 个)。
正解
思路
1. 朴素模拟与它的两个瓶颈
最直接的想法是把全体蚯蚓放进一个「能取最大、能插入」的结构(比如堆):每轮取出最大、插入两段,这一步只要
- 真的逐个改,每轮
, 轮共 ;按最大规模 、 估算约 次操作,完全不可行; - 用堆也躲不掉:统一加
会破坏堆序,仍然要逐个调整。
所以瓶颈很明确:「全体
2. 全局偏移量:把「全体 +q」记成一个变量
观察到增量只与「经过了几轮」有关,与蚯蚓是哪一条无关:一条蚯蚓被加的总量始终是
- 初始蚯蚓在
时入队, ; - 每轮结束
,就等价于「其余全体 」,而所有存储值纹丝不动; - 新生两段不参加本轮
,所以要先 、再以新 归一化入队:左段 ,右段 ,保证入队瞬间 恰好等于切出的真实长度。
更关键的是:比较两条蚯蚓时
3. 三个降序队列:每轮 O(1) 取最大
按存储值取最大,只需知道每个「来源」的最大存储值,而来源恰好只有三段。(有了偏移量当然也可以把全部存储值扔进堆里做
- 初始段:读入后排序一次即降序;
- 左段、右段:按切出顺序各自单调不增——这是本题的核心引理。
为什么两段天然降序? 记第
-
不增:取走 后,留下的旧值都 ( 是当轮最大);新生两段 (用 ), (用 )。故 。 -
左段不增:记
,则 。由 得 差值是整数且
,只能 ; 时 恒为 , 就是单调函数作用在不增的 上,直接不增。 -
右段不增:由
得 同样整数差
; 时 ,随 不增而不增。
于是三段都是只出不进的存储值降序队列:任意时刻全局最大就是三个队头之一,每轮
样例 1(
| 秒 |
切时 |
切口 |
存储值 |
新生两段(世界长度) | 新段存储值 |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 3 | 1, 2 | 0, 1 |
| 2 | 1 | 4 | 3 | 1, 3 | -1, 1 |
| 3 | 2 | 4 | 2 | 1, 3 | -2, 0 |
| 4 | 3 | 4 | 1 | 1, 3 | -3, -1 |
| 5 | 4 | 5 | 1 | 1, 4 | -4, -1 |
| 6 | 5 | 5 | 0 | 1, 4 | -5, -2 |
| 7 | 6 | 6 | 0 | 2, 4 | -5, -3 |
读法:第 2 列是公共偏移
4. 收尾
rest[t-1::t] 抽出第
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NEG = -(1LL<<60);
int main(){ios::sync_with_stdio(false); cin.tie(nullptr); int n,m,q,u,v,t; if(!(cin>>n>>m>>q>>u>>v>>t)) return 0; vector<ll> big(n); for(int i=0;i<n;i++) cin>>big[i]; sort(big.begin(),big.end(),greater<ll>()); vector<ll> leftv,rightv,cut; int ia=0,ib=0,ic=0,nb=0; ll off=0; ll ha=n?big[0]:NEG,hb=NEG,hc=NEG; for(int sec=1;sec<=m;sec++){ll x; if(ha>=hb && ha>=hc){x=ha+off; ia++; ha=ia<n?big[ia]:NEG;} else if(hb>=hc){x=hb+off; ib++; hb=ib<nb?leftv[ib]:NEG;} else {x=hc+off; ic++; hc=ic<nb?rightv[ic]:NEG;} if(sec%t==0) cut.push_back(x); off+=q; ll head=(ll)u*x/v; leftv.push_back(head-off); rightv.push_back(x-head-off); nb++; if(hb==NEG) hb=ib<nb?leftv[ib]:NEG; if(hc==NEG) hc=ic<nb?rightv[ic]:NEG;} vector<ll> rest; for(int i=ia;i<n;i++) rest.push_back(big[i]+off); for(int i=ib;i<nb;i++) rest.push_back(leftv[i]+off); for(int i=ic;i<nb;i++) rest.push_back(rightv[i]+off); sort(rest.begin(),rest.end(),greater<ll>()); for(int i=0;i<(int)cut.size();i++){if(i) cout<<' '; cout<<cut[i];} cout<<'\n'; bool first=true; for(int i=t-1;i<(int)rest.size();i+=t){if(!first) cout<<' '; first=false; cout<<rest[i];} cout<<'\n'; return 0;}#!/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-02 12:30
# update_at: 2026-10-02 12:30
import sys
NEG = -1 << 60 # 空队头哨兵:任何合法存储值都比它大
def simulate(a: list[int], m: int, q: int, u: int, v: int, t: int) -> tuple[list[int], list[int]]:
"""模拟 m 秒切割,返回(每 t 秒记录的切口长度, m 秒后全部蚯蚓长度的降序序列)。
长度统一存"归一化坐标":世界长度 = 存储值 + off,off 是每秒对全体(除新生者)
累加的 q。这样每秒不必真的给每只蚯蚓加 q。三段来源(初始段、左半段、右半段)
的存储值都天然降序,因此各自只需比较队头就能取到全局最大。
"""
big = sorted(a, reverse=True) # 初始段:读入后整体降序
left: list[int] = [] # 左半段 ⌊px⌋:切出顺序天然降序
right: list[int] = [] # 右半段 x-⌊px⌋:切出顺序天然降序
na, off, nb = len(big), 0, 0 # nb 同时是 left/right 的长度(两段总是成对产生)
ia = ib = ic = 0 # 三个队列各自被取走的个数
ha = big[0] # 三个队头的存储值,空队头记为 NEG
hb = hc = NEG
cut: list[int] = []
for sec in range(1, m + 1):
if ha >= hb and ha >= hc: # 谁的队头大就切谁(平局任选,题目允许)
x = ha + off # 归一化坐标换回切口前的世界长度
ia += 1
ha = big[ia] if ia < na else NEG
elif hb >= hc:
x = hb + off
ib += 1
hb = left[ib] if ib < nb else NEG
else:
x = hc + off
ic += 1
hc = right[ic] if ic < nb else NEG
if sec % t == 0: # 第 t, 2t, 3t, … 秒记录切口
cut.append(x)
off += q # 其余蚯蚓本轮集体长 q;新生的两段不参与这一次增长
head = u * x // v # ⌊px⌋,p = u/v
left.append(head - off) # 新生段用"新 off"归一化,即此刻世界长度恰好是 head
right.append(x - head - off)
nb += 1
# 若某个队头还是哨兵而它的队列刚进了新元素,就把哨头换成真值
if hb == NEG:
hb = left[ib] if ib < nb else NEG
if hc == NEG:
hc = right[ic] if ic < nb else NEG
rest = [x + off for x in big[ia:] + left[ib:] + right[ic:]] # 存储值换回世界长度
rest.sort(reverse=True)
return cut, rest
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
m = next(data)
q = next(data)
u = next(data)
v = next(data)
t = next(data)
lengths = [next(data) for _ in range(n)]
cut, rest = simulate(lengths, m, q, u, v, t)
# 第 1 行取第 t, 2t, … 个切口;第 2 行取降序序列的第 t, 2t, … 名,空行也要输出
print(' '.join(map(str, cut)))
print(' '.join(map(str, rest[t - 1::t])))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
——排序两次、主循环 轮每轮 ,与 C++ 正解同阶。 - 空间复杂度:
——三队列合计存储 个整数,终态再拼接 个。
总结
- 「每轮全体
」与「每轮取最大」互相牵制:用全局偏移量 统一记账,真实长度 存储值 ,加 退化成一个变量自增,比较退化成比存储值。 - 存储值坐标下,初始段、左段、右段各自都是降序的只出队列,三队头比较即全局最大,模拟主体
。 - 抽样输出(第
轮 / 第 名)分别用 sec % t == 0与切片rest[t-1::t]处理,两行为空时也各输出一个空行。
图示解析
下图展示正解的数据流:三个存储值降序队列供出队头,切割结果回填左右两队,终态统一还原排序。
flowchart LR
A["big 初始段(排序降序)"] --> H{"比较三队头 ha/hb/hc"}
B["left 左段(切出即降序)"] --> H
C["right 右段(切出即降序)"] --> H
H -->|"存储值最大者 c,x = c + off"| R{"sec 是 t 的倍数?"}
R -->|是| O["记录切口 x"]
R -->|否| S["off += q,切出 ⌊px⌋ 与 x-⌊px⌋"]
O --> S
S -->|"按新 off 归一化后追加"| B
S --> C
H -->|"m 轮结束,三队剩余"| Z["加 off 还原 → 降序排序 → 取第 t,2t,… 名"]
读者应注意到:
