题解:洛谷 P4090 Greedy Gift Takers
【题目来源】
洛谷:P4090 [USACO17DEC] Greedy Gift Takers P - 洛谷
【题目描述】
有 \(N(1≤N≤10^5)\)头牛按顺序排成一列,编号从 \(1\) 到 \(N\),\(1\) 号牛在队头,\(N\) 号牛在队尾。
每次位于队头的牛 \(i\) 拿到一个礼物,然后插入到从队尾数\(c_i\)头牛之前的位置。举个栗子初始队列 \(1,2,3,4,5\) \(c_1 = 2\),\(c_2 = 3\),则第一次操作后的序列为 \(2,3,1,4,5\),第二次操作后的序列为 \(3,2,1,4,5\)。重复无限次操作,求最后有几头牛拿不到礼物。
【输入】
The first line contains a single integer, \(N\).
The second line contains \(N\) space-separated integers \(c_1, c_2, \dots, c_N\).
【输出】
Please output the number of cows who cannot receive any gifts.
【输入样例】
3
1 2 0
【输出样例】
1
【核心思想】
-
问题分析:给定 \(N\) 头牛按顺序排列,每头牛 \(i\) 有一个参数 \(c_i\)。每次队头牛拿到礼物后,插入到从队尾数第 \(c_i\) 头牛之前的位置。重复无限次后,求拿不到礼物的牛的数量。这是一个整数二分 + 贪心验证问题,关键在于发现:若某头牛最终能到达队首拿到礼物,则它前面的所有牛必须都能被"推"到它后面。
-
算法选择:
- 整数二分:二分查找能成为队首的最远位置 \(mid\),即前 \(mid\) 头牛都能拿到礼物
- 贪心验证:将前 \(mid-1\) 头牛的 \(c_i\) 排序,模拟它们依次被插入到 \(mid\) 之后的过程。若某头牛的 \(c_i\) 大于当前 \(mid\) 之后的人数,则它只能插入到 \(mid\) 之前,导致 \(mid\) 无法成为队首
-
关键步骤:
- 读取输入:\(N\) 和 \(c[1..N]\)
- 二分初始化:\(l = 0\),\(r = N + 1\)
- 二分过程(\(l + 1 < r\)):
- \(mid = (l + r) / 2\)
- 若
check(mid):\(l = mid\) - 否则:\(r = mid\)
- check 函数:
- 复制 \(c[1..mid-1]\) 到 \(b\) 数组并排序
- \(num = N - mid\)(\(mid\) 之后的人数)
- 遍历排序后的 \(b[1..mid-1]\):
- 若 \(b[i] \le num\):该牛可插入到 \(mid\) 之后,\(num \mathrel{+}= 1\)(\(mid\) 位置相对后移)
- 否则:返回
false(\(mid\) 无法成为队首)
- 返回
true
- 输出 \(N - l\)(拿不到礼物的牛数)
-
时间/空间复杂度:
- 时间复杂度:\(O(\log N \cdot (N \log N))\),二分 \(O(\log N)\) 轮,每轮排序 \(O(N \log N)\)
- 空间复杂度:\(O(N)\),临时数组
-
二分贪心验证的核心思想:
- 单调性:若位置 \(mid\) 能成为队首,则所有 \(< mid\) 的位置也能成为队首;若不能,则所有 \(> mid\) 的位置也不能。满足二分条件
- 贪心排序:将前 \(mid-1\) 头牛的 \(c_i\) 排序后,优先让 \(c_i\) 小的牛先插入(更容易满足 \(c_i \le num\)),这是验证可行性的最优策略
- 位置后移效应:每有一头牛插入到 \(mid\) 之后,\(mid\) 的相对位置后移一位(\(num\) 增加),形成正向反馈
- 适用于"队列操作 + 可行性判定"类问题,核心是通过二分将问题转化为可贪心验证的形式
【算法标签】
提高+ #整数二分
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 100005; // 定义数组最大长度
int n; // 队伍总人数
int c[MAX_N]; // 存储每个人的能力值
int b[MAX_N]; // 临时数组,用于排序处理
// 检查mid位置是否能成为队首
bool check(int mid)
{
// 将mid之前的人复制到临时数组
for (int i = 1; i < mid; i++)
b[i] = c[i];
// 对mid之前的人进行排序(贪心策略)
sort(b + 1, b + mid);
int num = n - mid; // mid之后的人数
// 检查mid之前的人是否能被插入到mid之后
for (int i = 1; i < mid; i++)
{
if (b[i] <= num) // 可以插入到mid之后
num++; // mid位置前移
else // 必须插入到mid之前
return false; // mid不能成为队首
}
return true; // mid可以成为队首
}
int main()
{
// 输入队伍总人数
cin >> n;
// 输入每个人的能力值
for (int i = 1; i <= n; i++)
cin >> c[i];
// 二分查找能成为队首的最远位置
int l = 0, r = n + 1;
while (l + 1 < r)
{
int mid = (l + r) / 2;
if (check(mid)) // 如果mid可以成为队首
l = mid; // 尝试更大的mid
else
r = mid; // 尝试更小的mid
}
// 输出需要移动的人数(总人数-能成为队首的位置)
cout << n - l << endl;
return 0;
}
【运行结果】
3
1 2 0
1
浙公网安备 33010602011771号