代码随想录算法训练营第三十天| 01背包问题 二维、01背包问题 一维、416. 分割等和子集

写代码的第三十天
开始搞背包了

01背包问题 二维

思路

插播一个视频b站上的大佬,讲的极其清晰,爱了爱了,我自己画了好几遍矩阵没画明白,听了一遍懂了:https://www.bilibili.com/video/BV1pY4y1J7na/?spm_id_from=333.337.search-card.all.click&vd_source=b8d26c5dcd36537aff4487004dbe9245看完这个我就悟了!
什么是0-1背包问题?
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划五部曲解决。
解决问题1:dp[i][j] 的的含义是什么?使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
解决问题2:递推公式是什么?dp[i][j]分为两种情况,一种是第i个放到背包中了,另一种是没有放到背包中。第一种情况下也就是第i个并没有被放到背包中,所以和上一个结果一样,所以还是dp[i-1][j];第二种情况也就是第i个放到背包中了,那么就是当i的空间大于j的情况下证明不可以装进去,那么此时的dp[i][j]就是dp[i-1][j],如果当i的空间小于j的情况,就证明可以装进去,此时的价值应该由两部分组成,第一部分是装进去的i的价值,第二部分是如果当i装进去之后此时包里还剩的空间为j-weight[i],那么第二部分的价值就是当空间为j-weight[i]大的时候的物品从0到i-1时的价值,所以是dp[i-1][j-weight[i]]+value[i]。我们要的是最大value的背包,所以最后的递推公式是max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])。
解决问题3:dp数组如何初始化?卡哥那个我理解但是写着有点别扭,所以我这个是将物品作为矩阵的列标,背包容量作为矩阵的行标,并且多加了一行在最上面作为当物品为0时,空间为j的时候的价值,所以初始化均为0,因为从下标为1的行开始才是我们要计算的位置,并且都会被生成的值覆盖,没覆盖的那些本身就是零(当物品时0的时候,占用空间为0,所以第0行全部是0,当背包容量最大为0的时候,什么东西也放不进去,所以第一列也是零,其他位置都会被后续的生成所覆盖,所以设置为什么值都行,我就设置了全是0)。
解决问题4:如何确定遍历顺序?根据dp[i][j]的含义以及递推公式我们知道dp[i][j]的值和i-1位有关,所以最好是先遍历i再遍历j。(反过来也行,但是我觉得还是先i后j比较好理解)。
解决问题5:输出搭配数组。为了判断是否和题意一致,方便后续改错。
错误第一版:if weight[i] > j:这句话一致outofrange,我的想法是每次判断当前要加入背包的weight和j的大小,如果大于j那么不能加入,只能用之前的最大值,但是需要注意的是i是从0开始计数的!!!!

def test_2_wei_bag_problem1(weight, value, bagweight):
    dp_hang = len(weight) + 1
    dp_lie = bagweight + 1
    dp = [[0 for _ in range(dp_lie)] for _ in range(dp_hang)]
    
    for i in range(1, dp_hang):
        for j in range(1, dp_lie):
            if weight[i] > j:  # 修正这里的索引错误
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1])
    
    return dp[dp_hang-1][dp_lie-1]

if __name__ == "__main__":
    m, bagweight = map(int, input().split())
    weight = list(map(int, input().split()))
    value = list(map(int, input().split()))

    result = test_2_wei_bag_problem1(weight, value, bagweight)
    print(result)

正确代码

def test_2_wei_bag_problem1(weight, value, bagweight):
    dp_hang = len(weight) + 1
    dp_lie = bagweight + 1
    dp = [[0 for _ in range(dp_lie)] for _ in range(dp_hang)]
    
    for i in range(1, dp_hang):
        for j in range(1, dp_lie):
            if weight[i-1] > j:  # 修正这里的索引错误
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1])
    
    return dp[dp_hang-1][dp_lie-1]

if __name__ == "__main__":
    m, bagweight = map(int, input().split())
    weight = list(map(int, input().split()))
    value = list(map(int, input().split()))

    result = test_2_wei_bag_problem1(weight, value, bagweight)
    print(result)

01背包问题 一维

思路

