题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(N\) 头牛按顺序排列,每头牛 \(i\) 有一个参数 \(c_i\)。每次队头牛拿到礼物后,插入到从队尾数第 \(c_i\) 头牛之前的位置。重复无限次后,求拿不到礼物的牛的数量。这是一个整数二分 + 贪心验证问题,关键在于发现:若某头牛最终能到达队首拿到礼物,则它前面的所有牛必须都能被"推"到它后面。

  2. 算法选择

    • 整数二分:二分查找能成为队首的最远位置 \(mid\),即前 \(mid\) 头牛都能拿到礼物
    • 贪心验证:将前 \(mid-1\) 头牛的 \(c_i\) 排序,模拟它们依次被插入到 \(mid\) 之后的过程。若某头牛的 \(c_i\) 大于当前 \(mid\) 之后的人数,则它只能插入到 \(mid\) 之前,导致 \(mid\) 无法成为队首
  3. 关键步骤

    • 读取输入\(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\)(拿不到礼物的牛数)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(\log N \cdot (N \log N))\),二分 \(O(\log N)\) 轮,每轮排序 \(O(N \log N)\)
    • 空间复杂度:\(O(N)\),临时数组
  5. 二分贪心验证的核心思想

    • 单调性:若位置 \(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
posted @ 2026-08-29 09:37  团爸讲算法  阅读(7)  评论(0)    收藏  举报