题解:洛谷 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
【核心思想】
-
问题分析:有 \(N\) 头编号为 \(1 \sim N\) 的奶牛排成一列,但顺序被打乱。对于队伍中的每头奶牛(从第 2 头开始),我们知道在它前面有多少头奶牛的编号比它小,记作 \(a_i\)(即第 \(i\) 个位置上的奶牛,其前面有 \(a_i\) 头奶牛编号小于它)。需要根据这些信息还原出完整的排列。本质:这是一个经典的逆序对数量还原排列问题。从后往前处理,对于第 \(i\) 个位置,它在剩余未确定编号的奶牛中,应该是第 \(a_i + 1\) 小的(因为前面有 \(a_i\) 个比它小,所以它是当前剩余编号中的第 \(a_i+1\) 小)。用树状数组维护剩余编号的“存在性”,支持快速查询第 \(k\) 小的可用编号。
-
算法选择:
- 树状数组 + 二分查找:维护一个长度为 \(N\) 的布尔数组,表示每个编号是否尚未被使用(初始全为 1)。从后往前确定每个位置的编号。对位置 \(i\),它应该是当前剩余编号中第 \(a_i + 1\) 小的数。使用树状数组维护前缀和(即已使用的数量),然后二分查找使得前缀和 \(\ge a_i + 1\) 的最小位置,即为该编号。找到后将其从树状数组中删除(加 -1)。
-
关键步骤:
- 读入:\(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]\) 按顺序输出。
-
时间/空间复杂度:
- 时间复杂度:\(O(N \log N)\),每次二分查找 \(O(\log N)\),树状数组操作 \(O(\log N)\)。
- 空间复杂度:\(O(N)\)。
-
树状数组 + 第 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
浙公网安备 33010602011771号