从后往前逐位确定身高:第 i 头牛的身高是剩余空闲身高中第 A_i+1 小的,用树状数组维护 0/1 空闲标记并在其上倍增找第 k 小。
OJ: roj
题目 ID: 3111
难度:普及+/提高-
标签:树状数组二分python
创建: 2026-10-01 17:40
更新: 2026-10-10 11:30
形式化题目
设身高是
即第
数据范围:
暴力解法
思路
核心观察来自最后一头牛:它前面站着全部
把这个观察推广到从后往前的每一个位置。处理到位置
- 比
小的身高共 个,其中恰有 个要排在它前面; - 剩下
个更矮的身高留给后面的位置也没关系——后面的牛的约束 只统计"它前面"的牛,往位置 里填什么更小的身高, 既不会冒出新的"比 矮且在前面"的牛(那些身高已被 的推导占位), 也不会消失,归纳可证每一步的选择都合法且唯一。
于是得到朴素做法:逆序扫描,每次在剩余身高集合中"找第
代码
复杂度与瓶颈
- 时间
:每头牛一次线性扫描找第 小 + 一次 删除。 - 空间
。 - 瓶颈:动态集合上的"按名次查询"没有利用任何序结构,每次都从头数。
正解
关键观察
"在剩余身高集合中找第
"第
思路
- 初始化树状数组为全
: tree[i] = i & -i直接构造,前缀恰有 个空闲。 - 逆序枚举
,在树状数组上倍增找第 小的空闲身高 ,记录答案。 - 把身高
的标记减 (树状数组单点修改),表示该身高已占用。 - 逆序求出的是
,最后反转输出。
用样例
| 位置 |
剩余空闲身高 | 取第 |
||
|---|---|---|---|---|
| 5 | 0 | 第 1 小 | 1 | |
| 4 | 1 | 第 2 小 | 3 | |
| 3 | 2 | 第 2 小 | 4 | |
| 2 | 1 | 第 2 小 | 5 | |
| 1 | 0 | 第 1 小 | 2 |
表格从下往上读就是最终答案序列
代码
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
*/
// 排队:树状数组上倍增求「剩余身高中第 k 小」。
#include <cstdio>
#include <vector>
typedef long long ll;
const ll MAXN = 100005;
ll n;
ll a[MAXN]; // a[i]:第 i 头牛前面比它矮的数量(i ≥ 2)
ll tree[MAXN]; // 树状数组中 tree[i] 记录 (i-lowbit(i), i] 里空闲身高的个数
ll ans[MAXN];
// 树状数组上倍增:返回剩余(未被占用)身高中第 k 小的下标(1-based)
ll kth_free(ll k, ll log) {
ll pos = 0;
for (ll p = log; p >= 0; p--) {
ll step = 1LL << p;
ll nxt = pos + step;
if (nxt <= n && tree[nxt] < k) { // 前缀和不够 k,第 k 小还在更右边
pos = nxt;
k -= tree[nxt];
}
}
return pos + 1;
}
int main() {
if (scanf("%lld", &n) != 1) return 0; // 空输入安全返回
a[1] = 0; // 第 1 头牛前面没有牛
for (ll i = 2; i <= n; i++) {
scanf("%lld", &a[i]);
}
for (ll i = 1; i <= n; i++) {
tree[i] = i & -i; // 初值即建树:所有身高都空闲
}
ll log = 1;
while ((1LL << log) <= n) {
log++;
}
log--; // 2^log ≤ n < 2^(log+1)
for (ll i = n; i >= 1; i--) {
// 从后往前确定:第 i 头牛的身高是剩余身高里第 a[i]+1 小的
ll h = kth_free(a[i] + 1, log);
ans[i] = h;
ll p = h;
while (p <= n) { // 身高 h 已被占用
tree[p] -= 1;
p += p & -p;
}
}
for (ll i = 1; i <= n; i++) {
printf("%lld\n", ans[i]);
}
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-01 17:40
# update_at: 2026-10-01 17:40
import sys
def kth_free(tree: list[int], k: int, log: int) -> int:
"""树状数组上倍增:返回剩余(未被占用)身高中第 k 小的下标(1-based)。"""
pos = 0
for step in (1 << p for p in range(log, -1, -1)):
nxt = pos + step
if nxt < len(tree) and tree[nxt] < k: # 前缀和不够 k,说明第 k 小还在更右边
pos, k = nxt, k - tree[nxt]
return pos + 1
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
a = [0, 0] # a[0] 占位;a[1] = 0(第 1 头牛前面没有牛,未读入)
for _ in range(n - 1):
a.append(next(data)) # a[i](i≥2):第 i 头牛前面比它矮的数量
tree = [0] * (n + 1)
for i in range(1, n + 1):
tree[i] = i & -i # 初值即建树:所有身高都空闲,前缀 [1..i] 恰有 i 个空闲
log = n.bit_length() # 倍增跳的最大步长 2^log 覆盖 n
ans: list[int] = []
for i in range(n, 0, -1):
# 从后往前确定:第 i 头牛的身高是剩余身高里第 a[i]+1 小的
h = kth_free(tree, a[i] + 1, log)
ans.append(h)
p = h
while p <= n: # 树状数组单点 -1:身高 h 已被占用
tree[p] -= 1
p += p & -p
print('\n'.join(map(str, reversed(ans))))
if __name__ == "__main__":
solve()实现说明:
kth_free是树状数组上倍增:步长从降到 , tree[nxt] < k说明第小还在更右边,跳过去并把 扣掉这一段的前缀和; 结束时 pos + 1就是第小的空闲身高。 - 输入只有
个数: a数组长度为, a[1] = 0(第头牛前面没有牛), 这里的下标错位是最容易写错的坑。 - 逆序求出
ans后reversed输出。
复杂度
- 时间
:每头牛一次倍增定位( 步)+ 一次单点修改( 步)。 - 空间
。
总结
- 破题点是最后一头牛的身高被唯一确定,由此自然推出"从后往前 + 在剩余身高中取第
小"。 - 朴素实现是
的线性扫描,瓶颈在动态第 小查询; 树状数组 + 上倍增把它降到 ,且初始化可以用 i & -i一步建好。 - 本题是"树状数组上倍增找第
小"的标准入门模型:同样技巧适用于 权值第 小、逆序对还原排列等一类问题。