Codeforces 1208D 从加权前缀到逆序重建排列
先明确状态与不变量,再对照手算、证明、边界和实现。
章节速览8 节 · 展开选择
在树状数组序列计数里,树状数组经常保存“某个值出现了几次”。这次要把每个仍可使用的值 v 存成权重 v,再反过来追问:前缀和第一次超过一个给定数的位置在哪里?
这篇沿着逆序处理的思路继续走。倒着恢复并非为了反转循环本身,而是为了让“左侧还有哪些数”变成一个已经确定的集合。
官方题意与为什么正序难猜
1208D · Restore Permutation 官方难度 1900,标签为 binary search、data structures、greedy、implementation。元信息与约束核对于 2026-10-22;亦可查阅官方题目 API。
原排列 p 恰好包含 1 到 n 各一次。输入给出每个位置 i 的 sᵢ:在 i 之前、数值又小于 pᵢ 的那些元素的数值之和。例如左侧较小的值是 1 和 3,贡献为 4,而非元素个数 2。
范围为 1≤n≤200000、0≤sᵢ≤n(n−1)/2,保证输入来自合法排列,答案唯一。正文重新组织解法,不复述整段题面。
正序处理时,sᵢ=0 并不能确定 pᵢ:它可能是当前出现过的最小值,也可能左侧根本没有元素。若枚举所有排列,搜索规模达到 n!;即使已经想到逆序,每次线性扫描剩余值仍需 O(n²)。
逆序以后,剩余集合就是原前缀
从 i=n 向 1 恢复,令 U 为尚未被放入答案的数值集合。处理位置 i 前,不变量是:
U 恰好由真实排列的 p₁,…,pᵢ 组成,已经恢复的后缀不再属于 U。
初始 U={1,…,n},显然成立。现在假设真实 pᵢ=x。U 中除 x 外的所有值都在原位置 i 左侧,因此
sᵢ = U 中所有严格小于 x 的值之和。
原题同时要求“在左侧”和“值更小”,逆序后,前一项已经由 U 的定义自动满足,只需在数值轴上找后一项。
flowchart LR A[从最后一个位置开始] --> B[剩余集合 U 等于原前缀] B --> C[左侧较小之和变成 U 的加权前缀] C --> D[选择当前值 x] D --> E[删除权重 x] E --> F[继续恢复前一个位置] F --> B
找到首个超过 s 的加权前缀
定义值域权重 w[v]:v∈U 时为 v,否则为 0;再定义 F(t)=w[1]+…+w[t],并令 F(0)=0。
对于刚才的 x,恰有 F(x−1)=sᵢ,而 F(x)=sᵢ+x>sᵢ。对任何 t<x 都有 F(t)≤sᵢ。因此 x 正是:
最小的、使 F(x)>sᵢ 成立的数值位置。
删除过的位置权重为零,会产生平坦段,但不会改变这个“首次严格超过”的定义。合法输入保证该位置存在,且它的当前权重大于零。计数第 k 小与本题不是同一个权重模型;不能把 sᵢ 直接当作还剩多少个较小元素。
这也给出完整正确性证明:归纳假设成立时,上述边界唯一选出真实 pᵢ。将它从 U 移除后,U 恰好剩下 p₁,…,pᵢ₋₁,不变量继续成立。到 i=1 时恢复全部排列。每步都是唯一选择,因此合法 s 不可能对应两个不同排列。
树状数组倍增为何只需一层对数
树状数组保存权重和,删除值 x 就执行 add(x,-x)。初始化时对每个 v 加入 v;不需要把原数组下标 i 存进树状数组。
若对值域二分,每次调用一次前缀求和,单次选择会是 O(log²n)。树状数组自己的块结构允许直接倍增,把选择降为 O(log n)。维护 pos 与 acc:acc 始终等于 F(pos),且不超过目标 s。
从不大于 n 的最大二次幂 step 开始,尝试 next=pos+step。在这个倍增顺序下,pos 是 2×step 的倍数,因此 next 的 lowbit 恰为 step,tree[next] 正好覆盖 (pos,next]。于是 acc+tree[next] 就是 F(next),不必重新查询。
如果 next≤n 且候选和≤s,就接受整块,更新 pos 和 acc;否则保留原位置。随后 step 减半。在每个二进制位选择最大可行前缀,最终得到最大的 F(pos)≤s 的位置,答案为 pos+1。
所有权重非负,故 F 单调不减,这是倍增选择的前提。搜索采用 ≤s 的可跳过前缀,才能返回首个 >s 的位置;若把等号写反,就会在恰好命中的边界提前停止。
flowchart TD
A[pos 为 0,acc 为 0] --> B[从最大二次幂尝试一整块]
B --> C{未越界且候选和不超过 s}
C -->|是| D[接受该块并前进]
C -->|否| E[保持已有前缀]
D --> F[步长减半]
E --> F
F --> G{还有步长}
G -->|有| B
G -->|无| H[返回 pos 加 1]
五个数的完整手算
自拟排列 p=[3,1,5,2,4]。逐位置按定义编码,得到 s=[0,0,4,1,6]:第三项前面较小的是 3 与 1,第五项前面较小的是 3、1、2。
| 恢复位置 i | 选择前剩余集合 U | sᵢ | 首个跨越边界 | 恢复 pᵢ | 删除的权重 |
|---|---|---|---|---|---|
| 5 | {1,2,3,4,5} | 6 | F(3)=6,F(4)=10 | 4 | 4 |
| 4 | {1,2,3,5} | 1 | F(1)=1,F(2)=3 | 2 | 2 |
| 3 | {1,3,5} | 4 | F(4)=4,F(5)=9 | 5 | 5 |
| 2 | {1,3} | 0 | F(0)=0,F(1)=1 | 1 | 1 |
| 1 | {3} | 0 | F(2)=0,F(3)=3 | 3 | 3 |
答案写回原下标,因此输出仍为 3 1 5 2 4,不把恢复顺序 4 2 5 1 3 直接打印出来。第三行的 F(4)=4 包含已经删除的值 2、4 的零权槽位;搜索的是数值位置,而非集合中元素的连续编号。
自检:sᵢ=0 时应该固定选择值 1 吗?
应该选当前剩余集合的最小值。例子最后只剩 {3},F(1)=F(2)=0,首个超过零的位置是 3;值 1 早已用于第二项。
自检:恢复第五项时,前缀和恰好达到 6,为何还要继续?
s₅只计算严格小于 p₅ 的剩余值。和值为 6 的前缀仍位于答案之前,应跳过;再加入答案本身的正权重才会首次超过 6。本例因此得到 4,而非 3。
完整 C++17 实现
实现遵守题目保证,不另行定义非法输入的输出协议。firstPrefixGreater 的调用前提是权重非负且 0≤target<全部剩余权重和;合法重建每一步都满足它。
1 |
|
边界与复杂度
初始化 n 次加入;每个位置进行一次 O(log n) 选择与一次 O(log n) 删除。因此总时间 O(n log n),空间 O(n)。也可以线性初始化树状数组,但不改变整个重建的渐进时间。
| 情况 | 应有行为 | 常见错误 |
|---|---|---|
| n=1、s=[0] | 返回 [1] | 起始倍增步长错误 |
| 全部 sᵢ=0 | 每轮取当前最小值,最终原排列递减 | 总是选择值 1,或输出恢复顺序 |
| 原排列递增 | sᵢ=i(i−1)/2 | 将数值和误作元素个数 |
| 前缀恰好等于 sᵢ | 继续跳过,寻找首个严格大于 | 倍增判断误写为严格小于 |
| 删除产生零权平坦段 | 可以连续跳过,答案仍为非零权位置 | 误以为前缀必须严格递增 |
| n 不是二次幂 | 对每个候选检查 next≤n | 只按整棵完全树处理 |
| 最大规模 | 所有权重和与输入使用 long long | int 前缀溢出,或删除时只减 1 |
最大 n=200000 时,输入上界为 19999900000,初始全部权重和为 20000100000,均超过有符号 32 位整数。树节点、输入与累积量都使用 long long;下标与单个排列值使用 int 足够。
如果输入不保证合法,只有 0≤sᵢ<剩余总和 还不足以验证:还必须检查选中值之前的精确加权前缀等于 sᵢ。例如剩余值 {1,3} 时,目标 2 落在一次跳跃内部,虽能找到首个超过它的位置 3,却不存在恰好为 2 的较小值之和。本实现依赖官方合法性保证,不把“有上界位置”误当作完整合法性检查。
验证与继续阅读
验证程序 tests/verify-restore-permutation-article.cjs 从正文提取原样 C++17,以 -O2 -Wall -Wextra -pedantic -Werror 在 MinGW g++ 13.1.0 无警告编译。n=1…8 的全部 46233 个排列按原定义逐对编码,既核对恢复值,也核对这些编码互不相同;另有 600 个预定种子的随机排列与上文手算,合计 46834 个批量实例。
原样 main 另运行 6 例,包含递增、递减、高低交替的三组 n=200000 边界;大规模参考采用归并排序累计贡献,不复用树状数组选择。全部检查共恢复 1030743 个值,另核对 74168 个任意非负权前缀选择,覆盖零权平坦段与恰好命中的边界。五行手算和两图结构也经程序检查,54 个图中文字节点均在画布内;验证针对合法整数模型,不把本机耗时当作所有评测机的保证。
先回到树状数组计数与逆序奇偶,比较“查询给定前缀的和”与“根据和选择前缀边界”;再看单调栈重建山峰,比较哪些重建问题靠剩余集合,哪些依赖局部单调结构。