解决问题1:dp[j] 的的含义是什么?容量为j的背包,所背的物品价值可以最大为dp[j]。
解决问题2:递推公式是什么?和上面的思路是一样的,都是当前的第i个物品放或不放两种情况,如果不放就是dp[j]的值不变不动,如果放进去那么也是由两部分组成,一部分是物品i本身的价值,一部分是j减去物品i时的重量满足的最大价值,二者之和。所以递推公式是dp[j] = max(dp[j], dp[j - weight[i]] + value[i])。
解决问题3:dp数组如何初始化?根据dp数组的定义可以知道,容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。而其他的值无论初始化为多少都无所谓,因为都会被覆盖。
解决问题4:如何确定遍历顺序?倒序遍历,并且要遍历到当前i的重量的时候,此时的重量是需要更新的。其实这个题本质上还是一个对二维数组的遍历,我们在遍历二维数组的时候是根据上方和左上方的值判断当前位置的值,变成一维的时候就是压缩了一下,我们还是要根据左侧的数值进行判断当前的数值,因此我们需要保证左边的值仍然是上一层的,从右向左覆盖,所以要到序遍历。
解决问题5:输出搭配数组。为了判断是否和题意一致,方便后续改错。
正确代码:这里的for j in range(bagWeight, weight[i] - 1, -1):用的是weight【i】-1是因为range语法问题,我们要的范围是bagWeight, weight[i],但是语法上不到weight[i],所以才有-1这一步。

def test_2_wei_bag_problem(weight, value, bagWeight):
    dp = [0] * (bagWeight + 1)
    for i in range(len(weight)):
        for j in range(bagWeight, weight[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

    return dp[bagWeight]        

if __name__ == "__main__":
    m, bagweight = map(int, input().split())
    weight = list(map(int, input().split()))
    value = list(map(int, input().split()))

    result = test_2_wei_bag_problem(weight, value, bagweight)
    print(result)

416. 分割等和子集

思路

这个题我是真没想到用背包问题解决。。。
就相当于给了一个nums数组,这个数组如果能分割成两个子集,子集的值相等,返回true,否则事false。那么也就是nums数组的总和一定是偶数,如果是奇数直接返回false,在偶数的情况下我们对其进行➗2的操作,然后就变成了,找数组中的数字,和为sum(nums)/2那么返回true。向背包问题上靠拢一下,也就是物品是nums数组下标,重量是nums数组中下标对应的值,价值是nums数组中下标对应的值,也就是说我们用nums的值代表weight和value,背包最大容量是sum(nums)/2。接下来用背包问题的五部曲解决。
解决问题1:dp[j] 的的含义是什么?容量为j的背包,所背的物品价值可以最大为dp[j]。
解决问题2:递推公式是什么?当前的第i个物品放或不放两种情况,如果不放就是dp[j]的值不变不动,如果放进去那么也是由两部分组成,一部分是物品i本身的价值,一部分是j减去物品i时的重量满足的最大价值,二者之和。所以递推公式是dp[j] = max(dp[j], dp[j - weight[i]] + value[i])。这里我们选用max是因为我们想尽力去装看能不能到达sum(nums)/2的值,然后在判断。
解决问题3:dp数组如何初始化?根据dp数组的定义可以知道,容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。而其他的值无论初始化为多少都无所谓,因为都会被覆盖。
解决问题4:如何确定遍历顺序?倒序遍历,并且要遍历到当前i的重量的时候,此时的重量是需要更新的。其实这个题本质上还是一个对二维数组的遍历,我们在遍历二维数组的时候是根据上方和左上方的值判断当前位置的值,变成一维的时候就是压缩了一下,我们还是要根据左侧的数值进行判断当前的数值,因此我们需要保证左边的值仍然是上一层的,从右向左覆盖,所以要到序遍历。
解决问题5:输出搭配数组。为了判断是否和题意一致,方便后续改错。
怎么判断什么时候返回true什么时候返回false呢?重新看dp数组的含义。dp[j]代表的是当最大容量为j的时候的最大价值,本题中设置的最大价值和最大容量是一个意思,所以当最大价值==最大容量的时候此时返回True。
错误第一版:dp数组的长度应该最小是target值的长度!!!因为容量为j的背包,所背的物品价值可以最大为dp[j]!!!!!这些值是加起来的!!!所以后面for循环j的时候遍历列的范围也错了!!!

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

正确代码

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

总结

太难了啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊
太难了太难了太难了!!!!!!!!!!
两天时间我才算看懂了50%我真要哭了!!!!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值