题目描述
给出一个数组nums,问能不能分为k份,每份的和都相等,数组的长度不超过16
f1-状态压缩+记忆化搜索 |
基本分析
- 数组长度不超过16?暗示可以对选择情况用二进制进行表达
- dfs的过程需要怎么实现?(1)参数就是当前的选择情况和对应的和;(2)最终退出条件就是s取到了mask,可以返回True;(3)对当前的s,需要在nums中逐个去遍历i和v,需要满足一些条件(s中i位置还是0并且p+v不能超均值),如果以上都满足了,那么dfs(s,p)就和dfs(ns, np)一条战线了;(4)如果找完了nums,都不能满足,就返回False
- 其他一些预处理的技巧?(1)和要整除(2)某个元素不能超过均值(3)对nums排序某些情况能减枝
代码
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
n = len(nums)
total = sum(nums)
if total % k != 0:
return False
line = total // k
nums.sort()
if nums[-1] > line:
return False
mask = (1<<n) - 1
@cache
def dfs(s, p):
# 选完了
if s == mask:
return True
for i, v in enumerate(nums):
# s中i处需要没选,是0
if s>>i & 1:
continue
# 大可以直接减枝,后面肯定不满足
if p + v > line:
break
# 如果后续能满足,那么s时候可以满足
if dfs(s | 1<<i, (p+v)%line):
return True
return False
return dfs(0, 0)
复杂度
时间:状态个数是\(2^n, 每个状态尝试n次,最终是O(n \times 2^n)\)
空间:\(状态数组的开销是O(2^n)\)
总结
- 其实还是从没选到选的顺序进行递推,只不过把后面当做了已知
f2-状态压缩+动态规划 |
基本分析
- 这里和存在哪里?用f[s]来表示s当前对应的%line的值,-1表示还没有选或者不能选,其余值对应dfs里面的p
- 往后递推的思路?和dfs中是类似的,(1)这里先遍历每个可能的s,s不满足就去下一个s;(2)如果s满足,去遍历可能的i和v,需要满足s中i处是0,且f[s]的值+v不超过均值,如果以上都满足了,如果新的ns没有更新,那么就更新他
- 最后怎么判断是不是满足要求?最后看填满时候f[-1]的值,如果是-1或者其他非0的值,那就不满足
代码
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
n = len(nums)
total = sum(nums)
if total % k != 0:
return False
line = total // k
nums.sort()
if nums[-1] > line:
return False
mask = 1<<n
f = [-1] * mask
f[0] = 0
# 对当前所有的状态进行遍历
for s in range(mask):
# 如果s状态不能满足,那么就下一个s
if f[s] == -1:
continue
# 遍历可能的数字
for i, v in enumerate(nums):
# 首先还是需要考虑s中不为1的位置
if s >> i & 1:
continue
# 其次和不能超
if f[s] + v > line:
continue
# 都满足了,就对新的ns进行更新,为什么需要判断f[ns]==-1的情况?可能是结果唯一,再更新没意义
ns = s | 1<<i
if f[ns] == -1:
f[ns] = (f[s] + v) % line
return f[-1] == 0
复杂度
时间:\(状态个数是2^n, 每个状态尝试n次,因此O(n \times 2^n)\)
空间:\(状态数组空间开销是O(2^n)\)
总结
- 和dfs的区别在哪?(1)总的递推过程是类似的;(2)这里需要显式的先去遍历可能的所有s,dfs只是在最后和他的后续可能建立关系;(2)dfs中对和直接用参数进行传递,这里显式的定义了f数组,兼顾存和和判断是当前否当前可行的作用;(3)结束的时候,这个f[-1]需要判断和0的关系,dfs里面直接取到mask就返回True了?