背包
背包
前言:
可撤销背包,曾一次次洗刷着我对于背包的认知.
模拟赛考了一道背包神题:Log Set.
ABC 考了可撤销 01 背包的板子.
这三者是我想来一次彻底整理的源头吧。
多年之后,当我依稀听见这个陌生而熟悉的词,心中多少有一分怀念温存。
省流版前言:学动态规划不学背包,就像打二游不玩某二字开放世界游戏,看番剧不看进击的巨人,玩旮旯不玩千恋万花,只能度过一个相对失败的人生。
默认读者会基础的背包问题。
Pre-Definition
物品:物品。
背包:用来装物品的一个容器。
体积/重量:一件物品在背包中占用的体积/重量。
价值:将一件物品装入背包后获得的价值。
01背包:一件物品只能选一次或不选。
完全背包:一件物品可以选任意个。
多重背包:一件物品 \(i\) 可以选至多 \(s_i\) 个。
01背包和完全背包在(一维)实现上的区别在于枚举顺序。
Tricks
换维
交换体积和价值,并改变 dp 数组的含义。
以 Knapsack 2 为例,体积是 \(10^9\) 级别的,但价值很小。
由于要在限制的体积内求出价值总和最大值,因此转成记得到一个确定的价值,最少需要多少体积。
bitset优化判定性背包
CF1854B Earn or Unlock
当最终解锁的牌数确定时,最终价值也是确定的,所以只需要知道对于每一个 \(k\),是否存在一种方案使恰好前 \(k\) 张牌解锁。
设计 \(dp_{i}\) 表示上述状态,对于所有 \(j \ge i\),\(dp_{j + a_i} |= dp_j\)。此操作完成后,需将 \(dp_i\) 清空(因为不能在没解锁一张牌时使用这张牌),因此需要另开一个数组记录答案。
注意要开 \(2n\)。
[ABC221G] Jumping sequence
单独考察一维,这个问题就是个 01 背包。麻烦的是前后两维是绑定在一起的。
如何解除这种绑定?转切比雪夫距离!
\((A, B) \to (A + B, A - B)\)。
对应地,考察每一步的变化量也以同样的方式进行转化:
- \((0, d_i) \to (d_i, -d_i)\)
- \((0, -d_i) \to (-d_i, d_i)\)
- \((d_i, 0) \to (d_i, d_i)\)
- \((-d_i, 0) \to (-d_i, -d_i)\)
然后就可以分别考虑两个维度的背包问题了!
具体地,要求出一种分配正负号的方案,使 \(\pm{d_i} \pm{d_2} \pm \dots \pm{d_n} = A(\text{ or } B)\)。
考虑两边同时加上 \(\sum{d}\),转化为对每个物品选或不选的问题,这样物品就会带上一个 \(2\) 倍的系数。
为了优化时空,可以采用 bitset 优化这种判断是否可以拼出一个数的背包问题。
很遗憾,如果只转化到了这一步,你会被出题人卡空间。你没有办法进行滚动优化,因为你需要根据之前的 dp 值倒推移动方案。
你发现如果 \(A (\text{ or } B) + \sum{d}\) 不能被 \(2\) 整除,那么一定无解,因为每个物品的权值都是 \(2\) 的倍数。
于是可以将左右同除 \(2\),将空间砍掉一半,这样就能过了。
时空复杂度均为 \(O(\frac{n\sum{d}}{w})\)。
多重背包的二进制优化
把一件物品的数量拆成 \(\log\) 级别的。
多重背包的前缀和优化
适用于计数类的多重背包。
考察一个物品数量为 \(n\),背包体积为 \(m\) 的计数类多重背包问题。
记 \(f(i, j)\) 表示考虑到第 \(i\) 个物品,背包体积为 \(j\) 的方案数。记 \(s_i\) 表示 \(i\) 物品的数量上界,\(v_i\) 表示一件 \(i\) 物品的占用体积。则有朴素转移:
对模 \(v_i\) 意义下的同余类进行前缀和处理(相当于先做一次完全背包),再用前缀和相减满足个数限制。具体实现见下:
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j + v[i] <= m; ++j)
Inc(f[j + v[i]], f[j]);
for(int j = m; j >= (s[i] + 1) * v[i]; --j)
Dec(f[j], f[j - (s[i] + 1) * v[i]]);
}
这样可以将原本为 \(O(m\sum\limits{s})\) 的时间复杂度降为 \(O(mn)\),使其与物品数量无关。
[ARC104D] Multiset Mean
把每种数值看作一种物品,每种物品的数量为 \(k\) 个,价值为数值本身,重量为 \(1\),求价值为重量的 \(x\) 倍的方案数。
这个价值为重量的 \(x\) 倍就很讨厌,不方便统计答案,但如果 \(x = 0\) 时,就只需所有数的和加起来为 \(0\),最终价值是确定的。
那么我们每选取一个数,就将这个数减 \(x\),也就是选的数值落在 \([1 -x , n - x]\) 中。这是在正确性的保证下 强行改变价值。
把数值本身塞进下标做多重背包,考虑到要对所有 \(n\) 个 \(x\) 求出方案数,不妨把 \([1 - x, n - x]\) 拆成 \([1 - x, 0), \{0 \}, (0, n - x]\),\(0\) 可以选 \([0, k]\) 个,剩下的正负两区间和为 \(0\)。两个部分其实本质是一样的,我们只需要求出用 \(1 \sim i\) 获得价值为 \(j\) 的方案数 \(f_{i, j}\),则对于 \(x\),答案为:
注意减一以避免空集。
对于多重背包的部分,直接做是 \(O(n^3k^2)\) 的(价值值域为 \(O(n^3k)\))。
但是可以用前缀和优化到 \(O(n^3k)\)。
生成函数观点
这是个天坑,此处只介绍一道题。
LOJ556 「Antileaf's Round」咱们去烧菜吧
LOJ556 「Antileaf's Round」咱们去烧菜吧
考虑每一种物品的生成函数:
- 若 \(b_i = 0\):\(F_i(x) = \sum\limits_{k \ge 0}x^{ka_i} = \frac{1}{1 - x^{a_i}}\)。
- 否则:\(F_{i}(x) = \sum\limits_{k = 0}^{b_i}x^{ka_i} = \sum\limits_{k \ge 0}x^{ka_i} - \sum\limits_{k \ge b_i + 1}x^{ka_i} = (1 - x^{a_i(b_i + 1)})\sum\limits_{k \ge 0}x^{ka_i} = \frac{1 - x^{a_i(b_i + 1)}}{1 - x^{a_i}}\)。
套路:把乘积形式通过取对数转为 \(\exp\) 套和式。
仍然分为有限与无限两种情况讨论。
- 若无限:\(\ln{F_i(x)} = \ln{\frac{1}{1 - x^{a_i}}} = \sum\limits_{k \ge 1}\frac{x^{a_ik}}{k}\)。
- 若有限:\(\ln{F_i(x)} = \ln{\frac{1 - x^{a_i(b_i + 1)}}{1 - x^{a_i}}} = \ln(1 - x^{a_i(b_i + 1)}) - \ln(1 - x^{a_i}) = \ln\left( \frac{1}{1 - x^{a_i}} \right) - \ln\left( \frac{1}{1 - x^{a_i(b_i + 1)}} \right) = \sum\limits_{k \ge 1}\frac{x^{a_ik} - x^{a_i(b_i + 1)k}}{k}\)。
求 \(\sum\limits_{i = 1}^{m}F_i(x)\) 的各项系数可以调和级数 \(O(n\ln{n})\) 处理出来,然后 \(\exp\) 一遍即可。
Others
在特殊限制下,我们可以用更加开放的思路去解决特定问题。
CF3B Lorry
当其中一种重量的物品数量确定时,另一种重量的物品数量上界可以被确定。
对于同一种重量的所有物品,当知道选择个数为 \(k\) 时,可以 贪心 地选择价值最大的 \(k\) 个。这个最优价值经排序预处理后可以 \(O(1)\) 得知。
因此支持我们枚举重量为 \(1\) 的物品选多少件,对所有情况的最优策略取最大值。
可撤销背包
01背包的撤销操作
记未加入 \(x\) 时的 dp 数组 状态 为 \(f\),加入 \(x\) 时的 dp 数组 状态 为 \(g\)。
考虑正向加入的更新过程,是 \(f_i \to g_{i + x}\)。
因此删除时,应该使 \(g_{i + x}\) 减去 \(f_i\),此时需要减去的是未加入 \(x\) 状态的 dp 值,所以应当先操作较小体积的 dp 值,保证体积为 \(i\) 时的 dp 数组状态为 \(f\),再对较大体积的 dp 值进行 \(x\) 的撤销。这个过程的枚举顺序与正向加入恰好相反。
void insert(int x)
{
for(int i = n; i >= x; --i)
Inc(f[i], f[i - x]);
}
void remove(int x)
{
for(int i = x; i <= n; ++i)
Dec(f[i], f[i - x]);
}
[ABC321F] #(subset sum = K) with Add and Erase
关于可撤销背包,需要知道这个常用技巧:
可行性背包转方案数背包,以方便进行回退。
由于方案数很大,所以需要取模;而取模后为 \(0\) 的概率很小,所以正确率极高。
[ABC056D] No Need
第一道例题对上面的技巧进行解释。
具体地,先整体做一遍正常的 01 背包,\(f_i\) 表示合成数值 \(i\) 的方案数。
对每个物品进行回退(\(\ge K\) 的可以直接跳过,因为它一定不是可有可无的数字),若物品 \(i\) 回退后,dp 数组的 \([K - a_i, K - 1]\) 范围内所有 dp 值为 \(0\),那这个物品就是一个可有可无的数字。
然后还要分析一下体积上限,因为我们只需要判断 \([K - a_i, K - 1]\) 内的 dp 值是否合法,而加上 \(a_i\) 后的值域不超过 \(2K\)(已排除 \(\ge K\) 的元素),所以 dp 数组只需要处理到 \(2K\)。
CF981E Addition on Segments
仍然用到了上述技巧。
做可撤销背包,如果一个数能表示出来,则一定有办法让它成为一个所选子集中能表示出来的数的最大值。时间复杂度 \(O(n^2)\)。
此题有线段树分治的另解,作为线段树分治的讲解题目也很适合。
P6808 [BalticOI 2010 Day2] Candies
P6808 [BalticOI 2010 Day2] Candies
仍然用到了上述技巧。
感觉这是一个背包好题!但是不如 Log Set。
对于一个能表示出 \(w\) 个数的多重集 \(S\),若再加入一个数 \(Q\) 构成 \(S^{'}\),使得 \(S^{'}\) 能表示出的数的个数 \(w^{'}\) 尽量大,则当 \(Q\) 足够大时,\(w^{'} = 2w + 1\)。
所以可以将两个问分别考虑。下记 \(m = \sum\limits_{i = 1}^{n}B_i\)。
求 \(P\),只需要知道删掉一个元素后能表示出来的数的个数 \(w\) 的最大值,就能够确定 \(w^{'}\) 的最大值。这个可以在 \(O(nm)\) 的时间复杂度内完成。
求 \(Q\),只需要使其加入后能表示出的数的个数为 \(w^{'}\)。有一个显然的可行解是 \(m + 1\),试图枚举 \(Q\),判断是否可行。但这样是 \(O(m^2)\) 的。等等,我们不正是把可行性背包转成了方案数背包吗?我们现在并不需要支持撤销了,因为除了剩下的一个数之外,其它的 \(n - 1\) 个数都是确定的。那直接上 bitset
优化!
但是 \(m\) 可以到达 \(7 \times 10^5\),还是很寄。
看来要探究 \(Q\) 的本质了。考虑去掉 \(P\) 的多重集 \(S\),则不能有:
神仙!把除 \(P\) 以外的 \(\{ -B \} \cup \{ B \}\) 扔在一起做一遍可行性 dp 得出所有不满足条件的数。找出第一个不能被 \(\{ -B \} \cup \{ B \}\) 表示出来的数就是答案。
时间复杂度为 \(O(nm)\)。用不用 bitset
已经无所谓了。
注意加入负数时,01 背包的枚举顺序要变!并且应当先加入所有正数,再加入负数。
完全背包的撤销操作
void insert(int x)
{
for(int i = x; i <= n; ++i)
Inc(f[i], f[i - x]);
}
void remove(int x)
{
for(int i = n; i >= x; --i)
Dec(f[i], f[i - x]);
}
[AGC049D] Convex Sequence
\(2a_i \le a_{i - 1} + a_{i + 1} \iff a_{i} - a_{i + 1} \le a_{i - 1}- a_{i}\)。
可以用 差分 来理解式子,也可以把这个序列看成是 凸性 的,具体这道题是下凸。
考虑如何构造一个合法序列。
枚举第一个最小值的位置 \(i\)。
- 整个数列加 \(1\)。
- 选择 \(j < i\),给 \(a_j, a_{j - 1},a_{j - 2},\cdots, a_1\) 分别加上 \(1, 2, 3,\cdots\)。由于我们限制 \(i\) 必须是第一个最小值,因此必须选择 \(j = i - 1\) 进行一次这种操作。
- 选择 \(j > i\),给 \(a_j, a_{j + 1}, a_{j + 2},\cdots, a_n\) 分别加上 \(1, 2, 3,\cdots\)。
不同的 \(i\) 对应的数列一定不同,相同 \(i\) 的不同高度对应的数列一定不同。对于一个第一个最小值的位置为 \(i\) 的数列,其操作序列是固定的,即 操作序列与数列是一一对应的。
这一点很重要,因为这样我们就可以仅仅从操作序列求得答案。
考虑操作的过程相当于一个完全背包问题,从前往后枚举 \(i\) 的过程中,要对背包进行增加和删除物品的操作。
考虑有效的背包物品数量为 \(O(\sqrt{m})\),因此时间复杂度没有问题。
(计数类)多重背包的撤销操作
[ARC028D] 注文の多い高橋商店
强制第 \(k\) 种物品必须选 \(x\) 个,相当于求只考虑第 \(k\) 种之外的物品,总个数为 \(m - x\) 的选取方案数,然后这个是多重背包板子。
但是有多组询问。对于每个物品,预处理只考虑第 \(i\) 种之外的背包数组。这里考察了可撤销多重背包。
具体地:
记 \(f[i][j]\) 表示考虑前 \(i\) 种物品,共选择了 \(j\) 件物品的方案数。
这个过程不用枚举 \(k\),而转成前缀和优化,使朴素的 \(O(n^3)\) 多重背包降为 \(O(n^2)\)。空间上面其实也可以把第一位给砍掉。
可撤销背包中,删除一种物品有多种方法。
Question:为什么这里要讲这么多方法?
Answer:因为之前写这篇题解的时候就是这样的。
-
仿照上面的做法,倒着处理一遍 dp 数组 \(g\),统计答案时排除要求的物品即可。
由于查询时还要枚举左右物品的数量分配,因此时间复杂度为 \(O(nm + Qm)\)。
-
用生成函数表示多重背包的总方案数,即对于一种数量为 \(s\) 的物品,其贡献为 \(\sum\limits_{i = 0}^{s}x^{i}\),合起来的生成函数即为若干个分别的生成函数相乘。删除一种物品,即为除去该种物品对应的生成函数,多项式除法,但不用多项式工业,直接暴力即可,\(O(nm)\)。
-
采用分治思想,递归到叶节点表示删除该种物品。那么如果递归左边,右边就可以背包合并起来;反之。如此,每种物品会被合并 \(O(\log{n})\) 次,总时间复杂度为 \(O(nm\log{n})\)。
Kami
gym100702D Log Set
版本 T0。
学背包不做 Log Set,就像打二游不玩某二字开放世界游戏,追星不追理塘王丁真珍珠,玩泣系旮旯不玩克拉纳的,只能度过一个相对失败的人生。
Problem
有一个大小为 \(m(m \le 60)\) 的多重集 \(S\),它的所有子集(包括空集)和组成了一个大小为 \(2^{m}\) 的多重集 \(T\)。
现在以如下方式给定 \(T\):给定一个正整数 \(n(n \le 10^4)\),表示 \(T\) 中不同元素的数量。
给定 \(n\) 个二元组,第 \(i\) 个二元组 \((a_i, b_i)\) 表示数字 \(a_i\) 在 \(T\) 中出现了 \(b_i\) 次。
请求出符合条件的 \(S\) 中按元素大小升序排序后字典序最小的方案。
\(S^{'} < S^{''}\),当且仅当 \(\exists k, 1 \le k \le m\),\(\forall i, 1 \le i < k, S^{'}_i = S^{''}_i\),\(S^{'}_k < S^{''}_k\)。
Solution
模拟赛时的部分分设了一个 \(m \le 20\),\(T\) 中的元素非负。好啊!每次拎出 \(T\) 中的最小值,这个最小值一定是 \(S\) 中的元素,对其做一遍撤销。直到所有元素确定完。好!模拟赛时获得了 \(35pts\) 的好成绩。
这个部分分给的太牛了,因为正解是每次按 \(T\) 中元素最大的那两个来确定 \(S\) 中元素的!!!(/yun/yun/yun
记 \(A\) 表示 \(T\) 中当前最大的值,\(B\) 表示 \(T\) 中当前次大值。
记 \(d = B - A\)。
若 \(d = 0\),说明 \(T\) 中只剩下一种数了,可以单独判掉。
否则,\(d\) 或 \(-d\) 一定是 \(S\) 中的元素,且一定是 \(S\) 中(除 \(0\) 外)的最小元素。
对 \(d \neq 0\) 简要说明:要么 \(B\) 加一个负数成为 \(A\),要么 \(A\) 加一个正数成为 \(B\),这里只讨论 \(A + d = B, d > 0\) 的情况。
\(A\) 经过不断加数 \(x\) 成为 \(B\) 的过程,一定有 \(x \ge 0\),否则 \(B\) 不是最大值。
假设 \(\exists x, y \in S, x > 0, y > 0\),\(A + x + y = B\),则 \(B > A + x, A + y > A\),\(A\) 不是 \(T\) 中次大值,所以 \(A\) 至多加一个 \(S\) 中的数成为 \(B\)。
又因为 \(A \neq B\),\(A\) 是次大值,所以中间缺的这一块是 \(S\) 中(非 \(0\) 元素)的最小值。
现在我们尚且只知道了 \(S\) 中一个元素的绝对值,那第二个元素、第三个元素又如何确定呢?就算确定出来了 \(S\) 中所有元素的绝对值,我们又如何得知 \(S\) 中每个元素真正的值、又如何得出字典序最小的方案呢?
01 背包中改变一个元素 \(x\) 的正负,等价于将 dp 数组整体平移 \(x\) 个单位。
神中神!这就是背包啊!
简要说明:只讨论 \(x > 0\) 的情况,其它情况类似。
现在有一个未加入 \(x\) 的 dp 数组 \(f\),考虑加入 \(x\) 的本质:
先将 \(f\) 向右平移 \(x\) 个单位,得到 dp 数组 \(f^{'}\),再将 \(f\) 与 \(f^{'}\) 合并(对应下标的 \(f\) 与 \(f^{'}\) 值相加),得到 dp 数组 \(g\)。
将 \(x\) 反号,再做一遍:先将 \(f\) 向左平移 \(x\) 个单位,得到 dp 数组 \(f^{''}\),再将 \(f\) 与 \(f^{''}\) 合并(对应下标的 \(f\) 与 \(f^{''}\) 值相加),得到 dp 数组 \(h\)。
你发现 \(g\) 和 \(h\) 唯一的区别就是 \(x\) 个单位的位置偏移!
那先强行钦定得到的 \(d\) 全部取正数,这样并不会改变 dp 数组的值以及相对关系,只会改变 dp 数组的整体位置。
所以最大值和次大值的差是不变的,那么由上述算法得出的 \(d\) 也不会失其本原的意义。也就是说,我们可以得出 \(S\) 中每个元素的绝对值,我们记这个大小为 \(m\) 的可重集为 \(D\)。
现在可以进行定号了!
如果按照正确的 \(S\) 进行撤销,最终得到的 \(T\) 应该只剩下一个元素 \(0\),表示空集。
而我们的算法中,当 \(S\) 中存在负数时,最后得到的 \(T\) 会是一个负数——由于我们把某些 \(-d\) 强行钦定成了 \(+ d\),这会导致 dp 数组发生右偏,而 \(T\) 是我们在右偏错误下观测的,相对地,我们的原点发生的左偏——结果是,原点位于 \(0\) 的左侧,一个负数 \(\Delta\)。
幸运的是,我们能够知道,一个错误的元素能够导致多少偏移量。
贪心地按绝对值从大到小判断当前值是否能够反号。
这又是一个背包问题:有 \(m\) 个非负整数,选出若干个数使得它们的和为 \(-\Delta\),并使方案最优。
将 \(D\) 降序排序,并对排序后的 \(D\) 倒序做一遍可行性背包。再正序确定每个数是否能反号,这个事情用先前处理的可行性背包判断。
实现上,由于元素数值太大,需要用 map, set
等工具进行辅助。
做可撤销背包和求答案的时间复杂度为 \(O(nm\log{n})\)。由于这里限制了 \(n \le 10^4\),所以本做法可以通过。
Code
const int N = 1e4 + 5;
const int M = 65;
int T, n, m;
PLL a[N];
LL ans[M];
map<LL, LL> mp;
set<LL> f[M];
void work(int cas)
{
mp.clear(); LL sum = 0;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%lld", &a[i].fi);
for(int i = 1; i <= n; ++i)
scanf("%lld", &a[i].se);
for(int i = 1; i <= n; ++i)
{
mp.insert(a[i]);
sum += a[i].se;
}
m = log2(sum);
for(int i = 1; i <= m + 1; ++i)
f[i].clear();
for(int i = 1; i <= m; ++i)
{
vector<PLL> vec; vec.clear();
LL mx1 = 7210721, mx2 = 7210721;
mx2 = mx1 = prev(mp.end())->fi;
if(prev(mp.end()) != mp.begin())
mx2 = prev(prev(mp.end()))->fi;
LL p = mx1 - mx2;
ans[i] = p;
for(auto now : mp)
{
LL x = now.fi, c = now.se;
if(c == 0)
{
vec.EB(MP(x, c));
continue;
}
if(mp.find(x + p) == mp.end())
continue;
if(p == 0) mp[x] >>= 1;
else mp[x + p] -= c;
}
for(auto now : vec)
mp.erase(now.fi);
}
LL delta = abs(mp.begin()->fi);
sort(ans + 1, ans + m + 1, [&](LL x, LL y){
return x > y;
});
f[m + 1].insert(0);
for(int i = m; i >= 1; --i)
for(auto x : f[i + 1])
{
f[i].insert(x);
f[i].insert(x + ans[i]);
}
for(int i = 1; i <= m; ++i)
if(f[i + 1].find(delta - ans[i]) != f[i + 1].end())
delta -= ans[i], ans[i] = -ans[i];
sort(ans + 1, ans + m + 1);
printf("Case #%d: ", cas);
for(int i = 1; i <= m; ++i)
printf("%lld ", ans[i]);
puts("");
}
int main()
{
scanf("%d", &T);
for(int cas = 1; cas <= T; ++cas)
work(cas);
return 0;
}
[ARC096F] Sweet Alchemy
将原题限制转化为父节点 \(p_i\) 向子节点 \(i\) 连边,把选取单点转化为选取子树,相当于做了一次差分,这样选取任意一个点的子树的次数都不能超过 \(d\)(根节点对应的整棵树除外)
这是一个显然的多重背包问题:选取一个子树 \(T\),对应体积 \(v_i = \sum\limits_{x \in T}m_x\),价值 \(w_i = size_T\),最多选 \(d\) 次(根节点对应的整棵树除外)。
但是体积和物品数量都是 \(10^9\) 级别的,常规的多重背包根本没法做。
贪心
这个题最厉害的地方就是把背包和贪心结合在一起。
众所周知,按 \(\frac{w_i}{v_i}\) 从大到小排序尽量多选的方法是错误的,但不妨考虑这个贪心在什么时候是对的。
尽可能地把绝大多数规模的问题划归到一个正确的贪心上以保证时间复杂度正确。
假设现在有物品 \(i, j\),\(\frac{w_i}{v_i} > \frac{w_j}{v_j}\),即 \(w_iv_j > w_jv_i\),可以看作是选 \(w_i\) 个 \(j\) 物品的体积大于选 \(w_j\) 个 \(i\) 物品的体积。同时两种选取方式的价值都是一样的,为 \(w_iw_j\),因此 选 \(w_j\) 个 \(i\) 物品一定优于选 \(w_i\) 个 \(j\) 物品。
当选择的 \(i\) 物品数量超过 \(w_j\) 个时,\(j\) 物品最多选 \(w_i - 1\) 个。
记 \(c_x\) 表示物品 \(x\) 的选取个数,则不可能出现 \(c_i \le d - w_j\) 且 \(c_j \ge w_i\) 的情况,因为可以把 \(w_i\) 个 \(j\) 物品换成 \(w_j\) 个 \(i\) 物品。
借洛谷题解里的这个图:
可知中间部分的贪心是正确的,也即中间部分不可能有两个没填满的柱子。
把每个物品取 \(\min(d, n)\) 个出来做多重背包(\(n\) 对应物品价值的上界),剩下的就按 \(\frac{w}{v}\) 从大到小能选就选。
此时多重背包价值和的上界为 \(n^3\),于是背包时将价值与体积倒过来处理。