[noip2016-提高] 蚯蚓

GitHub跳转原题关系图返回列表

全局偏移量把每轮全体 +q 记成一个变量,三段存储值天然降序、只比队头每轮 O(1) 取最大,模拟压到排序 + O(m)。

OJ: roj

题目 ID: 3642

难度:提高

标签:队列模拟NOIPpython

创建: 2026-10-02 12:26

更新: 2026-10-10 11:30

形式化题目

给定 nn 个非负整数 a1,…,ana_1,\dots,a_n(蚯蚓长度)、非负整数 qq、有理数 p=u/v∈(0,1)p = u/v \in (0,1) 以及轮数 mm。进行 mm 轮,每轮对当前全体数构成的多重集执行:

  1. 取出最大值 xx(并列时任取一个);
  2. 把 xx 替换为 ⌊px⌋\lfloor px \rfloor 与 x−⌊px⌋x - \lfloor px \rfloor 两个数(等于 00 的段也保留);
  3. 除这两个新生数外,其余所有数同时加 qq(新生段不参加本轮的 +q+q)。

需要输出两个抽样序列:

  • 每轮取出的 xx 中,第 t,2t,3t,…t, 2t, 3t, \dots 轮对应的那些(共 ⌊m/t⌋\lfloor m/t \rfloor 个);
  • mm 轮结束后全部 n+mn+m 个数按从大到小排序,第 t,2t,3t,…t, 2t, 3t, \dots 名(共 ⌊(n+m)/t⌋\lfloor (n+m)/t \rfloor 个)。

正解

思路

1. 朴素模拟与它的两个瓶颈

最直接的想法是把全体蚯蚓放进一个「能取最大、能插入」的结构(比如堆):每轮取出最大、插入两段,这一步只要 O(log⁡N)O(\log N),其中 N=n+mN = n+m(排序数组取最大 O(1)O(1),但插入仍是 O(N)O(N))。但别忘了第 3 步——其余全体还要 +q+q:

  • 真的逐个改,每轮 O(N)O(N),mm 轮共 O(mN)O(mN);按最大规模 m=7×106m = 7\times10^6、N=7.1×106N = 7.1\times10^6 估算约 5×10135\times10^{13} 次操作,完全不可行;
  • 用堆也躲不掉:统一加 qq 会破坏堆序,仍然要逐个调整。

所以瓶颈很明确:「全体 +q+q」每轮一次 O(N)O(N) 与 「每轮找最大」 必须一起解决,因为前者会摧毁后者赖以加速的序结构。

2. 全局偏移量:把「全体 +q」记成一个变量

观察到增量只与「经过了几轮」有关,与蚯蚓是哪一条无关:一条蚯蚓被加的总量始终是 q×q \times「它出生后经历的轮数」。于是不再逐条修改,而是维护一个累计增量 offoff(初始 00,每轮结束 +q+q),每条蚯蚓只存存储值 cc,约定:

真实长度=c+off.\text{真实长度} = c + off.
  • 初始蚯蚓在 off=0off = 0 时入队,c=aic = a_i;
  • 每轮结束 off+=qoff \mathrel{+}= q,就等价于「其余全体 +q+q」,而所有存储值纹丝不动;
  • 新生两段不参加本轮 +q+q,所以要先 off+=qoff \mathrel{+}= q、再以新 offoff 归一化入队:左段 cL=⌊px⌋−offc_L = \lfloor px\rfloor - off,右段 cR=x−⌊px⌋−offc_R = x - \lfloor px\rfloor - off,保证入队瞬间 c+offc + off 恰好等于切出的真实长度。

更关键的是:比较两条蚯蚓时 offoff 是公共项,比真实长度等价于比存储值。找最大从此不用碰 offoff。

3. 三个降序队列:每轮 O(1) 取最大

按存储值取最大,只需知道每个「来源」的最大存储值,而来源恰好只有三段。(有了偏移量当然也可以把全部存储值扔进堆里做 O(mlog⁡m)O(m\log m);但下面会证明三段各自单调,于是每轮只比三个队头就行,模拟主体降到 O(m)O(m)——这正是 Python 版能按正解同阶跑完的关键。)

  • 初始段:读入后排序一次即降序;
  • 左段、右段:按切出顺序各自单调不增——这是本题的核心引理。

