【LeetCode】1655. Distribute Repeating Integers 分配重复整数

在这里插入图片描述
一题多解。
首先理解问题,可以抽象为:

  • 有若干物品,大小分别为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,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[i1][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个背包里了)
  • 快速遍历所有前置状态:利用(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)

    1. 初始:全放,selected = s
    2. 减少一个,selected = (selected - 1) & s,此时k = s - selected
    3. 重复2,直到selected=0

    s = 1001010100为例:

    1. selected = 1001010100 (1111)
    2. selected = 1001010000 (1110)
    3. selected = 1001000100 (1101)
    4. selected = 1001000000 (1100)
    5. 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]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值