knapsack 题解

个物品,每个物品可以用二元组 表示,代表物品的体积、单价。

有若干个大小为 的背包,每个背包能装下任意多个体积和不超过 的物品,此外使用一个背包还需要花费代价,代价是此背包内所有物品单价的最大值

现在,你需要将这 个物品放入任意多个背包中,使得所有背包的代价和最小,另外你需要输出有多少种方案。

两种方案不同定义为存在两个物品 ,满足 在一个方案中被装在了同一个背包中,而另一个方案不在同一个背包中。

方案数对 取模。

对于所有数据,满足

感觉是不是不止一次见到过这个题目了。在洛谷网校看到了一次,在云斗看到了一次。感觉是一个很有趣的题目。

做法

最小值

首先我们考虑如何算出最小值。

见到这个数据范围基本可以想到状压 DP(面向数据范围编程)。有一个较为朴素的暴力。

考虑 表示已经将 集合中分为了若干组之后最小的代价。转移的时候枚举子集,判断重量和是否小于 直接转移即可。时间复杂度 。其实这道题部分 OJ 上的数据比较水,这个方法加上一些剪枝可能就过了。我手造了一个简单的 Hack( 个的重量和代价都是 的物品)卡掉了一些。

通常来说想要优化掉枚举子集有一个常见的技巧,记录最后一组的信息,考虑一次加入一个元素对于答案的影响。此时最后一组的信息包括最大值和子集和,此外你还需要记录代价。但是你发现这个结果并不是很好定义一个偏序关系,因此你只能考虑把一些信息放到状态里。比如说用 在集合 中最后一组的最大值为 ,重量和为 的最小代价。此时算法复杂度变成了 ,仍然不足以通过。此时尝试缩减状态数之类的,比如钦定从大到小加入元素,但是你仍然要记录上一个加入的元素是什么,其实缩减不了空间。

接下来说说这个神奇的正解。考虑折半搜索,先分成前后两半搜索,计算出不存在一组跨过中间的情况的最优解。接下来考虑跨过中间的最优解,令 表示左侧选择了集合 的最小代价。由于分组的代价只和最大价值有关,因此考虑将数组按照价值排序,这样代价只和右半部分有关了。因此,我们考虑分步,先处理右半部分的转移。

具体地,我们定义两个数组 ,前者存放 值,后者存放所有 对,满足第一步只转移右半部分时(左半部分不变),可以转移到 的方案。之后再枚举 的一个超集,求出其重量。那么一个合法的转移要求就是 。此时对左右两侧分别做一个双指针即可。

算法的流程如下:

  • 对于每个 ,预处理集合的重量和和代价(价值的最大值),将集合的和记作 ,代价记作 。即
  • 枚举
    • 枚举 ,向 中加入
    • 枚举 ,记录
    • 将记录下来的 对和 (请注意,不是 )排序,双指针转移到 。注意需要考虑右半边不选的情况,此时代价就是左边的最大值。

注意到两个枚举子集和的时间复杂度都是 ,需要枚举 次,因此总复杂度就是 乘上一个排序的复杂度。但是,这个仍然不足以通过这道题。

注意到对于每个集合,它的子集和超集以及差集的重量是确定的,我们只需要提前把顺序预处理出来即可。

计数

我们注意到这个转移方法虽然可以求出最小值,但是直接计数会导致很多重复情况。考虑什么时候会重复计算,容易发现一个方案的分组会以不同的顺序加入导致重复计算。此时我们只需要钦定每个集合的转移顺序即可。比如,我们钦定最后一个加入集合的数必须要包含集合中编号最小的元素,这样每个方案的分组加入集合的顺序就是确定的(编号最小的元素从大到小)。

实现细节

在考虑跨过中间的情况,如果形如 可以直接从之前右半部分的搜索复制过来,而形如 则会从 转移过来。

附件

knapsack.zip


knapsack 题解
https://blogs.sving1024.top/posts/47057/
发布于
2025年10月10日
许可协议