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]