Knapsack

Knapsack


1. 题意与答案公式

有 \(n\) 组物品,第 \(i\) 组有 \(a_i\) 个物品,每个物品重量为 \(2^{b_i}\)。
有 \(m\) 个背包,每个背包容量为 \(k\)。要求把所有物品都放入背包,求最小的 \(k\)。

对某个层 \(p\),把所有重量至少为 \(2^p\) 的物品都折算成“多少个 \(2^p\) 块”:

\[t_p=\sum_{b\ge p} a_b\cdot 2^{b-p} \]

每个容量为 \(k\) 的背包最多能放

\[\left\lfloor\frac{k}{2^p}\right\rfloor \]

个这样的块。
\(m\) 个背包一共能放

\[m\left\lfloor\frac{k}{2^p}\right\rfloor \]

个块,必须不少于 \(t_p\),所以

\[m\left\lfloor\frac{k}{2^p}\right\rfloor\ge t_p \]

推出

\[k\ge 2^p\left\lceil\frac{t_p}{m}\right\rceil \]

对所有 \(p\) 取最大,得到答案:

\[\boxed{ \text{ans}=\max_{p\ge 0} 2^p\left\lceil\frac{t_p}{m}\right\rceil } \]

这个下界是可达的,因此它也是最小可行容量。


2. 为什么只检查有物品的层

如果某一层 \(p\) 没有物品,即 \(a_p=0\),那么

\[t_{p-1}=2t_p \]

于是

\[2^{p-1}\left\lceil\frac{t_{p-1}}{m}\right\rceil = 2^{p-1}\left\lceil\frac{2t_p}{m}\right\rceil \le 2^p\left\lceil\frac{t_p}{m}\right\rceil \]

所以从有物品的层往下降,经过空层时答案不会变大,只会不变或变小。
因此最大值只会出现在某个有物品的 \(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\) 的定义

令

\[r_p=(-t_p)\bmod m,\qquad 0\le r_p<m \]

它满足

\[t_p+r_p=m\left\lceil\frac{t_p}{m}\right\rceil \]

于是

\[2^p\left\lceil\frac{t_p}{m}\right\rceil = 2^p\cdot\frac{t_p+r_p}{m} \]

代码中:

ll r = (m - tm % m) % m;

因为 M = m * MOD 是 \(m\) 的倍数,所以

\[tm\bmod m=t_p\bmod m \]

于是

\[r=(m-tm\bmod m)\bmod m=(-t_p)\bmod m \]

即 r 就是 \(r_p\),并且是准确的。


5. 维护 \(t_p \bmod (m\cdot MOD)\)

设相邻两个有物品层为

\[p'=b[i-1],\qquad p=b[i],\qquad g=p'-p>0 \]

