题解:洛谷 P2376 Allowance

【题目来源】

洛谷:P2376 [USACO05OCT] Allowance S / [USACO09OCT] Allowance G - 洛谷

【题目描述】

作为创造产奶纪录的回报,Farmer John 决定开始每个星期给 Bessie 一点零花钱。

FJ 有一些硬币,一共有 \(N\ (1 \le N \le 20)\) 种不同的面额。每一个面额都能整除所有比它大的面额。

他想用给定的硬币的集合,每个星期至少给 Bessie 某个零花钱的数目 \(C\ (1 \le C \le 10^8)\)。请帮他计算他最多能支付多少个星期的零花钱。

【输入】

\(1\) 行:两个由空格隔开的整数:\(N\)\(C\)

\(2\) 到第 \(N+1\) 行:每一行有两个整数表示一个面额的硬币:硬币面额 \(V\ (1 \le V \le 10^8)\) 拥有的该面额的硬币数 \(B\ (1 \le B \le 10^6)\)

【输出】

\(1\) 行:一个单独的整数,表示最多能支付多少个星期的零花钱。

【输入样例】

3 6
10 1
1 100
5 120

【输出样例】

111

【核心思想】

  1. 问题分析:给定 \(N\) 种硬币,每种有面额 \(V_i\) 和数量 \(B_i\),且小面额能整除大面额。每周需支付至少 \(C\) 元,求最多能支付多少周。这是一个贪心 + 批量扣除问题,关键在于利用面额的整除关系,设计两阶段贪心策略构造每周的最优支付方案,然后批量扣除可重复的方案。

  2. 算法选择

    • 两阶段贪心构造方案
      • 阶段一(从大到小):尽量用大面额凑接近 \(C\) 但不超过 \(C\) 的金额
      • 阶段二(从小到大补齐):若阶段一后仍有缺口,用小面额补齐至 \(\ge C\)
    • 批量扣除:找到当前方案中各硬币使用数量的最小倍数,一次性扣除多周
  3. 关键步骤

    • 读取输入\(N, C\) 和每种硬币的 \((V_i, B_i)\)
    • 排序:按面额从小到大排序
    • 主循环(直到总金额 \(< C\)):
      • init():计算剩余硬币总价值 \(sum\)
      • \(sum < C\):跳出循环
      • calc() 构造支付方案:
        • 从大到小:\(b[i] = \min(B_i,\ \lfloor rest / V_i \rfloor)\)\(rest \mathrel{-}= b[i] \times V_i\)
        • 从小到大:\(b[i] \mathrel{+}= \min(B_i - b[i],\ \lceil rest / V_i \rceil)\)\(rest \mathrel{-}= b[i] \times V_i\)
      • del() 批量扣除:
        • \(minn = \min_{b[i] > 0} (B_i / b[i])\),即当前方案最多可重复次数
        • \(ans \mathrel{+}= minn\)\(B_i \mathrel{-}= minn \times b[i]\)
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \cdot \log(\max B_i))\),每轮批量扣除至少清空一种硬币,轮数受限于 \(N\)
    • 空间复杂度:\(O(N)\),硬币信息和方案数组
  5. 贪心批量扣除的核心思想

    • 整除关系的优势:小面额能整除大面额,保证贪心时不会出现"用大面额导致小面额无法精确补齐"的情况
    • 两阶段互补:阶段一尽量接近目标减少浪费,阶段二确保达到最低支付额 \(C\)
    • 批量优化:找到方案的重复次数 \(minn\),一次性扣除多周,避免逐周模拟的低效
    • 方案稳定性:由于面额整除,同一方案可重复执行直到某种硬币耗尽,保证批量扣除的正确性
    • 适用于"有限硬币 + 面额整除关系 + 重复支付"类问题,核心是利用整除性设计可批量执行的贪心方案

【算法标签】

普及+ #贪心

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 25;
int n, c, ans;  // n: 货币种类数, c: 总金额, ans: 最大支付次数
int b[N];  // 当前支付的货币数量
struct Node
{
    int v, b;  // v: 面值, b: 数量
} a[N];

bool cmp(Node x, Node y)  // 按面值从小到大排序
{
    return x.v < y.v;
}

// 计算剩余总金额
int init()
{
    int res = 0;
    for (int i = 1; i <= n; i++)
    {
        res += a[i].v * a[i].b;
    }
    return res;
}

// 计算当前支付方案
void calc()
{
    int rest = c;  // 剩余需要支付的金额
    // 贪心1:从大到小尽量用
    for (int i = n; i >= 1; i--)
    {
        int k = rest / a[i].v;  // 最多能用几张
        k = min(k, a[i].b);  // 不超过库存
        rest -= k * a[i].v;  // 扣除金额
        b[i] = k;  // 记录使用数量
    }
    // 贪心2:如果还有剩余,从小到大补齐
    for (int i = 1; i <= n; i++)
    {
        int k = ceil(1.0 * rest / a[i].v);  // 最少需要几张
        k = min(k, a[i].b - b[i]);  // 不超过剩余库存
        rest -= k * a[i].v;  // 扣除金额
        b[i] += k;  // 增加使用数量
    }
}

// 删除可支付的最大次数
void del()
{
    int minn = 1e9;  // 最多能支付几次
    for (int i = 1; i <= n; i++)
    {
        if (!b[i])  // 没用这种货币
        {
            continue;
        }
        minn = min(minn, a[i].b / b[i]);  // 这种货币能支付几次
    }
    ans += minn;  // 累计支付次数
    for (int i = 1; i <= n; i++)
    {
        a[i].b -= minn * b[i];  // 扣除已支付的货币
    }
}
signed main()
{
    cin >> n >> c;  // 输入货币种类数和总金额
    for (int i = 1; i <= n; i++)  // 输入每种货币的面值和数量
    {
        cin >> a[i].v >> a[i].b;
    }
    sort(a + 1, a + n + 1, cmp);  // 按面值从小到大排序

    while (true)
    {
        memset(b, 0, sizeof(b));  // 清空支付方案
        int sum = init();  // 计算剩余总金额
        if (sum < c)  // 如果不够支付一次
        {
            break;
        }
        calc();  // 计算支付方案
        del();  // 扣除可支付的次数
    }
    cout << ans << endl;  // 输出最大支付次数
    return 0;
}

【运行结果】

3 6
10 1
1 100
5 120
111
posted @ 2026-08-28 15:54  团爸讲算法  阅读(2)  评论(0)    收藏  举报