题解:洛谷 P10497 Lost Cows

【题目来源】

洛谷:P10497 [USACO03OPEN] Lost Cows - 洛谷

【题目描述】

\(N (2 \leq N \leq 8000)\) 头奶牛,它们在编号范围 \([1,N]\) 内具有唯一的标号。在一个极其糟糕的判断展示中,它们去了附近的“水坑”,在晚饭前喝了太多啤酒。到了排队吃晚饭的时候,它们没有按照编号升序的顺序排队。可惜的是,FJ 没有办法对它们进行排序。

此外,他在观察问题方面也不太擅长。他没有记下每头奶牛的编号,而是得到了一个相当愚蠢的统计数据:对于队伍中的每一头奶牛,他知道在这头奶牛之前,有多少头奶牛的编号确实比它小。给定这些数据,请告诉 FJ 奶牛的精确排列顺序。

【输入】

第 1 行,一个整数 \(N\)

第 2 到第 \(N\) 行,这 \(N-1\) 行描述了在队伍中某头奶牛之前有多少头奶牛的编号比她小。

当然,没有奶牛排在第 1 头奶牛前面,所以她没有被列出。第 2 行描述了排在第 2 个位置的奶牛之前,有多少奶牛的编号比她小;第 3 行描述了排在第 3 个位置的奶牛之前,有多少奶牛的编号比她小;依此类推。

【输出】

第 1 到第 \(N\) 行,输出的这 \(N\) 行中的每一行表示排队中的一头奶牛的编号。输出的第 1 行表示排在队伍第 1 个位置的奶牛的编号;第 2 行表示排在第 2 个位置的奶牛的编号;依此类推。

【输入样例】

5
1
2
1
0

【输出样例】

2
4
5
3
1

【核心思想】

  1. 问题分析:有 \(N\) 头编号为 \(1 \sim N\) 的奶牛排成一列,但顺序被打乱。对于队伍中的每头奶牛(从第 2 头开始),我们知道在它前面有多少头奶牛的编号比它小,记作 \(a_i\)(即第 \(i\) 个位置上的奶牛,其前面有 \(a_i\) 头奶牛编号小于它)。需要根据这些信息还原出完整的排列。本质:这是一个经典的逆序对数量还原排列问题。从后往前处理,对于第 \(i\) 个位置,它在剩余未确定编号的奶牛中,应该是第 \(a_i + 1\) 小的(因为前面有 \(a_i\) 个比它小,所以它是当前剩余编号中的第 \(a_i+1\) 小)。用树状数组维护剩余编号的“存在性”,支持快速查询第 \(k\) 小的可用编号。

  2. 算法选择

    • 树状数组 + 二分查找:维护一个长度为 \(N\) 的布尔数组,表示每个编号是否尚未被使用(初始全为 1)。从后往前确定每个位置的编号。对位置 \(i\),它应该是当前剩余编号中第 \(a_i + 1\) 小的数。使用树状数组维护前缀和(即已使用的数量),然后二分查找使得前缀和 \(\ge a_i + 1\) 的最小位置,即为该编号。找到后将其从树状数组中删除(加 -1)。
  3. 关键步骤

    • 读入\(N\),以及 \(a_2 \ldots a_N\)\(a_1\) 默认为 0,因为第一头牛前面没有牛)。
    • 初始化树状数组:将所有 \(1 \sim N\) 的编号都标记为可用(add(i, 1))。
    • 从后往前遍历 \(i = N\)\(1\)
      • 计算目标排名 \(k = a_i + 1\)(即当前奶牛在剩余编号中第 \(k\) 小)。
      • 在树状数组上二分查找最小的 \(pos\),使得 query(pos) >= k。这个 \(pos\) 就是当前奶牛的编号。
      • ans[i] = pos
      • 将该编号从树状数组中移除:add(pos, -1)
    • 输出\(ans[1..N]\) 按顺序输出。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),每次二分查找 \(O(\log N)\),树状数组操作 \(O(\log N)\)
    • 空间复杂度:\(O(N)\)
  5. 树状数组 + 第 k 小查询的核心思想

    • 逆序还原:因为给定的是“前面比它小的数量”,从后往前确定时,后面奶牛的信息已经确定并移除了,当前奶牛的相对排名在剩余编号中就是 \(a_i + 1\)
    • 动态集合维护:需要支持删除元素和查询第 \(k\) 小元素,树状数组 + 二分查找是经典解决方案。
    • 适用场景:适用于已知每个元素在剩余元素中的排名,需要还原原始排列的问题。核心是动态维护一个有序集合。

【算法标签】

普及- #树状数组

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 100005;  // 最大数组大小

int n;                 // 人数
int h[N];             // h[i]: 第i个人前面比它高的人数
int ans[N];           // ans[i]: 第i个人的实际身高排名
int tr[N];            // 树状数组,用于统计剩余可用的身高排名

/**
 * 计算lowbit:获取x的最低位的1
 * @param x 输入数值
 * @return x的最低位的1所代表的值
 */
int lowbit(int x)
{
    return x & -x;  // 利用补码性质
}

/**
 * 树状数组单点更新操作
 * @param x 更新位置
 * @param c 增加的值
 */
void add(int x, int c)
{
    // 树状数组标准更新操作,向上更新所有相关节点
    for (int i = x; i <= n; i += lowbit(i))
        tr[i] += c;
}

/**
 * 树状数组前缀和查询操作
 * @param x 查询位置
 * @return 前x个位置的和
 */
int query(int x)
{
    int res = 0;
    // 树状数组标准查询操作,向下累加所有相关节点
    for (int i = x; i; i -= lowbit(i))
        res += tr[i];
    return res;
}

int main()
{
    // 输入人数
    cin >> n;

    // 读入每个人前面比它高的人数(从第2个人开始)
    for (int i = 2; i <= n; i++)
        cin >> h[i];

    // 初始化树状数组:所有身高排名都可用(标记为1)
    for (int i = 1; i <= n; i++)
        add(i, 1);

    // 从后往前处理每个人(从第n个人到第1个人)
    for (int i = n; i; i--)
    {
        int k = h[i] + 1;  // 计算当前人应该在第k个可用的身高排名
        int l = 1, r = n;  // 二分查找边界
        int ans1;          // 存储二分查找结果

        // 二分查找:找到第k个可用的身高排名
        while (l <= r)
        {
            int mid = (l + r) >> 1;  // 计算中点

            if (query(mid) >= k)
            {
                // 前mid个位置中至少有k个可用排名
                ans1 = mid;      // 记录当前位置
                r = mid - 1;     // 继续向左查找更小的满足条件的位置
            }
            else
            {
                l = mid + 1;     // 向右查找
            }
        }

        // 记录第i个人的身高排名
        ans[i] = ans1;

        // 标记该身高排名已被使用
        add(ans1, -1);
    }

    // 输出每个人的实际身高排名(从矮到高,排名越小越矮)
    for (int i = 1; i <= n; i++)
        cout << ans[i] << endl;

    return 0;
}

【运行结果】

5
1
2
1
0
2
4
5
3
1
posted @ 2026-08-29 09:36  团爸讲算法  阅读(3)  评论(0)    收藏  举报