力扣之背包问题 2022-02-28~03-06

背包问题

主要是0-1背包和完全背包
重点在于边界条件(也就是一个都没有选的时候,初始应该是怎样的)和转移方程
背包九讲

背包问题

代表前i个物品放入一个大小为j的背包获取到的最大价值 ,就有
由上述状态转移方程可知, 的值只与 有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)

下面是不降维写法

01背包

416. 分割等和子集

边界条件:没有任何元素可以选取时,可以得到的和为0,因此
转移方程

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total & 1:
            return False
        target = total//2
        n = len(nums)
        dp = [[False] * (target+1) for _ in range(n+1)]
        dp[0][0] = True
        for i in range(1, n+1):
            for j in range(target+1):
                if j >= nums[i-1]:
                    dp[i][j] = dp[i-1][j-nums[i-1]] | dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j]
        return dp[-1][-1]

474. 一和零

边界条件:没有任何元素可以选取时,可以得到的字符串数量只能是0,因此时,
转移方程

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        l = len(strs)
        dp = [[[0]*(n+1) for _ in range(m+1)] for _ in range(l+1)]
        for k in range(1,l+1): # l个元素依次增加
            for i in range(m+1):
                for j in range(n+1):
                    cnt0 = strs[k-1].count('0')
                    cnt1 = strs[k-1].count('1')
                    if cnt0 <= i and cnt1 <= j:
                        dp[k][i][j] = max(dp[k-1][i][j], dp[k-1][i-cnt0][j-cnt1]+1)
                    else:
                        dp[k][i][j] = dp[k-1][i][j]
        return dp[-1][-1][-1]

494. 目标和

边界条件:没有任何元素可以选取时,元素和只能为0,因此边界条件为,或者

转移方程:这里需要用加号表示不选两种情况的数目之和

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total = sum(nums)
        if (total+abs(target)) & 1 == 1:
            return 0
        aim = (total+abs(target))//2
        n = len(nums)
        dp = [[0]*(aim+1) for _ in range(n+1)]
        # dp[i][j] 用i个数能达到j的组合数量
        dp[0][0] = 1
        for i in range(1, n+1):
            for j in range(aim+1):
                if j >= nums[i-1]:
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
                else:
                    dp[i][j] = dp[i-1][j]
        return dp[-1][-1]

879. 盈利计划

注意:下面的代码会超时
过两天再写一遍不超时的(0302),也就是改成至少达到利润k
定义:是前i项工作,恰好使用上j个人,恰好能达到利润k

class Solution:
    def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
        m = len(group) # 工作数
        maxProf = sum(profit) # 最大可能的利润
        dp = [[[0] * (maxProf+1) for _ in range(n+1)] for _ in range(m+1)]
        # 边界条件 这里的定义是“恰好用上j个人”,所以只有dp[i][0][0]才为1
        # for i in range(m+1):
        #    for j in range(n+1):
        #        dp[i][j][0] = 1 # 要改成dp[i][0][0]才是正确的
        dp[0][0][0] = 1
        for i in range(1, m+1):
            for j in range(n+1):
                for k in range(maxProf+1):
                    if k >= profit[i-1] and j >= group[i-1]:
                        dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-group[i-1]][k-profit[i-1]]
                    else:
                        dp[i][j][k] = dp[i-1][j][k]
        res = 0
        for j in range(n+1):
            for k in range(minProfit, maxProf+1):
                res += dp[-1][j][k]
        return res

完全背包

322. 零钱兑换

边界条件: 达到金额为0最少用0枚硬币

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        n = len(coins)
        dp = [[float('inf')]*(amount+1) for _ in range(n+1)]
        dp[0][0] = 0
        # for i in range(n+1):
        # dp[i][0] = 0
        for i in range(1, n+1):
            for j in range(amount+1):
                if j >= coins[i-1]:
                    dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i-1]]+1)
                else:
                    dp[i][j] = dp[i-1][j]
        return dp[-1][-1] if dp[-1][-1] != float('inf') else -1

518. 零钱兑换 II

含义:为使用前i种硬币能达到金额为j的组合总数
边界条件: 不用硬币能达到金额为0的一种
转移方程

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)
        dp = [[0] * (amount+1) for _ in range(n+1)]
        dp[0][0] = 1
        for i in range(1,n+1):
            for j in range(amount+1):
                if j >= coins[i-1]:
                    dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
                else:
                    dp[i][j] = dp[i-1][j]
        return dp[-1][-1]

对比

494(01背包)518(完全背包)的状态转移方程


会发现,它们的区别在于后面的是还是
但其实,完全背包的 还可以进一步改变

可以看出来,下面的比起上面的多了一个,也就是数量不仅限于一个。
或者,换一种理解
01背包中,求时,用到的是上一轮求得的,所以肯定只能被选一次。
而在完全背包中,求时,用到的是在本轮中之前就已经更新的值,已经更新意味着已经被选取过。

1449. 数位成本和为目标值的最大数字

含义: 前个数字达到时使用数字次数最多时的情况
自己写的代码如下,有很多可以改进的地方

  1. 初始化时,设置为float('-inf'),最后在判断时,看cnt是否大于0能知道是否能拼凑出target,就可以省略if dp[i][j-cost[i-1]]['cnt'] != -1 and的判断
  2. 使用一个from数组来记录状态,而不是用这个['list']
    还有空间优化,具体看官方题解
class Solution:
    def largestNumber(self, cost: List[int], target: int) -> str:
        n = 9
        dp = [[0]*(target+1) for _ in range(n+1)]
        for i in range(n+1):
            for j in range(target+1):
                dp[i][j] = {
                    'cnt': -1,
                    'list': [0]*(n+1),
                }
        # 边界条件
        dp[0][0]['cnt'] = 0
        for i in range(1, n+1):
            for j in range(target+1):
                if j >= cost[i-1]: 
                    if dp[i][j-cost[i-1]]['cnt'] != -1 and dp[i][j-cost[i-1]]['cnt']+1 >= dp[i-1][j]['cnt']:
                        dp[i][j]['cnt'] = dp[i][j-cost[i-1]]['cnt']+1
                        dp[i][j]['list'] = dp[i][j-cost[i-1]]['list'].copy()
                        dp[i][j]['list'][i] += 1
                    else:
                        dp[i][j]['cnt'] = dp[i-1][j]['cnt']
                        dp[i][j]['list'] = dp[i-1][j]['list'].copy()
                else:
                    dp[i][j]['cnt'] = dp[i-1][j]['cnt']
                    dp[i][j]['list'] = dp[i-1][j]['list'].copy()
        res = ""
        if dp[-1][-1]['cnt'] == -1:
            return "0"
        else:
            for i in range(9,0,-1):
                for _ in range(dp[-1][-1]['list'][i]):
                    res += str(i)
        return res

你可能感兴趣的:(力扣之背包问题 2022-02-28~03-06)