Knapsack
Knapsack
1. 题意与答案公式
有 \(n\) 组物品,第 \(i\) 组有 \(a_i\) 个物品,每个物品重量为 \(2^{b_i}\)。
有 \(m\) 个背包,每个背包容量为 \(k\)。要求把所有物品都放入背包,求最小的 \(k\)。
对某个层 \(p\),把所有重量至少为 \(2^p\) 的物品都折算成“多少个 \(2^p\) 块”:
每个容量为 \(k\) 的背包最多能放
个这样的块。
\(m\) 个背包一共能放
个块,必须不少于 \(t_p\),所以
推出
对所有 \(p\) 取最大,得到答案:
这个下界是可达的,因此它也是最小可行容量。
2. 为什么只检查有物品的层
如果某一层 \(p\) 没有物品,即 \(a_p=0\),那么
于是
所以从有物品的层往下降,经过空层时答案不会变大,只会不变或变小。
因此最大值只会出现在某个有物品的 \(b\) 层。
代码中:
- 先把所有 \((b_i,a_i)\) 按 \(b\) 降序排序;
- 再合并相同的 \(b\);
- 得到数组
a[i]和b[i],其中b[i]降序,a[i]是对应层的物品数。
这样只需要扫描这些有物品的层。
3. 代码变量与公式记号对应
| 代码变量 | 含义 |
|---|---|
n,m |
物品组数、背包数 |
ba |
输入的 \((b,a)\),读入时 ba[i].first=b, ba[i].second=a |
a[i], b[i] |
合并相同 \(b\) 后的物品数和层号,b[i] 降序 |
y |
合并后不同 \(b\) 的个数 |
M = m * MOD |
关键模数,用来保存 \(t_p\) 的模值并支持最后除以 \(m\) |
tlast |
上一层 \(t_{b[i-1]}\) 在模 \(M\) 下的值 |
rlast |
上一层补余 \(r_{b[i-1]}\) |
tm |
当前层 \(t_{b[i]}\) 在模 \(M\) 下的值 |
r |
当前层补余 \(r_{b[i]}\) |
q,tq,rq |
当前最优答案对应的层 \(q\)、\(t_q\bmod M\)、补余 \(r_q\) |
z |
用于比较当前候选答案与最优答案的带权差值 |
cap |
截断上界,cap = 1<<62 |
g |
相邻有物品层的层差,g = b[i-1] - b[i] |
4. 补余 \(r_p\) 的定义
令
它满足
于是
代码中:
ll r = (m - tm % m) % m;
因为 M = m * MOD 是 \(m\) 的倍数,所以
于是
即 r 就是 \(r_p\),并且是准确的。
5. 维护 \(t_p \bmod (m\cdot MOD)\)
设相邻两个有物品层为
因为 \(p'\) 和 \(p\) 之间没有其他有物品层,所以
代码中:
if(i == 0) tm = a[0] % M;
else tm = (modmul(modpow(2, b[i - 1] - b[i], M), tlast, M) + a[i] % M) % M;
就是计算
即 tm 是 \(t_p\bmod (m\cdot MOD)\) 的准确值。
为什么要用 \(M=m\cdot MOD\)?
- 算补余
r需要 \(t_p\bmod m\); - 最后算答案需要 \(\dfrac{t_p+r_p}{m}\bmod MOD\);
- 单独模 \(m\) 或单独模 \(MOD\) 都会丢信息;
- 模 \(m\cdot MOD\) 同时保留两者,且 \(m\cdot MOD<10^{18}\),可以用
long long存。
最后有
而 \(t_p+r_p\) 是 \(m\) 的倍数,所以
这正是最后答案需要的商。
6. 如何比较巨大的候选答案
扫描到当前层 \(p\) 时,候选答案是
当前最优层记为 \(q\),对应答案
\(k_p,k_q\) 都可能巨大,不能直接算出来比较。
定义带权差值
代入 \(k_p,k_q\):
即
因为 \(m>0,2^p>0\),所以
因此只需要知道 z 的符号,就可以判断当前层是否比当前最优更好。
7. \(z\) 的递推公式(详细推导)
扫描顺序是 \(b\) 降序,所以当前层 \(p\) 满足 \(p\le q\)。
上一层是 \(p'=b[i-1]\),当前层是 \(p=b[i]\),层差
上一层的 \(z\) 为
当前层的 \(z\) 为
由 (1) 得
因为 \(p'-p=g\),所以
在 (3) 两边同乘 \(2^g\):
又因为
所以
把 (4)(5) 代入 (2):
展开:
消去 \(2^g t_{p'}\):
即
对应代码:
z = ((i128)1 << g) * (z - rlast) + a[i] + r;
其中:
z是 \(z_{\text{prev}}\);rlast是 \(r_{p'}\);a[i]是 \(a_p\);r是 \(r_p\);g = b[i-1] - b[i]。
注意:这个递推式里已经没有 \(t\) 的真实值,只依赖 \(z_{\text{prev}},r_{p'},a_p,r_p\)。
所以前面 tm 只保存模值,不会影响 z 的符号判断。
8. 更新最优层时为什么把 \(z\) 置 0
如果
if(z > 0) q = b[i], tq = tm, rq = r, z = 0;
含义是当前层 \(p\) 的候选答案比原最优层 \(q\) 更大,于是更新最优层为 \(p\)。
更新后按定义
所以把 z 置 0 不是随便写的,而是新最优层下 \(z\) 必须为 0。
9. CAP 截断与 g > 60 分支
9.1 为什么需要 CAP
递推式
中 \(2^g\) 可能非常大,连续乘会溢出 __int128。
但代码只需要知道 \(z_{\text{new}}\) 的符号,所以可以截断。
const i128 cap = (i128)1 << 62;
当 \(|z|>\text{cap}\) 时,把它改成 \(\pm\text{cap}\),保持符号不变:
if(z > cap) z = cap;
else if(z < -cap) z = -cap;
9.2 为什么截断不会翻转符号
因为
而 \(m,a_p\le 10^9\),\(r_p<m\)。
- 若真实 \(z_{\text{prev}}>\text{cap}\),则 \(z_{\text{prev}}-r_{p'}>0\),截断后 \(\text{cap}-r_{p'}>0\),符号一致;
- 若真实 \(z_{\text{prev}}<-\text{cap}\),则 \(z_{\text{prev}}-r_{p'}<0\),截断后 \(-\text{cap}-r_{p'}<0\),符号一致;
- 乘 \(2^g>0\) 不改变符号;
- 加 \(a_p+r_p\) 是 \(10^9\) 量级的小量,无法把绝对值约 \(2^{62}\) 的数拉过 0。
所以截断只压缩大小,不改变符号。
正 CAP 当层会触发 if(z > 0),更新最优并置 0;
负 CAP 后续会一直保持负,不会更新最优。
9.3 g > 60 分支
当 \(g>60\) 时,\(2^g\) 太大,不能真的乘。代码只判断 \(z-r_{p'}\) 的符号:
i128 d = z - (i128)rlast;
z = d > 0 ? cap : (d < 0 ? -cap : (i128)a[i] + (i128)r);
- 若 \(d>0\),\(2^g d\) 是巨大正数,加小量仍正,记 \(+\text{cap}\);
- 若 \(d<0\),\(2^g d\) 是巨大负数,加小量仍负,记 \(-\text{cap}\);
- 若 \(d=0\),则 \(2^g d=0\),只剩 \(a_p+r_p\),直接算。
这个分支同样只依赖符号,结果正确。
10. 最后答案怎么算
最优层确定后,答案是
代码中 tq 是 \(t_q\bmod M\),rq 是 \(r_q\),\(M=m\cdot MOD\)。
因为
且 \(t_q+r_q\) 是 \(m\) 的倍数,所以
代码中:
ll num = (tq + rq) / m;
ll ans = modmul(num, modpow(2, q, MOD), MOD);
更严谨可写成:
ll num = (ll)(((i128)tq + rq) % M / m);
ll ans = modmul(num, modpow(2, q, MOD), MOD);
这样得到
11. 整体流程
- 读入所有 \((a_i,b_i)\),按 \(b\) 降序排序。
- 合并相同 \(b\),得到
a[i], b[i]。 - 令
M = m * MOD。 - 从高 \(b\) 向低 \(b\) 扫描有物品层。
- 用\[t_p=2^g t_{p'}+a_p \]维护
tm = t_p mod M,并算补余r = (-t_p) mod m。 - 用\[z_{\text{new}}=2^g(z_{\text{prev}}-r_{p'})+a_p+r_p \]维护差值
z,判断当前层是否更优。 - 若
z > 0,更新q,tq,rq,并把z置 0。 - 最后用最优层的
q,tq,rq算\[2^q\cdot\frac{t_q+r_q}{m}\bmod MOD \]
12. 代码变量速查
| 代码 | 公式 |
|---|---|
M |
\(m\cdot MOD\) |
tlast |
\(t_{p'}\bmod M\) |
rlast |
\(r_{p'}=(-t_{p'})\bmod m\) |
tm |
\(t_p\bmod M\) |
r |
\(r_p=(-t_p)\bmod m\) |
q |
当前最优层 |
tq |
\(t_q\bmod M\) |
rq |
\(r_q=(-t_q)\bmod m\) |
z |
\(z=(t_p+r_p)-2^{q-p}(t_q+r_q)\) |
g |
\(p'-p=b[i-1]-b[i]\) |
cap |
\(2^{62}\),符号压缩上界 |
核心结论:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using i128 = __int128;
using pll = pair<ll, ll>;
const ll MOD = 998244353;
const i128 cap = (i128)1 << 62;
ll modmul(ll a, ll b, ll m) {
return (ll)((__int128) a * b % m);
}
ll modpow(ll a, ll b, ll m) {
ll r = 1;
while(b) {
if(b & 1) r = modmul(r, a, m);
a = modmul(a, a, m);
b >>= 1;
}
return r;
}
void solve() {
int n; ll m; cin >> n >> m;
vector<pll> ba(n);
for(int i = 0; i < n; i ++) cin >> ba[i].second >> ba[i].first;
sort(ba.begin(), ba.end(), greater<pll>());
vector<ll> a, b;
for(auto [bs, as] : ba) {
if(!b.empty() && b.back() == bs) a.back() += as;
else b.push_back(bs), a.push_back(as);
}
int y = b.size();
ll M = m * MOD;
// kq = ceil(tq / m) * 2^q
// kp = ceil(tp / m) * 2^p
// rp为补余,rp = (-tp) mod m
// tp + rp = m * ceil(tp / m)
ll tlast = 0, rlast = 0;
ll q = 0, tq = 0, rq = 0; //最优k对应的 q和tq
// z = (kp - kq) * m / (2 ^ p) z = 0说明k相等,z > 0, 说明kp大,更新kq
i128 z = 0;
for(int i = 0; i < y; i ++) {
ll tm;
if(i == 0) tm = a[0] % M;
else tm = (modmul(modpow(2, b[i - 1] - b[i], M), tlast, M) + a[i] % M) % M;
ll r = (m - tm % m) % m;
if(i == 0) q = b[0], tq = tm, rq = r;
else {
ll g = b[i - 1] - b[i];
//cap截断,截断后,后续z的正负不会发生变化(cap取的足够大)
if(g <= 60) {
z = ((i128)1 << g) * (z - rlast) + a[i] + r;
if(z > cap) z = cap;
else if(z < -cap) z = -cap;
}
else {//g太大,可能爆int128
i128 d = z - (i128)rlast;
z = d > 0 ? cap : (d < 0 ? -cap : (i128)a[i] + (i128)r);
}
if(z > 0) q = b[i], tq = tm, rq = r, z = 0;
}
tlast = tm, rlast = r;
}
ll num = (tq + rq) / m;
ll ans = modmul(num, modpow(2, q, MOD), MOD);
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int _ = 1; cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号