题解:洛谷 P1677 HoofBall

【题目来源】

洛谷:P1677 [USACO18FEB] Hoofball B - 洛谷(luogu.com.cn)

【题目描述】

为了准备即将到来的蹄球锦标赛,Farmer John正在训练他的N头奶牛(方便起见,编号为\(1\dots N\),其中\(1\le N\le 100\))进行传球。这些奶牛在牛棚一侧沿直线排列,第\(i\)号奶牛位于距离牛棚\(x_i\)的地方\((1\le x_i\le 1000)\)。每头奶牛都在不同的位置上。

在训练开始的时候,Farmer John会将若干个球传给不同的奶牛。当第\(i\)号奶牛接到球时,无论是从Farmer John或是从另一头奶牛传来的,她会将球传给最近的奶牛(如果有多头奶牛与她距离相同,她会传给其中距左边最远的那头奶牛)。为了使所有奶牛都有机会练习到传球,Farmer John想要确保每头奶牛都持球至少一次。帮助他求出为了达到这一目的他开始时至少要传出的球的数量。假设他在开始的时候能将球传给最适当的一组奶牛。

【输入】

输入的第一行包含\(N\)。第二行包含\(N\)个用空格分隔的整数,其中第\(i\)个整数为\(x_i\)

【输出】

输出Farmer John开始的时候最少需要传出的球的数量,使得所有奶牛至少持球一次。

【输入样例】

5
7 1 3 11 4

【输出样例】

2

【核心思想】

  1. 问题分析:给定 \(N\) 头奶牛在直线上的位置,每头奶牛接球后会传给最近的奶牛(距离相同则传给左边最远的)。求 Farmer John 初始至少需要传出多少个球,使得每头奶牛至少持球一次。这是一个模拟 + 图论分析问题,关键在于将传球规则建模为每个节点的出边,分析入度为 \(0\) 的节点和互相指向的环。

  2. 算法选择

    • 排序 + 最近邻确定:先对位置排序,每头奶牛的最近邻只可能在左右相邻的两头奶牛中产生
    • 入度统计\(cnt[i]\) 记录指向奶牛 \(i\) 的奶牛数量,入度为 \(0\) 的奶牛必须被初始传球
    • 双向环检测:若相邻两头奶牛互相指向(\(nxt[i] = i+1\)\(nxt[i+1] = i\)),形成一个 \(2\)-元环,需要额外放一个球打破循环
  3. 关键步骤

    • 读取输入\(N\) 和位置数组 \(a[1..N]\)
    • 排序sort(a+1, a+N+1),使位置有序
    • 确定传球方向
      • 端点:\(nxt[1] = 2\)\(nxt[N] = N-1\)(边界奶牛只有一个邻居)
      • 中间点(\(i\)\(2\)\(N-1\)):
        • \(a[i] - a[i-1] > a[i+1] - a[i]\):右边更近,\(nxt[i] = i+1\)\(cnt[i+1] \mathrel{+}= 1\)
        • 否则:左边更近(或相等时选左边),\(nxt[i] = i-1\)\(cnt[i-1] \mathrel{+}= 1\)
    • 统计答案
      • 第一轮:遍历 \(i\)\(1\)\(N\),若 \(cnt[i] = 0\)(无牛传给它),\(ans \mathrel{+}= 1\)
      • 第二轮:遍历 \(i\)\(1\)\(N-1\),若 \(cnt[i] = 1\)\(cnt[i+1] = 1\) 且互相指向,\(ans \mathrel{+}= 1\)
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),排序主导
    • 空间复杂度:\(O(N)\),位置数组、方向数组、入度数组
  5. 传球图分析的核心思想

    • 功能图性质:每个节点出度为 \(1\),图由若干基环树组成,环上节点互相传球形成闭环
    • 入度为 \(0\) 必放球:没有牛传向它的奶牛无法通过传球链获得球,必须初始持球
    • 二元环特判:两个相邻奶牛互相指向时,虽然各自入度为 \(1\),但形成孤立闭环,需要额外放球确保两者都被覆盖
    • 排序降维:排序后最近邻只可能是左右相邻点,将 \(O(N^2)\) 的最近邻搜索降为 \(O(N)\)
    • 适用于"最近邻传递 + 覆盖所有节点"类问题,核心是分析功能图的入度与环结构

【解题思路】

【算法标签】

普及- #模拟

【代码详解】

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

int n;              // 奶牛的数量
int a[105];         // 存储奶牛的位置坐标
int cnt[105];       // 计数器,记录每个位置被指向的次数
int nxt[105];       // 记录每个奶牛传球的方向(下一个奶牛的位置)
int ans = 0;        // 需要的最少球数

// 文件输入输出流(注释掉的代码)
// ifstream filein("hoofball.in");
// ofstream fileout("hoofball.out");

int main()
{
    // 输入奶牛数量
    cin >> n;

    // 输入奶牛的位置坐标
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 对奶牛位置进行升序排序
    sort(a + 1, a + n + 1);

    // 处理左端点(第一个奶牛)
    nxt[1] = 2;      // 最左边的奶牛只能向右传球
    cnt[2] = 1;      // 第二个奶牛被指向一次

    // 处理右端点(最后一个奶牛)
    nxt[n] = n - 1;  // 最右边的奶牛只能向左传球
    cnt[n - 1] = 1;  // 倒数第二个奶牛被指向一次

    // 处理中间位置的奶牛(从第2个到倒数第2个)
    for (int i = 2; i < n; i++)
    {
        // 比较向左和向右的距离,选择较近的方向
        if (a[i] - a[i - 1] > a[i + 1] - a[i])
        {
            // 向右传球(右边更近)
            nxt[i] = i + 1;  // 记录传球方向为右边
            cnt[i + 1]++;     // 右边奶牛被指向次数加1
        }
        else
        {
            // 向左传球(左边更近)
            nxt[i] = i - 1;  // 记录传球方向为左边
            cnt[i - 1]++;     // 左边奶牛被指向次数加1
        }
    }

    // 第一轮统计:找出没有被指向的奶牛(需要放球的位置)
    for (int i = 1; i <= n; i++)
    {
        // 如果某个奶牛没有被任何其他奶牛指向(cnt[i]==0)
        // 说明需要在这个位置放一个球
        if (cnt[i] == 0)
        {
            ans++;
        }
    }

    // 第二轮统计:处理互相传球的情况(循环传球)
    for (int i = 1; i < n; i++)
    {
        // 检查相邻两个奶牛是否形成互相传球的关系
        if (cnt[i] == 1 && cnt[i + 1] == 1 &&    // 两个奶牛都只被指向一次
            nxt[i] == i + 1 && nxt[i + 1] == i)  // 互相指向对方
        {
            // 这种循环传球的情况需要额外放一个球
            ans++;
        }
    }

    // 输出最少需要的球数
    cout << ans << endl;

    return 0;
}

【运行结果】

5
7 1 3 11 4
2
posted @ 2026-08-27 11:07  团爸讲算法  阅读(4)  评论(0)    收藏  举报