因为 \(p'\) 和 \(p\) 之间没有其他有物品层,所以

\[t_p=2^g t_{p'}+a_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 = (2^g\cdot tlast+a[i])\bmod 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 存。

最后有

\[(tm+r)\bmod M\equiv t_p+r_p\pmod M \]

而 \(t_p+r_p\) 是 \(m\) 的倍数,所以

\[\frac{(tm+r)\bmod M}{m} \equiv \frac{t_p+r_p}{m} \pmod{MOD} \]

这正是最后答案需要的商。


6. 如何比较巨大的候选答案

扫描到当前层 \(p\) 时,候选答案是

\[k_p=2^p\left\lceil\frac{t_p}{m}\right\rceil = 2^p\cdot\frac{t_p+r_p}{m} \]

当前最优层记为 \(q\),对应答案

\[k_q=2^q\cdot\frac{t_q+r_q}{m} \]

\(k_p,k_q\) 都可能巨大,不能直接算出来比较。
定义带权差值

\[\boxed{ z=\frac{m(k_p-k_q)}{2^p} } \]

代入 \(k_p,k_q\):

\[z = \frac{m}{2^p} \left( 2^p\frac{t_p+r_p}{m} - 2^q\frac{t_q+r_q}{m} \right) \]

即

\[\boxed{ z=(t_p+r_p)-2^{q-p}(t_q+r_q) } \]

因为 \(m>0,2^p>0\),所以

\[z>0\iff k_p>k_q \]

\[z=0\iff k_p=k_q \]

\[z<0\iff k_p<k_q \]

因此只需要知道 z 的符号,就可以判断当前层是否比当前最优更好。


7. \(z\) 的递推公式(详细推导)

扫描顺序是 \(b\) 降序,所以当前层 \(p\) 满足 \(p\le q\)。

上一层是 \(p'=b[i-1]\),当前层是 \(p=b[i]\),层差

\[g=p'-p>0 \]

上一层的 \(z\) 为

\[z_{\text{prev}}=(t_{p'}+r_{p'})-2^{q-p'}(t_q+r_q) \tag{1} \]

当前层的 \(z\) 为

\[z_{\text{new}}=(t_p+r_p)-2^{q-p}(t_q+r_q) \tag{2} \]

由 (1) 得

\[2^{q-p'}(t_q+r_q)=(t_{p'}+r_{p'})-z_{\text{prev}} \tag{3} \]

因为 \(p'-p=g\),所以

\[2^{q-p}=2^{q-p'}\cdot 2^{p'-p}=2^{q-p'}\cdot 2^g \]

在 (3) 两边同乘 \(2^g\):

\[2^{q-p}(t_q+r_q) = 2^g\left[(t_{p'}+r_{p'})-z_{\text{prev}}\right] \tag{4} \]

又因为

\[t_p=2^g t_{p'}+a_p \]

所以

\[t_p+r_p=2^g t_{p'}+a_p+r_p \tag{5} \]

把 (4)(5) 代入 (2):

\[z_{\text{new}} = \left(2^g t_{p'}+a_p+r_p\right) - 2^g\left[(t_{p'}+r_{p'})-z_{\text{prev}}\right] \]

展开:

\[z_{\text{new}} = 2^g t_{p'}+a_p+r_p -2^g t_{p'}-2^g r_{p'}+2^g z_{\text{prev}} \]

消去 \(2^g t_{p'}\):

\[z_{\text{new}} = 2^g z_{\text{prev}} -2^g r_{p'} +a_p+r_p \]

即

\[\boxed{ z_{\text{new}} = 2^g\left(z_{\text{prev}}-r_{p'}\right) +a_p+r_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=\frac{m(k_p-k_q)}{2^p} = \frac{m(k_p-k_p)}{2^p} =0 \]

所以把 z 置 0 不是随便写的,而是新最优层下 \(z\) 必须为 0。


9. CAP 截断与 g > 60 分支

9.1 为什么需要 CAP

递推式

\[z_{\text{new}}=2^g(z_{\text{prev}}-r_{p'})+a_p+r_p \]

中 \(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 为什么截断不会翻转符号

因为

\[\text{cap}=2^{62}\gg m,\ a_p,\ r_p \]

而 \(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. 最后答案怎么算

最优层确定后,答案是

\[2^q\left\lceil\frac{t_q}{m}\right\rceil = 2^q\cdot\frac{t_q+r_q}{m} \]

代码中 tq 是 \(t_q\bmod M\),rq 是 \(r_q\),\(M=m\cdot MOD\)。

因为

\[(tq+rq)\bmod M\equiv t_q+r_q\pmod M \]

且 \(t_q+r_q\) 是 \(m\) 的倍数,所以

\[\frac{(tq+rq)\bmod M}{m} \equiv \frac{t_q+r_q}{m} \pmod{MOD} \]

代码中:

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);

这样得到

\[\text{ans} = 2^q\cdot\frac{t_q+r_q}{m} \bmod MOD \]


11. 整体流程

  1. 读入所有 \((a_i,b_i)\),按 \(b\) 降序排序。
  2. 合并相同 \(b\),得到 a[i], b[i]。
  3. 令 M = m * MOD。
  4. 从高 \(b\) 向低 \(b\) 扫描有物品层。
  5. 用

    \[t_p=2^g t_{p'}+a_p \]

    维护 tm = t_p mod M,并算补余 r = (-t_p) mod m。
  6. 用

    \[z_{\text{new}}=2^g(z_{\text{prev}}-r_{p'})+a_p+r_p \]

    维护差值 z,判断当前层是否更优。
  7. 若 z > 0,更新 q,tq,rq,并把 z 置 0。
  8. 最后用最优层的 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}\),符号压缩上界

核心结论:

\[\boxed{ z>0\iff k_p>k_q } \]

\[\boxed{ t_p=2^g t_{p'}+a_p } \]

\[\boxed{ z_{\text{new}}=2^g(z_{\text{prev}}-r_{p'})+a_p+r_p } \]

\[\boxed{ \text{ans}=2^q\left\lceil\frac{t_q}{m}\right\rceil\bmod MOD } \]


#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;
}
posted @ 2026-10-26 14:14  thenrify  阅读(18)  评论(0)    收藏  举报