按原价降序刻画贪心失败的关键交换对,用组合数统计坏定价方案后从 2^n 中扣除。
OJ: luogu
题目 ID: P14636
难度:提高+/省选-
标签:贪心组合计数排序
日期: 2026-06-22 17:57
题意
有 n 颗糖果,第 i 颗糖果原价为 a_i。每颗糖果的清仓价可以独立定成 1 元或 2 元。
定价方案确定后,小 R 会把糖果按性价比 a_i / w_i 从大到小排序;性价比相同则原价高的先买,仍相同则编号小的先买。然后他按这个顺序扫描,钱够就买,否则跳过。
题目要求统计:在所有 2^n 种定价方案里,有多少种方案使得小 R 按这个贪心策略买到的原价总和,等于预算 m 下能买到的最大原价总和。答案对 998244353 取模。
输入第一行的第一个整数是测试点编号,可以读入但不用。
思路
先看一个能直接验证定义的暴力做法:枚举每种定价方案,模拟小 R 的性价比贪心,再用 0/1 背包求同一方案下真正的最优原价和,比较二者是否相等。
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
const int MOD = 998244353;
int control_id, test_count;
int n, m;
long long a[MAXN];
int w[MAXN];
bool cmp_item(int x, int y) {
long long left = a[x] * w[y];
long long right = a[y] * w[x];
if (left != right) {
return left > right;
}
if (a[x] != a[y]) {
return a[x] > a[y];
}
return x < y;
}
long long greedy_value() {
vector<int> id;
for (int i = 1; i <= n; i++) {
id.push_back(i);
}
sort(id.begin(), id.end(), cmp_item);
int money = m;
long long sum = 0;
for (int i = 0; i < (int)id.size(); i++) {
int x = id[i];
if (money >= w[x]) {
money -= w[x];
sum += a[x];
}
}
return sum;
}
long long optimal_value() {
vector<long long> dp(m + 1, -1);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int money = m; money >= w[i]; money--) {
if (dp[money - w[i]] < 0) {
continue;
}
dp[money] = max(dp[money], dp[money - w[i]] + a[i]);
}
}
long long best = 0;
for (int money = 0; money <= m; money++) {
best = max(best, dp[money]);
}
return best;
}
void solve_one() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
long long ans = 0;
int total = 1 << n;
// 枚举每颗糖果定价为 1 或 2 元。
for (int mask = 0; mask < total; mask++) {
for (int i = 1; i <= n; i++) {
if (mask & (1 << (i - 1))) {
w[i] = 2;
} else {
w[i] = 1;
}
}
if (greedy_value() == optimal_value()) {
ans++;
}
}
cout << ans % MOD << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> control_id >> test_count;
while (test_count--) {
solve_one();
}
return 0;
}这个做法的瓶颈很明显:定价方案有 2^n 种,而 n 最大是 5000。所以我们反过来统计“不合法”的定价方案。
先把糖果按原价从大到小排序:
a_1 >= a_2 >= ... >= a_n如果一个定价方案不合法,说明贪心买到的集合不是最优集合。由于清仓价格只有 1 和 2,一次最关键的失败可以看成:
- 有一颗原价更高的糖果
i,它被定价为2元; - 有一颗原价更低的糖果
j,它被定价为1元; - 糖果
j在贪心顺序里排在糖果i前面,并且被买走; - 到贪心考虑糖果
i时,剩余钱已经不足2元; - 把
j换成i后,原价总和会变大。
这里必须有 a_i > a_j,否则交换不会让答案变大。还必须有:
a_j / 1 > a_i / 2也就是:
2a_j > a_i否则 j 不会排在 i 前面,也挡不住它。
接下来固定这对关键糖果 (i, j)。糖果 j 是一个已经买走的 1 元糖果,糖果 i 是被漏掉的 2 元糖果。为了让贪心考虑 i 时剩余钱少于 2,前面必须已经花掉至少 m - 1 元。
在这个关键结构里,除了糖果 j 之外,还需要从 j 前面的糖果里选出:
m - i - 1个会被贪心买走的 1 元糖果。可选位置有 j - 2 个,因此贡献:
C(j - 2, m - i - 1)还剩一类自由选择:如果某个后缀糖果 k 满足:
a_k + a_j < a_i那么它无论定成 1 还是 2,都不会改变由 (i,j) 定位的这次关键失败。设满足条件的后缀起点为 p,这部分贡献:
2^(n - p + 1)于是每个关键对 (i,j) 对坏方案数的贡献为:
C(j - 2, m - i - 1) * 2^(n - p + 1)枚举所有合法的 (i,j),累加坏方案数 bad,最后答案就是:
2^n - bad实现时预处理组合数 C(i,j) 和 2^i。内层枚举 j 时,用指针 p 维护满足 a[p] + a[j] < a[i] 的后缀起点。组合数参数不合法时直接视为 0。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5005;
const int MOD = 998244353;
int control_id, test_count;
int n, m;
long long a[MAXN];
int comb[MAXN][MAXN]; // comb[i][j] = C(i, j)
int pow2[MAXN]; // pow2[i] = 2^i
void prepare() {
pow2[0] = 1;
for (int i = 1; i < MAXN; i++) {
pow2[i] = pow2[i - 1] * 2LL % MOD;
}
comb[0][0] = 1;
for (int i = 1; i < MAXN; i++) {
comb[i][0] = comb[i][i] = 1;
for (int j = 1; j < i; j++) {
comb[i][j] = comb[i - 1][j] + comb[i - 1][j - 1];
if (comb[i][j] >= MOD) {
comb[i][j] -= MOD;
}
}
}
}
int get_comb(int x, int y) {
if (x < 0 || y < 0 || y > x) {
return 0;
}
return comb[x][y];
}
void solve_one() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a + 1, a + n + 1, greater<long long>());
long long bad = 0;
// 枚举最优解会买的 2 元糖果 i,以及贪心会用来卡住它的 1 元糖果 j。
for (int i = 1; i <= n; i++) {
int need_extra = m - i - 1;
if (need_extra < 0) {
break;
}
int p = n + 1;
int start_j = max(i + 1, m - i + 1);
for (int j = start_j; j <= n; j++) {
if (a[i] == a[j]) {
continue;
}
if (a[i] >= 2LL * a[j]) {
break;
}
// p 是最靠前的位置,使得 [p, n] 的糖果都能作为“较低性价比的 1 元糖果”。
while (p > 1 && a[p - 1] + a[j] < a[i]) {
p--;
}
bad += 1LL * get_comb(j - 2, need_extra) * pow2[n - p + 1] % MOD;
if (bad >= MOD) {
bad -= MOD;
}
}
}
int ans = (pow2[n] - (int)(bad % MOD) + MOD) % MOD;
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
prepare();
cin >> control_id >> test_count;
while (test_count--) {
solve_one();
}
return 0;
}复杂度
预处理组合数的时间复杂度为
每组数据排序复杂度为
总结
这题的关键不是模拟小 R 的贪心,而是找出贪心失败的最小结构:一个被买走的 1 元低原价糖果,挡住了一颗更高原价的 2 元糖果。
找到这个结构后,题目就变成了组合计数:固定关键对 (i,j),前面选多少个 1 元糖果由组合数统计,后缀中不会影响关键失败的糖果用二次幂统计。最后用总方案数减去坏方案数即可。