把购买拆成最便宜的两颗组和若干个单颗项,预处理奇偶最优值后二分答案。
OJ: luogu
题目 ID: P14635
难度:普及+/提高
标签:二分答案贪心推导noip
日期: 2026-06-22 17:57
题意
有 n 种糖果,每种糖果数量无限。第 i 种糖果的价格按购买次数交替:
x_i, y_i, x_i, y_i, ...也就是说,买第 1,3,5,... 颗花 x_i 元,买第 2,4,6,... 颗花 y_i 元。
给定预算 m,问最多可以买多少颗糖果。只关心总数量,不关心买了哪些种类。
思路
先看一个可以直接验证想法的朴素解。它按糖果种类做小预算 DP,枚举每种糖果买多少颗:
// brute.cpp:小数据暴力 DP,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
const int INF = 1000000000;
int n, m;
int x[MAXN], y[MAXN];
int dp[205];
int cost_of_count(int id, int cnt) {
int pairs = cnt / 2;
int cost = pairs * (x[id] + y[id]);
if (cnt % 2 == 1) {
cost += x[id];
}
return cost;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i];
}
for (int j = 1; j <= m; j++) {
dp[j] = -INF;
}
dp[0] = 0;
// 逐种糖果枚举买几颗。m 很小时,每种糖果最多买 m 颗即可覆盖所有可能。
for (int i = 1; i <= n; i++) {
int next_dp[205];
for (int j = 0; j <= m; j++) {
next_dp[j] = dp[j];
}
for (int money = 0; money <= m; money++) {
if (dp[money] < 0) {
continue;
}
for (int cnt = 1; cnt <= m; cnt++) {
int cost = cost_of_count(i, cnt);
if (money + cost > m) {
break;
}
next_dp[money + cost] = max(next_dp[money + cost], dp[money] + cnt);
}
}
for (int j = 0; j <= m; j++) {
dp[j] = next_dp[j];
}
}
int ans = 0;
for (int j = 0; j <= m; j++) {
ans = max(ans, dp[j]);
}
cout << ans << '\n';
return 0;
}这个做法能帮助理解题意,但它依赖预算大小。题目中 m 最大到 10^18,所以不能按钱数开数组。
把选择拆成两类
对同一种糖果,如果买两颗,成本一定是:
x_i + y_i如果买奇数颗,就相当于若干个“两颗组”之外,再多买一颗奇数位糖,额外成本是 x_i。
因此任意方案都可以拆成:
| 部分 | 贡献数量 | 成本 | 可选次数 |
|---|---|---|---|
| 两颗组 | 2 |
x_i + y_i |
可以无限次 |
| 单颗项 | 1 |
x_i |
每种糖果最多一次 |
两颗组可以无限买,所以最优时一定一直使用最便宜的 min(x_i + y_i)。
单颗项每种糖果最多选一次,所以如果要选 r 个单颗项,就选最小的 r 个 x_i。
判断能否买到 k 颗
设要买 k 颗。我们枚举单颗项数量 r,它必须满足:
0 <= r <= min(n, k)
r 和 k 同奇偶剩下的 k-r 颗必须由两颗组补齐。
若把所有 x_i 排序,记最小的 r 个 x_i 之和为 prefix_x[r],再记:
p = min(x_i + y_i)那么固定 r 时的花费为:
prefix_x[r] + (k-r)/2 * p为了快速求最小值,把式子整理一下。
当 k 为偶数时,r 也为偶数:
cost = k/2 * p + (prefix_x[r] - r/2 * p)当 k 为奇数时,r 也为奇数:
cost = (k-1)/2 * p + (prefix_x[r] - (r-1)/2 * p)所以可以预处理:
best_even[t]:在r <= t且r为偶数时,括号里的最小值;best_odd[t]:在r <= t且r为奇数时,括号里的最小值。
这样 check(k) 只要看 k 的奇偶,取 t = min(n,k),就能 k 颗的最小花费。
二分答案
如果可以买到 k 颗,那么一定也可以买到更少的颗数;如果买不到 k 颗,那么一定买不到更多颗数。
所以答案具有单调性,可以二分最大可行的 k。
实现时注意 m 最大到 10^18,乘法用 __int128 计算,避免溢出。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const long long INF = (long long)4e18;
int n;
long long m;
long long x[MAXN], y[MAXN];
long long sx[MAXN]; // 排序后的 x
long long prefix_x[MAXN]; // 最小的若干个单颗成本之和
long long best_even[MAXN], best_odd[MAXN];
long long min_pair_cost;
void read_input() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i];
}
}
void prepare() {
min_pair_cost = INF;
for (int i = 1; i <= n; i++) {
sx[i] = x[i];
min_pair_cost = min(min_pair_cost, x[i] + y[i]);
}
sort(sx + 1, sx + n + 1);
for (int i = 1; i <= n; i++) {
prefix_x[i] = prefix_x[i - 1] + sx[i];
}
best_even[0] = 0;
best_odd[0] = INF;
for (int i = 1; i <= n; i++) {
best_even[i] = best_even[i - 1];
best_odd[i] = best_odd[i - 1];
if (i % 2 == 0) {
long long value = prefix_x[i] - (long long)(i / 2) * min_pair_cost;
best_even[i] = min(best_even[i], value);
} else {
long long value = prefix_x[i] - (long long)((i - 1) / 2) * min_pair_cost;
best_odd[i] = min(best_odd[i], value);
}
}
}
// 判断是否能用不超过 m 的钱买到 need 颗糖果。
bool check(long long need) {
int parity = (int)(need % 2);
int limit = (int)min((long long)n, need);
long long best_single_part = (parity == 0) ? best_even[limit] : best_odd[limit];
if (best_single_part >= INF / 2) {
return false;
}
__int128 pair_cnt = (need - parity) / 2;
__int128 cost = pair_cnt * min_pair_cost + best_single_part;
return cost <= m;
}
void solve() {
long long left = 0;
long long right = m;
long long ans = 0;
prepare();
while (left <= right) {
long long mid = (left + right) / 2;
if (check(mid)) {
ans = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
排序 x_i 需要 best_even、best_odd 需要 check(k) 是
总时间复杂度:
O(n log n + log m)空间复杂度:
O(n)总结
本题的关键不是按预算做背包,而是重新理解“交替价格”的结构。
同一种糖果每两颗形成一个固定成本的组,所有两颗组中只需要使用最便宜的一种;奇数位多出来的单颗糖,每种糖果最多贡献一次,按 x_i 排序后取前缀即可。
把这两部分拆开后,check(k) 就能快速判断,于是用二分答案求最大糖果数。