题解:洛谷 P2214 Mooo Moo

【题目来源】

洛谷:P2214 [USACO14MAR] Mooo Moo S - 洛谷

【题目描述】

FJ 的 \(N(1\le N\le100)\) 个牧场都是沿着一条笔直的道路分布的。每一个牧场可能有许多种品种的奶牛;FJ 拥有 \(B(1\le B\le20)\) 个不同品种的奶牛,而第 \(i\) 种奶牛的叫声音量为 \(V_i(1\le V_i\le100)\)。此外,有一股强风沿着道路吹来,将牛的叫声从左往右传递,如果某个牧场的总音量是 \(x\),那么它将传递 \(x-1\) 的音量到右边的下一个牧场。这就意味着,一个牧场里的总音量是处在该牧场的奶牛所发出的音量加上左边前一个牧场的总音量 \(-1\)。数据保证,每一个牧场内由该牧场所有奶牛所发出的总音量最多为 \(10^5\)

【输入】

\(1\) 行:两个用空格分隔的整数 \(N\)\(B\)
\(2 \sim B+1\) 行:第 \(i+1\) 行包含整数 \(V_i\)
\(B+2 \sim B+N+1\) 行:第 \(B+i+1\) 行表示在第 \(i\) 个牧场内所能监听到的总音量。

【输出】

共一行,即 FJ 拥有的最小奶牛数量。

如果 FJ 不可能拥有一种牧场配置满足给出的条件,输出 \(-1\)

【输入样例】

5 2
5
7
0
17
16
20
19

【输出样例】

4

【核心思想】

  1. 问题分析:给定 \(N\) 个牧场沿直线排列,每个牧场有监听到的总音量 \(a_i\)。音量从左往右传递,若某牧场总音量为 \(x\),则向右传递 \(x-1\)。每个牧场音量 = 该牧场奶牛音量之和 + 左邻牧场传递来的音量 \(-1\)。已知 \(B\) 种奶牛的音量 \(V_i\),求满足所有牧场监听音量的最少奶牛总数。这是一个完全背包 + 递推分解问题,关键在于将每个牧场的"净需求"分解出来,再用完全背包求每种净需求的最少奶牛数。

  2. 算法选择

    • 递推分解净需求:第 \(i\) 个牧场的净需求 \(b_i = a_i - \max(a_{i-1} - 1, 0)\),即扣除左邻传递后的实际需补充音量
    • 完全背包:每种奶牛可无限使用,求凑成每个 \(b_i\) 所需的最少奶牛数
    • 不可达判断:若某 \(b_i\) 无法用现有奶牛凑出,输出 \(-1\)
  3. 关键步骤

    • 读取输入\(N, B\)\(B\) 种奶牛音量 \(V[1..B]\)\(N\) 个牧场监听音量 \(a[1..N]\)
    • 计算净需求\(i\)\(1\)\(N\)):
      • \(b_i = a_i\)\(i = 1\)\(a_{i-1} = 0\)
      • \(b_i = a_i - (a_{i-1} - 1)\)(否则,扣除左邻传递的 \(a_{i-1} - 1\)
    • 完全背包预处理
      • \(dp[j] = \infty\)\(j\)\(1\)\(10^5\)),\(dp[0] = 0\)
      • 遍历每种奶牛 \(V_i\):对于 \(j\)\(V_i\)\(10^5\)\(dp[j] = \min(dp[j],\ dp[j - V_i] + 1)\)
    • 累加答案\(ans = \sum_{i=1}^{N} dp[b_i]\),若某 \(dp[b_i] = \infty\) 则输出 \(-1\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(B \cdot 10^5 + N)\),完全背包预处理 + 净需求累加
    • 空间复杂度:\(O(10^5)\)\(dp\) 数组
  5. 完全背包的核心思想

    • 无限物品选择:每种奶牛可重复使用,完全背包的递推顺序(正序遍历容量)保证每种物品可被多次选取
    • 递推分解传递效应:将链式传递的音量影响分解为每个牧场的独立净需求,避免复杂的链式模拟
    • 最优子结构\(dp[j]\) 表示凑成音量 \(j\) 的最少奶牛数,满足 \(dp[j] = \min_{V_i \le j}(dp[j - V_i] + 1)\)
    • 适用于"无限物品凑数 + 链式传递分解"类问题,核心是将链式影响转化为独立子问题后用背包求解

【算法标签】

普及 #完全背包

【代码详解】

// 只能拿100分,无法AC,一个RE,一个WA

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

const int N = 100005;  // 最大容量
int n, B;  // n: 关卡数量,B: 药水类型数量
int a[105], b[105], dp[N], v[25];  // a: 原始能量需求,b: 处理后的能量需求,dp: 动态规划数组,v: 药水能量值

int main()
{
    cin >> n >> B;  // 读入关卡数量和药水类型数量

    // 读入每种药水提供的能量值
    for (int i = 1; i <= B; i++)
    {
        cin >> v[i];
    }

    // 读入每个关卡的能量需求
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 计算每个关卡实际需要的能量
    for (int i = 1; i <= n; i++)
    {
        if (a[i - 1] > 0)  // 如果前一个关卡有剩余能量
        {
            b[i] = a[i] - (a[i - 1] - 1);  // 当前关卡需要额外补充的能量
        }
        else
        {
            b[i] = a[i];  // 否则需要全部能量
        }
    }

    // 初始化动态规划数组为无穷大
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;  // 获得0能量的成本为0

    // 完全背包:计算获得各种能量值所需的最少药水数量
    for (int i = 1; i <= B; i++)
    {
        for (int j = v[i]; j <= 100000; j++)
        {
            dp[j] = min(dp[j], dp[j - v[i]] + 1);
        }
    }

    int ans = 0;
    // 计算所有关卡所需的总药水数量
    for (int i = 1; i <= n; i++)
    {
        if (dp[b[i]] != 0x3f3f3f3f)  // 如果该能量值可达
        {
            ans += dp[b[i]];
        }
        else  // 如果不可达,输出-1并结束
        {
            cout << -1 << endl;
            return 0;
        }
    }

    cout << ans << endl;  // 输出总药水数量
    return 0;
}

【运行结果】

5 2
5
7
0
17
16
20
19
4
posted @ 2026-08-28 15:54  团爸讲算法  阅读(6)  评论(0)    收藏  举报