「Lost Cows」 谜一样的牛

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

从后往前逐位确定身高:第 i 头牛的身高是剩余空闲身高中第 A_i+1 小的,用树状数组维护 0/1 空闲标记并在其上倍增找第 k 小。

OJ: roj

题目 ID: 3111

难度:普及+/提高-

标签:树状数组二分python

创建: 2026-10-01 17:40

更新: 2026-10-10 11:30

形式化题目

设身高是 1∼n1 \sim n 的一个排列 h1,h2,…,hnh_1, h_2, \ldots, h_n。给定 A2,…,AnA_2, \ldots, A_n(第 11 头牛前面没有牛,A1=0A_1 = 0 未列出),其中 AiA_i 满足:

Ai=∣{ j∣j<i, hj<hi }∣A_i = \bigl|\{\, j \mid j < i,\ h_j < h_i \,\}\bigr|

即第 ii 头牛前面比它矮的牛的数量。要求还原整个排列 hh。

数据范围:1⩽n⩽1051 \leqslant n \leqslant 10^5。

暴力解法

思路

核心观察来自最后一头牛:它前面站着全部 n−1n-1 头牛,所以 AnA_n 就是全场比它矮的牛的数量,身高 hn=An+1h_n = A_n + 1 直接被锁死,没有任何自由度。

把这个观察推广到从后往前的每一个位置。处理到位置 ii 时,位置 i+1∼ni+1 \sim n 的牛 身高已经确定,可以认为这些身高被"占用"了。第 ii 头牛的身高应当是剩余空闲身高中 第 Ai+1A_i + 1 小的那个:

  • 比 hih_i 小的身高共 hi−1h_i - 1 个,其中恰有 AiA_i 个要排在它前面;
  • 剩下 hi−1−Aih_i - 1 - A_i 个更矮的身高留给后面的位置也没关系——后面的牛的约束 AjA_j 只统计"它前面"的牛,往位置 1∼i1 \sim i 里填什么更小的身高, 既不会冒出新的"比 hjh_j 矮且在前面"的牛(那些身高已被 AiA_i 的推导占位), 也不会消失,归纳可证每一步的选择都合法且唯一。

于是得到朴素做法:逆序扫描,每次在剩余身高集合中"找第 Ai+1A_i+1 小并删除"。 用有序数组维护,每次找第 kk 小要 O(n)O(n) 扫描,总复杂度 O(n2)O(n^2),n=105n = 10^5 时约 101010^{10} 次基本操作,不可行。

代码

复杂度与瓶颈

  • 时间 O(n2)O(n^2):每头牛一次线性扫描找第 kk 小 + 一次 O(n)O(n) 删除。
  • 空间 O(n)O(n)。
  • 瓶颈:动态集合上的"按名次查询"没有利用任何序结构,每次都从头数。

正解

关键观察

"在剩余身高集合中找第 kk 小并删除"是经典的可高效化结构:给每个身高一个 0/10/1 标记(11 = 空闲),用树状数组维护前缀和,则

pre(x)=身高 1∼x 中空闲的个数\text{pre}(x) = \text{身高 } 1 \sim x \text{ 中空闲的个数}

"第 kk 小的空闲身高"就是最小的 xx 使 pre(x)⩾k\text{pre}(x) \geqslant k。 由于 pre\text{pre} 对 xx 单调不减,可以在树状数组上倍增:从高位到低位贪心地 跳 2p2^p 步,只要跳完后的前缀和仍 <k< k 就跳过去并扣减 kk, 一次下降同时完成定位,复杂度 O(log⁡n)O(\log n)。

思路

  1. 初始化树状数组为全 11:tree[i] = i & -i 直接构造,前缀 [1..i][1..i] 恰有 ii 个空闲。
  2. 逆序枚举 i=n→1i = n \to 1,在树状数组上倍增找第 Ai+1A_i + 1 小的空闲身高 hih_i,记录答案。
  3. 把身高 hih_i 的标记减 11(树状数组单点修改),表示该身高已占用。
  4. 逆序求出的是 hn…h1h_n \ldots h_1,最后反转输出。

用样例 n=5n = 5,A=[_,1,2,1,0]A = [\_, 1, 2, 1, 0] 演示逆序推导:

位置 ii AiA_i 剩余空闲身高 取第 Ai+1A_i+1 小 hih_i
5 0 {1,2,3,4,5}\{1,2,3,4,5\} 第 1 小 1
4 1 {2,3,4,5}\{2,3,4,5\} 第 2 小 3
3 2 {2,4,5}\{2,4,5\} 第 2 小 4
2 1 {2,5}\{2,5\} 第 2 小 5
1 0 {2}\{2\} 第 1 小 2

表格从下往上读就是最终答案序列 2,4,5,3,12, 4, 5, 3, 1,与样例输出一致。 观察点:每一步"剩余空闲身高"恰好是"去掉比已确定身高更小的所有身高"后剩下的集合, 第 Ai+1A_i + 1 小自动保证了"前面有 AiA_i 头更矮的牛"。

代码

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 是树状数组上倍增:步长从 2log⁡n2^{\log n} 降到 11, tree[nxt] < k 说明第 kk 小还在更右边,跳过去并把 kk 扣掉这一段的前缀和; 结束时 pos + 1 就是第 kk 小的空闲身高。
  • 输入只有 n−1n - 1 个数:a 数组长度为 n+1n + 1,a[1] = 0(第 11 头牛前面没有牛), 这里的下标错位是最容易写错的坑。
  • 逆序求出 ans 后 reversed 输出。

复杂度

  • 时间 O(nlog⁡n)O(n \log n):每头牛一次倍增定位(log⁡n\log n 步)+ 一次单点修改(log⁡n\log n 步)。
  • 空间 O(n)O(n)。

总结

  • 破题点是最后一头牛的身高被唯一确定,由此自然推出"从后往前 + 在剩余身高中取第 Ai+1A_i+1 小"。
  • 朴素实现是 O(n2)O(n^2) 的线性扫描,瓶颈在动态第 kk 小查询; 树状数组 + 上倍增把它降到 O(nlog⁡n)O(n \log n),且初始化可以用 i & -i 一步建好。
  • 本题是"树状数组上倍增找第 kk 小"的标准入门模型:同样技巧适用于 权值第 kk 小、逆序对还原排列等一类问题。