一题多解。
首先理解问题,可以抽象为:
- 有若干物品,大小分别为
quantity[i]
。 - 假如某个数字重复x次,可理解为有一个容量为x的背包
- 要把物品放进背包里,问能不能放完
DFS
暴力解法,直接遍历所有可能,甚至也没有怎么剪枝,竟然也不超时……
class Solution:
def canDistribute(self, nums: List[int], quantity: List[int]) -> bool:
packages = list(sorted(Counter(nums).values(), reverse=True))
quantity = list(sorted(quantity, reverse=True))
n, m = len(packages), len(quantity)
def distribute(i):
if i >= m: return True
q = quantity[i]
for j in range(n):
if packages[j] < q: continue
packages[j] -= q
if distribute(i+1):
return True
packages[j] += q
return False
return distribute(0)
记忆化DFS
其实看到这个数据大小大概就知道是个指数级别的复杂度了,那基本上就是记忆化搜索或者状态压缩DP。记忆化递归和DP其实本质上是一样的,一个自顶向下一个自底向上罢了。
不过记忆化搜索比较容易理解。这里需要记录的就是:
- 当前放到第几个物品了
- 背包容量。因为背包的顺序不重要,所以容量[1,2,1]和容量[1,1,2]其实是一回事,所以索性从大到小排,还方便剪枝。
class Solution:
def canDistribute(self, nums: List[int], quantity: List[int]) -> bool:
packages = list(sorted(Counter(nums).values(), reverse=True))
quantity = list(sorted(quantity, reverse=True))
n, m = len(packages), len(quantity)
@lru_cache(typed=False)
def distribute(i, packs):
if i >= m: return True
q = quantity[i]
for j, p in enumerate(packs):
if p < q: break
new_packs = list(packs)
new_packs[j] -= q
if distribute(i+1, tuple(sorted(new_packs, reverse=True))):
return True
return False
return distribute(0, tuple(packages))
巨快……
状态压缩DP
状态: 二进制状态,1/0表示第i个物品有/没有被放进去.
状态转移:
f[i][s]
表示:只用前i个背包,能不能装下s指示的物品。- f [ i ] [ s ] = OR k ( f [ i − 1 ] [ k ] and weight(s-k) ≤ capacity[i] ) f[i][s] = \text{OR}_k(f[i-1][k] \text{ and } \text{weight(s-k)}\le \text{capacity[i]}) f[i][s]=ORk(f[i−1][k] and weight(s-k)≤capacity[i]),OR里的每一项都表示:前一个状态k与当前状态s之间相差的那些物品全放到当前的背包里。
- 需要预处理每个状态的大小之和
weight[s]
,这样DP的时候不需要再重复计算
注意点:
-
边界条件:
- i=0的时候,只有
f[i-1][0]
为True,其它都是false f[i][0]=True
,不管有多少个背包,“不放任何物品”的要求总是可以满足的- 前置状态k=s的时候,不要漏掉了(即背包i里一个都不放,全放前面的i-1个背包里了)
- i=0的时候,只有
-
快速遍历所有前置状态:利用
(k-1) & s
!x-1
的二进制特点:假如x里存在某位为1,则x-1会把最低位的1变成0,最低位的1右边的0全都变成1,而最低位的1左边的所有位都不变。这个特点可以跟其它数字配合在一起做一点位运算,例如:对(x-1)&x
来说,x最低位的1左边所有位不变,与之后也不变,而减1之后x最低位的1右边(包括这个1)全都取反了,所以与之后等于0。因此(x-1)&x
的结果就是把最低位的1去掉。而
(k-1) & s
则有这样的特点:对于k最低位的1右边的所有位,如果在s里的对应位为1,那么在(k-1) & s
里状态也为1。
所以可以这样遍历所有要放到背包i里的物品集合selected (= s-k)
- 初始:全放,
selected = s
- 减少一个,
selected = (selected - 1) & s
,此时k = s - selected
- 重复2,直到
selected=0
以
s = 1001010100
为例:selected = 1001010100 (1111)
selected = 1001010000 (1110)
selected = 1001000100 (1101)
selected = 1001000000 (1100)
selected = 1000010100 (1011)
- …
- 初始:全放,
class Solution:
def canDistribute(self, nums: List[int], quantity: List[int]) -> bool:
packages = list(Counter(nums).values())
n, m = len(packages), len(quantity)
M = 1 << m
# 预处理每个状态的物品大小之和
weights = [0] * M
for s in range(M):
if s == 0: continue
for i in range(m):
if s & (1<<i):
weights[s] = weights[s - (1<<i)] + quantity[i]
f = []
for i, p in enumerate(packages):
f.append([])
for s in range(M):
res = False
# case 1: f[i][0]
# case 2: selected=0
if s == 0 or (i > 0 and f[i-1][s]):
res = True
else:
# case 3: selected > 0,背包i里要放东西
selected = s
while selected > 0:
prev = s - selected
if weights[selected] <= p and (prev==0 or (i>0 and f[i-1][prev])):
res = True
break
selected = (selected-1) & s
f[-1].append(res)
if f[-1][M-1]:
return True
return f[-1][M-1]