01背包问题-等分数组的几种解法

dfs 递归+ 集合去重+剪枝

类似于背包问题,可以使用动态规划,也可以使用递归法,但不是直接暴力递归,否则复杂度为O(2^n), 通过使用集合去重后,复杂度降到O(n*(sum(nums)/2))

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        sum1 = sum(nums)
        length = len(nums)
        # print("sum1",sum1)
        if sum1 % 2 != 0: return False

        visted = set()

        def _dfs(nums,bag,cur,value):
            if bag > value:
                return 
            if bag == value:
                return True
            if cur == length:  # 这里需要注意,不能放在第一个位置,前面需要判断本节点是否满足要求,以及提前结束
                return

            bag += nums[cur] #
            if (bag,cur) not in visted: # 判断将要扩展的节点是否已经访问过
                visted.add((bag,cur))
                if _dfs(nums,bag,cur+1,value) : return True
            bag -= nums[cur] # 回溯

            if (bag,cur) not in visted: # 判断将要扩展的节点是否已经访问过
                visted.add((bag,cur))
                if _dfs(nums,bag,cur+1,value) : return True

        return _dfs(nums,0,0,sum1//2)

纯集合方法,非递归

另外一种是使用集合但非递归的方法,最简单,效率同样是O(n*(sum(nums)/2))
参考自:
https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/you-ya-de-bao-li-bi-dong-tai-gui-hua-kuai-yi-dian-/
解题思路
看了下动态规划的时间复杂度O(n * sum(nums)).而暴力法的时间复杂度O(2**n)(每一个元素都有取和不取两种情况).优化这么多的原因是当目前元素的和超过sum(nums)/2时就不用考虑啦。加上这个限制,暴力法的时间复杂度也能约束在O(n * sum(nums))以内。
实际上动态规划的时间复杂度比起暴力法还略高一些。因为动态规划每一次循环需要操作sum(nums)/2次,循环n次;而暴力法每次循环需要操作的次数必定小于sum(nums)/2(当元素总和超过sum(nums)/2就舍弃,再加上用哈希表排除重复元素).

作者:bai-yi-fei
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/you-ya-de-bao-li-bi-dong-tai-gui-hua-kuai-yi-dian-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        sum1 = sum(nums)
        if sum1%2 != 0: return False
        half = sum1 // 2
        set1 = {0}
        for num  in nums:
            tmpSet = set()
            for item in set1:
                newItem = item + num
                if newItem == half: return True
                if newItem < half : tmpSet.add(newItem)
            set1.update(tmpSet)
            # for it in tmpSet:
            #     set1.add(it)
            # print("set1",set1)

动态规划解法:

当前题目可以理解为,当背包的容量为sum(nums)//2时,对于给定的元素规模是否可以恰好装满。

背包问题的dp[i][j]: 表示当背包容量为i时,对于前j个元素(包含第j个元素)的规模,背包容量为i时候是否可以恰好装满?
初始状态:背包为空时,在所有的规模下,设定为装满了。
状态转移:

if i < nums[j-1]:
    dp[i][j] = dp[i][j-1]
else:
    dp[i][j] = dp[i][j-1] | dp[i-nums[j-1]][j-1]

当前背包容量为i,新增加的元素为nums[j-1],需要对之前的状态进行更新
当背包的容量小于新增加的元素时,背包dp[i][j]状态继承自之前的状态,于是 dp[i][j] = dp[i][j-1]
当背包的容量大约等于新增加的元素时,背包dp[i][j]状态可以继承自之前的状态,但也可以,查看上一个状态下剩余容量的状态,背包dp[i-nums[j-1]][j-1] 是否装满了

背包的容量从0到sum(nums)//2变化,每次新增加一个元素,需要遍历一次来更新

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        length = len(nums)
        sum1 = sum(nums)
        if sum1%2 != 0: return False
        half = sum1 // 2
        dp = [ [0]*(length+1) for _ in range(half+1) ] # 定义dp数组,length+1 包括空集情况,half+1 包含背包为空的情况
        for i in range(length+1):
            dp[0][i] = True
        for j in range(1,length+1):
            for i in range(1,half+1):
                if i < nums[j-1]:# 这里j-1表示当前取得的元素的下标总是比j小1,因为j从1开始,而nums从下标0开始
                    dp[i][j] = dp[i][j-1]
                else:
                    dp[i][j] = dp[i][j-1] | dp[i-nums[j-1]][j-1]
        return dp[half][length]

动态规划的压缩状态解法

滚动数组:
当前状态与后面3个状态有关:开辟长度为4的数组[0,1,2,3], 假设当前状态为15,15%4 == 3, 与后面3个状态 (15-1)%4 == 2, (15-2)%4 == 1, (15-3)%4 == 0 有关.边界条件:初始时,先初始化[0,1,2]的状态,从状态3开始迭代,然后是状态4,5,6,7…

对于本题: 当前状态只和后面一个状态有关,开辟长度为2的数组[0,1], 初始化时,状态0(0%2)是确定的,从状态1(1%2)开始迭代,然后是2(2%2),3(3%2),4(4%2),5(5%2), …length(length%2)

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        sum1 = sum(nums)
        length = len(nums)
        if sum1 % 2 != 0: return False
        half = sum1 // 2
        dp = [[0]*2 for i in range(half+1) ] # 这里的状态只有两列
        for i in range(2):
            dp[0][i] = True # 初始化状态 背包为空时,状态为真
        # print("dp",dp)
        for j in range(1,length+1): # j == 0 的状态要初始化的状态
            for i in range(1,half+1): # 
                bagSize= i
                if bagSize < nums[j-1]:# 这里j-1表示当前取得的元素的下标总是比j小1,因为j从1开始,而nums从下标0开始
                    dp[i][j%2] = dp[i][(j-1)%2] # 通过取模的方式来实现滚动数组
                else:
                    dp[i][j%2] = dp[i][(j-1)%2] or dp[bagSize-nums[j-1]][(j-1)%2]
        return dp[half][length%2]
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值