为什么两段天然降序? 记第 kk 轮切时的累计增量 offk=(k−1)qoff_k = (k-1)q、取走的存储值为 sks_k(真实长度 xk=sk+offkx_k = s_k + off_k),切后 offk+1=offk+qoff_{k+1} = off_k + q,入队值 lk=⌊pxk⌋−offk+1l_k = \lfloor px_k\rfloor - off_{k+1},rk=xk−⌊pxk⌋−offk+1r_k = x_k - \lfloor px_k\rfloor - off_{k+1}。

  • sks_k 不增:取走 sks_k 后,留下的旧值都 ≤sk\le s_k(sks_k 是当轮最大);新生两段 lk≤xk−offk+1=sk−q≤skl_k \le x_k - off_{k+1} = s_k - q \le s_k(用 ⌊pxk⌋<xk\lfloor px_k \rfloor < x_k),rk≤xk−offk+1=sk−q≤skr_k \le x_k - off_{k+1} = s_k - q \le s_k(用 ⌊pxk⌋⩾0\lfloor px_k\rfloor \geqslant 0)。故 sk+1≤sks_{k+1} \le s_k。

  • 左段不增:记 g=sk+1−sk⩽0g = s_{k+1}-s_k \leqslant 0,则 xk+1−xk=g+qx_{k+1}-x_k = g+q。由 ⌊a⌋−⌊b⌋⩽a−b+1\lfloor a\rfloor - \lfloor b\rfloor \leqslant a-b+1 得

    lk+1−lk=⌊pxk+1⌋−⌊pxk⌋−q⩽p(g+q)+1−q⩽pq+1−q=1−q(1−p)<1(q⩾1),l_{k+1}-l_k = \lfloor px_{k+1}\rfloor - \lfloor px_k\rfloor - q \leqslant p(g+q)+1-q \leqslant pq+1-q = 1-q(1-p) < 1 \quad (q \geqslant 1),

    差值是整数且 <1<1,只能 ⩽0\leqslant 0;q=0q=0 时 offoff 恒为 00,⌊pxk⌋\lfloor px_k\rfloor 就是单调函数作用在不增的 sks_k 上,直接不增。

  • 右段不增:由 ⌊a⌋−⌊b⌋⩾a−b−1\lfloor a\rfloor - \lfloor b\rfloor \geqslant a-b-1 得

    rk+1−rk=g−(⌊pxk+1⌋−⌊pxk⌋)⩽g−(p(g+q)−1)=g(1−p)−pq+1⩽1−pq<1(q⩾1),r_{k+1}-r_k = g - \big(\lfloor px_{k+1}\rfloor - \lfloor px_k\rfloor\big) \leqslant g - \big(p(g+q)-1\big) = g(1-p)-pq+1 \leqslant 1-pq < 1 \quad (q \geqslant 1),

    同样整数差 ⩽0\leqslant 0;q=0q=0 时 rk=⌈(1−p)sk⌉r_k = \lceil(1-p)s_k\rceil,随 sks_k 不增而不增。

于是三段都是只出不进的存储值降序队列:任意时刻全局最大就是三个队头之一,每轮 O(1)O(1) 比出最大、推进该队指针、把新切的两段追加到左右队尾即可。平局时先比到谁就取谁,题面允许任取。

样例 1(q=1q=1,p=1/3p=1/3)逐轮验证三个序列的单调性——下表同时演示归一化记账:

秒 kk 切时 offoff 切口 xx(输出值) 存储值 sk=x−offs_k = x-off 新生两段(世界长度) 新段存储值
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 列是公共偏移 offoff,第 3 列(真实切口)虽然一路涨到 6,但第 4 列存储值 3,3,2,1,1,0,03,3,2,1,1,0,0 单调不增;新段存储值按「左列 0,−1,−2,−3,−4,−5,−50,-1,-2,-3,-4,-5,-5、右列 1,1,0,−1,−1,−2,−31,1,0,-1,-1,-2,-3」分别不增,与上面的引理完全吻合——这正说明比较存储值与比较真实长度等价。样例输出的第一行 3,4,4,4,5,5,63,4,4,4,5,5,6 正是第 3 列;第二行 6,6,6,5,5,4,4,3,2,26,6,6,5,5,4,4,3,2,2 则是第 7 轮结束后三队剩余值加回 offoff、降序排序的结果(t=1t=1 全部输出)。

4. 收尾

mm 轮后把三队剩余的存储值统一加 offoff 还原成真实长度(加常数不改变大小序),整体降序排序,按 rest[t-1::t] 抽出第 t,2t,…t, 2t, \dots 名;切口序列则在循环里每逢第 tt 轮记录一次。两行各自独立地从头按 tt 计数(样例 2 强调第二行不接第一行没输出的余数),任何一行没有数要输出时也各打一个空行(样例 3 的第一行)。

代码

cpp
/**
 * 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;}
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-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()

复杂度

  • 时间复杂度:O(nlog⁡n+m+(n+m)log⁡(n+m))O(n\log n + m + (n+m)\log(n+m))——排序两次、主循环 mm 轮每轮 O(1)O(1),与 C++ 正解同阶。
  • 空间复杂度:O(n+m)O(n+m)——三队列合计存储 n+2mn+2m 个整数,终态再拼接 n+mn+m 个。

总结

  • 「每轮全体 +q+q」与「每轮取最大」互相牵制:用全局偏移量 offoff 统一记账,真实长度 == 存储值 +off+ off,加 qq 退化成一个变量自增,比较退化成比存储值。
  • 存储值坐标下,初始段、左段、右段各自都是降序的只出队列,三队头比较即全局最大,模拟主体 O(m)O(m)。
  • 抽样输出(第 t,2t,…t,2t,\dots 轮 / 第 t,2t,…t,2t,\dots 名)分别用 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,… 名"]

读者应注意到:offoff 只在右侧的记账步骤里自增一次,从不进入队列内容;左右两队既有「被取走」也有「被追加」,但取的永远是队头、加的永远是队尾,所以队内降序始终成立。