题解:学而思编程 监听音量

【题目来源】

学而思编程:监听音量

【题目描述】

农夫约翰完全忘了他有多少头牛了!他不好意思到牧场里去数牛,因为他不想让牛意识到他的健忘。取而代之的是,他决定在奶牛聚集的牧场里安装麦克风,秘密计算出他能从中听到的所有牛叫声的总音量,以便以此确定奶牛的数量。

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

给出FJ在每个牧场能监听到的总音量,请计算FJ最小可能拥有的奶牛数量。

【输入】

第一行包含两个整数 \(N\)\(B\)

接下来 \(B\) 行,每行一个整数,表示第ii种奶牛的叫声音量 \(V_i\)

再接下来 \(N\) 行,每行一个整数,表示从左到右第 \(i\) 个牧场能监听到的总音量。

【输出】

输出一行,表示FJ拥有的最小奶牛数量。

如果不存在方案,使得其满足监听到的音量序列,输出 \(−1\)

【输入样例】

5 2
5
7
0
17
16
20
19

【输出样例】

4

【核心思想】

  1. 问题分析:给定 \(N\) 个牧场和 \(B\) 种奶牛叫声音量 \(V_i\)。每个牧场的总音量由该牧场的奶牛音量加上左边牧场的传递音量(减1)组成。需要根据监听到的总音量序列,计算最小可能拥有的奶牛数量。这是一个完全背包预处理 + 净额计算问题,关键在于理解音量传递规则并计算每次需要补充的净额。

  2. 算法选择

    • 净额计算:根据音量传递规则,计算每个牧场需要补充的音量(即奶牛发出的音量)
    • 完全背包预处理:计算凑出每个金额所需的最少硬币数(奶牛数)
    • 累加答案:查询每个净额对应的硬币数并累加
  3. 关键步骤

    • 净额计算:对于第 \(i\) 个牧场
      • 若上次(第 \(i-1\) 个牧场)有余额(\(a[i-1] > 0\)),则本次需要支付 \(c[i] = a[i] - a[i-1] + 1\)(因为传递过来要减1)
      • 若上次没有余额,则 \(c[i] = a[i] - a[i-1]\)
    • 完全背包预处理dp[j] 表示凑出金额 \(j\) 所需的最少硬币数
      • 初始化:dp[0] = 0,其余为无穷大
      • 转移:dp[j] = min(dp[j], dp[j - V[i]] + 1)(正向遍历,完全背包)
    • 累加答案:对于每个净额 \(c[i]\),若 dp[c[i]] 可达,则 cnt += dp[c[i]],否则输出 \(-1\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(B \times M + N)\),其中 \(M = 10^5\) 为最大金额,完全背包预处理 \(O(B \times M)\),累加答案 \(O(N)\)
    • 空间复杂度:\(O(M)\),一维DP数组
  5. 完全背包预处理 + 净额计算的核心思想

    • 问题转化:将音量传递问题转化为多阶段付款问题,每个牧场需要补充一定的音量
    • 净额计算:根据传递规则(减1)计算每个阶段实际需要补充的金额
    • 完全背包预处理:先计算出凑出所有可能金额的最少硬币数,避免重复计算
    • 贪心累加:每个阶段独立计算最少硬币数,累加得到总数
    • 适用于"多阶段付款、每阶段最少硬币数"的问题

【算法标签】

完全背包

【代码详解】

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 105;
int n, b, v[N], a[N], c[N], dp[100005], cnt;  // dp: 完全背包DP数组, cnt: 总硬币数

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])  // 如果上次有余额
        {
            c[i] = a[i] - a[i - 1] + 1;  // 需要支付的金额
        }
        else
        {
            c[i] = a[i] - a[i - 1];  // 不需要找零的情况
        }
    }
    // 调试输出
    // for (int i=1;i<=n;i++)
    // 	cout<<c[i]<<' ';

    // 完全背包:计算凑出每个金额所需的最小硬币数
    memset(dp, 0x3f, sizeof(dp));  // 初始化为无穷大
    dp[0] = 0;
    for (int i = 1; i <= b; i++)
    {
        for (int j = v[i]; j <= 1e5; j++)
        {
            dp[j] = min(dp[j], dp[j - v[i]] + 1);
        }
    }

    // 计算总硬币数
    for (int i = 1; i <= n; i++)
    {
        if (dp[c[i]] != 0x3f3f3f3f)  // 如果可以凑出
        {
            cnt += dp[c[i]];
        }
        else
        {
            cout << -1;  // 无法凑出
            return 0;
        }
    }
    cout << cnt;
    return 0;
}

【运行结果】

5 2
5
7
0
17
16
20
19
4
posted @ 2026-06-14 10:47  团爸讲算法  阅读(7)  评论(0)    收藏  举报