93. 复原 IP 地址
- 思路
- example
- 返回所有可能的有效 IP 地址: 回溯
- 切割:[左边界,右边界] + '.'
- 桶装球 (4个桶)
- 左边界: "给定"
- 如果左边界=0,则右边界也是0。
- 右边界: 切割点,模向搜索
- 桶限制:
- 4个 (深度)
- 每个size <= 3
- 数字 <= 255
- 复杂度. 时间:O(), 空间: O()
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(s, start):
if start == len(s) and len(path) == 4:
res.append('.'.join(path))
for i in range(start, len(s)):
if int(s[start:i+1]) <= 255:
path.append(s[start:i+1])
backtrack(s, i+1)
path.pop()
if s[start] == '0':
break
res, path = [], []
backtrack(s, 0)
return res
- 加上一点剪枝
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(s, start, depth):
if depth == 3 and len(s)-start > 3:
return
if depth == 4 and start == len(s):
res.append('.'.join(path))
return
for i in range(start, min(start+3, len(s))):
if int(s[start:i+1]) <= 255:
path.append(s[start:i+1])
backtrack(s, i+1, depth+1)
path.pop()
if s[start] == '0':
break
if len(s) < 4 or len(s) > 12:
return []
res, path = [], []
backtrack(s, 0, 0)
return res
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(start):
if start == n and len(path) == 4:
res.append('.'.join(path))
return
if len(path) == 4 and start < n:
return
for i in range(start, min(n, start+3)):
if s[start] == '0' and i > start:
break
num = int(s[start:i+1])
if num >= 0 and num <= 255:
path.append(s[start:i+1])
backtrack(i+1)
path.pop()
res, path = [], []
n = len(s)
backtrack(0)
return res
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(start_idx):
if start_idx == len(s):
if len(path) == 4:
res.append('.'.join(path))
return
for i in range(start_idx, min(start_idx+3, len(s))):
if s[start_idx] == '0' and i > start_idx: # !!!
break
if int(s[start_idx:i+1]) <= 255:
if len(path) == 4:
break
path.append(s[start_idx:i+1])
backtrack(i+1)
path.pop()
res, path = [], []
backtrack(0)
return res
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(start):
if start == n:
if len(path) == 4:
res.append('.'.join(path))
return
if len(path) >= 4:
return
for i in range(start, min(start + 3, n)):
if int(s[start:i+1]) <= 255:
path.append(s[start:i+1])
backtrack(i+1)
path.pop()
if s[start] == '0': # !!!
break
n = len(s)
res, path = [], []
backtrack(0)
return res
78. 子集
- 思路
- example
- 数组中的元素 互不相同
- 返回该数组所有可能的子集: 回溯
- [1,2] 和 [2,1]是重复的,只取一种。
- 桶装球
- 球(数字)不能重复取
- 注意return 条件
if start == len(nums):
return
- 复杂度. 时间:O(), 空间: O()
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def backtrack(nums, start):
res.append(path[:])
if start == len(nums):
return
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(nums, i+1)
path.pop()
res, path = [], []
backtrack(nums, 0)
return res
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def backtrack(start):
if start <= n:
res.append(path[:])
# return # !!!
for i in range(start, n):
path.append(nums[i])
backtrack(i+1)
path.pop()
res, path = [], []
n = len(nums)
backtrack(0)
return res
90. 子集 II
- 思路
- example
- 数组可能包含重复元素
- 返回该数组所有可能的子集: 回溯
-
去重 (同层)
- sort
- startIndex
- 复杂度. 时间:O(), 空间: O()
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def backtrack(nums, start):
res.append(path[:])
if start == len(nums):
return
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]: # 同层去重
continue
path.append(nums[i])
backtrack(nums, i+1)
path.pop()
res, path = [], []
nums.sort() # 排序,方便去重
backtrack(nums, 0)
return res
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def backtrack(start):
if start <= n:
res.append(path[:])
for i in range(start, n):
if i > start and nums[i] == nums[i-1]:
continue
path.append(nums[i])
backtrack(i+1)
path.pop()
nums.sort()
n = len(nums)
res, path = [], []
backtrack(0)
return res
698. 划分为k个相等的子集
- 思路
- example
- 参考: 416. 分割等和子集
0-1背包(每个数字使用一次)
dp[i][j]: 数字nums[0],...,nums[i]中选取若干个,背包容量为j所能容纳的最大价值(子集和)。
Goal: dp[-1][target] where target = sum(nums)//2 (assuming the sum is even)
class Solution:
def canPartition(self, nums: List[int]) -> bool:
sum_ = sum(nums)
if sum_ % 2 == 1: return False
target = sum_ // 2
dp = [[0] * (target+1) for _ in range(len(nums))]
for j in range(nums[0], target+1):
dp[0][j] = nums[0]
for i in range(1, len(nums)):
for j in range(target+1):
if j < nums[i]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i])
return dp[len(nums)-1][target] == target
- k > 2时如何推广上面的DP方法?
思路
盒视角
球的视角分两种情况:
1、第一个球可以不装进任何一个盒子,这样的话你就需要将剩下 n - 1 个球放入 k 个盒子。
2、第一个球可以装进 k 个盒子中的任意一个,这样的话你就需要将剩下 n - 1 个球放入 k - 1 个盒子。
球盒模型
把装有 n 个数字的数组 nums 分成 k 个和相同的集合,你可以想象将 n 个数字分配到 k 个「桶」里,最后这 k 个「桶」里的数字之和要相同。
- 回溯:球视角 (球放桶)
- 纵向:球n
- 横向:桶k
- time:
// 主函数
boolean canPartitionKSubsets(int[] nums, int k) {
// 排除一些基本情况
if (k > nums.length) return false;
int sum = 0;
for (int v : nums) sum += v;
if (sum % k != 0) return false;
// k 个桶(集合),记录每个桶装的数字之和
int[] bucket = new int[k];
// 理论上每个桶(集合)中数字的和
int target = sum / k;
// 穷举,看看 nums 是否能划分成 k 个和为 target 的子集
return backtrack(nums, 0, bucket, target);
}
// 递归穷举 nums 中的每个数字
boolean backtrack(
int[] nums, int index, int[] bucket, int target) {
if (index == nums.length) {
// 检查所有桶的数字之和是否都是 target
for (int i = 0; i < bucket.length; i++) {
if (bucket[i] != target) {
return false;
}
}
// nums 成功平分成 k 个子集
return true;
}
// 穷举 nums[index] 可能装入的桶
for (int i = 0; i < bucket.length; i++) {
// 剪枝,桶装装满了
if (bucket[i] + nums[index] > target) {
continue;
}
// 将 nums[index] 装入 bucket[i]
bucket[i] += nums[index];
// 递归穷举下一个数字的选择
if (backtrack(nums, index + 1, bucket, target)) {
return true;
}
// 撤销选择
bucket[i] -= nums[index];
}
// nums[index] 装入哪个桶都不行
return false;
}
- 桶视角 (桶装球)
- 纵向:桶k
- 横向: 球n
- time:
- 「少量多次」
深搜桶,每一层有n个球供选择(有些球已选过要跳过)
- 状态(用bit)。第j个数已经被选:1<<j (把1的二进制左移j位)。比如(1001)表示第0,3个数已经被选过。总共2^n个壮态,(111…111)表示全部n个数都已进桶。
注意,假设状态为(1001),且第一桶还没填满,那么这个状态说明0,3号球在第一个桶;这个状态可以表示第一桶有球0,第二桶有球3?不可以,因为这样的话必须要先把第一个桶填满,状态不会是(1001)。- memo(额外数组存计算过的状态), dp[state] = -1, 0, 1表示state 没有计算,不work,work三种情况。目标:dp[total], total = (1<<n)-1或者说2^n-1。返回dp[total] == 1即可。
倒推方法。最后的结果其实是反馈到最初的状态的。- 排序,加快减枝。
- bucket一个一个来填满(和为target).到最后每个数都用到时,就找到一个分组方式。因为前面有判断当和>target时会停止继续搜索,到最后只能<=target,其实<是不可能的,因为这样的话前面的bucket至少有一个的和会大于target,矛盾。另外不需要对bucket进行计数,因为target=Sum//k,每个bucket和为target,全都完成必然是用到了k个bucket.
# 超时
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
def dfs(nums, state, curSum): # dfs, 纵向:桶, 当前桶所装球的总和为curSum,一个桶可能需要多个纵向搜索来完成。
if dp[state] != -1: # memo 已计算过
return dp[state]
if state == total: # 全部球已放进桶, 根据剪枝条件,此时curSum必定=target
dp[state] = 1
return dp[state]
for j in range(n): # 横向选择:球
if state & (1<<j) == 1: continue # 第j个球已经使用过
if curSum+nums[j] > target: break # 剪枝
new_state = state | (1<<j) # 更新状态
if dfs(nums, new_state, (curSum+nums[j]) % target) == 1: # 如果curSum+nums[j]=target,自动启用下一个桶。
dp[new_state] = 1 # 修改结果,方便迅速回到最初状态,结束程序。
return dp[new_state] # 提早结束搜索
dp[state] = 0 # 失败,当前状态不管如何加别的球都不能完成任务。
return dp[state]
nums.sort() # 排序, 方便剪枝
n = len(nums)
Sum = sum(nums)
if Sum % k != 0: return False
target = Sum // k
if nums[-1] > target: return False
# 状态 e.g., (01010001)_2
total = (1<<n) - 1 # 状态总数 2^n - 1
dp = [-1] * (total+1) # 记忆化
return dfs(nums, 0, 0)
# 超时
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
def backtrack(k, bucketSum, nums, start):
nonlocal used
if k == 0: # 只剩下0个桶,成功
return True
if used in memo: # 记忆化
return memo[used]
if bucketSum == target: # i当前桶装满
res = backtrack(k-1, 0, nums, 0) # 下一个桶
memo[used] = res
return res
for i in range(start, len(nums)):
if used & (1 << i):
continue
if nums[i] + bucketSum > target:
continue
used = used | (1 << i)
bucketSum += nums[i]
if backtrack(k, bucketSum, nums, i+1):
return True # 提早返回结果
used = used ^ (1 << i)
bucketSum -= nums[i]
return False
# 不需要预先排序
sum_ = sum(nums)
if sum_ % k != 0:
return False
target = sum_ // k
used = 0 # 状态,bit
memo = [-1 for _ in range(1<<len(nums))]
return backtrack(k, 0, nums, 0)
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
# @cache
cache = dict() # 手动实现记忆化单元
def dfs(state, summ):
if (state, summ) in cache: # 从记忆中直接读取
return cache[(state, summ)]
if state == (1<<n) - 1: # 所有整数均已划分,结束递归,并返回True
# cache[(state, summ)] = True
return True
for j in range(n):
if summ + nums[j] > target: # nums已升序排列,当前数字不行,后续肯定也不行
break
if state & (1<<j) == 0: # nums[i]暂未被划分
next_state = state + (1<<j) # 划分nums[j]
if dfs(next_state, (summ+nums[j]) % target): # 划分nums[j]能形成有效方案
cache[(state, summ)] = True # 加入记忆化单元【避免后续重复计算】
return True
cache[(state, summ)] = False # 加入记忆化单元【避免后续重复计算】
return False
total = sum(nums)
if total % k != 0:
return False
n = len(nums)
target = total // k # 目标非空子集的和
nums.sort() # 升序排列
if nums[-1] > target: # 最大值超过目标子集和,无法划分
return False
return dfs(0, 0)
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
cache = dict()
def dfs(state, cur_sum):
if (state, cur_sum) in cache:
return cache[(state, cur_sum)]
if state == (1<<len(nums))-1:
return True
for j in range(len(nums)):
if state & (1<<j) == 0:
if cur_sum + nums[j] > target:
break
if dfs(state+(1<<j), (cur_sum+nums[j]) % target):
cache[(state, cur_sum)] = True
return True
cache[(state, cur_sum)] = False
return False
total = sum(nums)
if total % k != 0:
return False
target = total // k
nums.sort()
if nums[-1] > target:
return False
return dfs(0, 0)