1. 问题描述:
达达帮翰翰给女生送礼物,翰翰一共准备了 N 个礼物,其中第 i 个礼物的重量是 G[i]。达达的力气很大,他一次可以搬动重量之和不超过 W 的任意多个物品。达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。
输入格式
第一行两个整数,分别代表 W 和 N。以后 N 行,每行一个正整数表示 G[i]。
输出格式
仅一个整数,表示达达在他的力气范围内一次性能搬动的最大重量。
数据范围
1 ≤ N ≤ 46,
1 ≤ W,G[i] ≤ 2 ^ 31−1
输入样例:
20 5
7
5
4
18
1
输出样例:
19
来源:https://www.acwing.com/problem/content/description/173/
2. 思路分析:
分析题目可以知道这是一道零一背包问题,将每一个礼物看成是一个物品,每一个物品的体积为G[i],背包容量为W,需要求解背包中可以装哪些物品使得在不超过背包容量的前提下物品的体积之和最大,但是这道题目的数据范围很大,如果使用零一背包问题求解时间复杂度为O(NW) = 46 * (2 ^ 23 - 1)肯定会超时,所以不能够使用零一背包的方式求解,因为N比较小所以我们可以考虑使用dfs来解决,在所有总质量不超过W的范围内找到最大值,依次枚举当前的物品选或者不选,这样一定可以枚举出所有的方案,但是直接这样做的时间复杂度为O(2 ^ 46) 也一定会超时,所以我们需要优化一下dfs的搜索方法,类似于之前双向bfs搜索的思想,dfs也可以使用两个方向进行搜索,我们将所有物品分为两部分来搜索,其中一个dfs方法从起点开始搜索到中间位置k,也即 k = n / 2的位置,n为物品的个数,枚举前k个物品选或者不选在递归出口将得到的重量之和记录在weights中,相当于是一个预处理,可以理解为空间换时间的思想,然后对得到的前k个物品的重量之和去重和排序方便后面的二分查找操作;第一个方法在搜索的时候可以先对所有物品的重量从大到小排序,这样可以优先搜索分支数目较少的节点;使用另外一个dfs方法来处理后一部分选择物品的策略,也是分为两个策略:选或者不选,如果当前递归的位置到达了最后一个物品的位置,那么我们可以使用二分找出在weights中记录前k个物品的重量之和中累加起来的小于等于W的最大位置(相当于是查表),最终更新一下答案即可,两个方向搜索可以避免一个方向搜索空间非常大的问题,时间复杂度为O(2 ^ (N / 2) + 2 ^ (N / 2) log2 2 ^ (N / 2)) ,在实际写代码的时候N / 2可以多取两个数,这样处理使得两边更均衡一点,时间复杂度可以降低一点。
3. 代码如下:
from typing import List
class Solution:
res = 0
def dfs1(self, u: int, s: int, k: int, m: int, w: List[int], weights: List[int]):
if u == k:
# 到达了边界, s存储的对应前k个物品选或者不选的重量之和
weights.append(s)
return
# 不选
self.dfs1(u + 1, s, k, m, w, weights)
# 可以选
if s + w[u] <= m:
self.dfs1(u + 1, s + w[u], k, m, w, weights)
def dfs2(self, u: int, s: int, k: int, m: int, w: List[int], weights: List[int]):
if u >= k:
l, r = 0, len(weights) - 1
p = 0
# 二分查找出加起来和小于等于m的最大的数字对应的位置
while l <= r:
mid = l + r >> 1
if weights[mid] + s <= m:
p = mid
l = mid + 1
else:
r = mid - 1
self.res = max(self.res, s + weights[p])
return
# 不选
self.dfs2(u + 1, s, k, m, w, weights)
# 可以选当前的物品
if s + w[u] <= m:
self.dfs2(u + 1, s + w[u], k, m, w, weights)
def process(self):
# m表示重量之和不超过m, n表示n个物品
m, n = map(int, input().split())
w = list()
for i in range(n):
w.append(int(input()))
# 暴力枚举前一半可以凑出来的重量之和
k = n // 2 + 2
# 从大到小排序, 优先搜索分支数目较少的节点
w.sort(reverse=True)
weights = list()
self.dfs1(0, 0, k, m, w, weights)
# 去重之后weights存储的就是当前前k个物品可以可以拼凑出来的无重复元素的重量之和, 通过递归后一半的物品找到能够拼凑出来的最大和
weights = list(set(weights))
# 排序
weights.sort()
# 递归后一半的物品, 在递归的出口使用二分来查找前一部分与后一部分重量之和的最大值
self.dfs2(k, 0, n, m, w, weights)
return self.res
if __name__ == '__main__':
print(Solution().process())