题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N\) 种硬币,每种有面额 \(V_i\) 和数量 \(B_i\),且小面额能整除大面额。每周需支付至少 \(C\) 元,求最多能支付多少周。这是一个贪心 + 批量扣除问题,关键在于利用面额的整除关系,设计两阶段贪心策略构造每周的最优支付方案,然后批量扣除可重复的方案。
-
算法选择:
- 两阶段贪心构造方案:
- 阶段一(从大到小):尽量用大面额凑接近 \(C\) 但不超过 \(C\) 的金额
- 阶段二(从小到大补齐):若阶段一后仍有缺口,用小面额补齐至 \(\ge C\)
- 批量扣除:找到当前方案中各硬币使用数量的最小倍数,一次性扣除多周
- 两阶段贪心构造方案:
-
关键步骤:
- 读取输入:\(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\)
-
时间/空间复杂度:
- 时间复杂度:\(O(N \cdot \log(\max B_i))\),每轮批量扣除至少清空一种硬币,轮数受限于 \(N\)
- 空间复杂度:\(O(N)\),硬币信息和方案数组
-
贪心批量扣除的核心思想:
- 整除关系的优势:小面额能整除大面额,保证贪心时不会出现"用大面额导致小面额无法精确补齐"的情况
- 两阶段互补:阶段一尽量接近目标减少浪费,阶段二确保达到最低支付额 \(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
浙公网安备 33010602011771号