回溯法主要体现在排列、组合、子集问题上。
# coding = "utf-8"
def generateParenthesis(n):
'''
数字 n 代表⽣成括号的对数,请你设计⼀个函数,⽤于能够⽣成所有可能的并且有效的括号组合。
有效括号组合需满⾜:左括号必须以正确的顺序闭合。
leetcode:22. 括号⽣成
input:n=3
output:["((()))","(()())","(())()","()(())","()()()"]
思路:
1. 利用回溯法,将所有的2n个括号进行组合
2. 回溯判断条件:
1) 当左右括号计数同时为n个的时候,说明括号数正好
2) 当左右括号其中一个大于n时,说明括号不合法
3) 当右括号数大于左括号时,说明括号不合法,这个判断条件很重要,可以筛选掉类似"())("这类不合法的组合
3. 初始条件:左右括号数为0,path为""
4. 入参:入参即左右括号数,记录路径的path
'''
if n == 0:
return []
res = []
def backtrack(left, right, trace):
if left == n and right == n:
res.append(trace)
if left > n or right > n or left < right:
return
backtrack(left + 1, right, trace + "(")
backtrack(left, right + 1, trace + ")")
backtrack(0, 0, "")
print(res)
return res
def solveSudoku(board):
'''
编写⼀个程序,通过填充空格来解决数独问题,数独的解法需遵循如下规则:
1、数字 1-9 在每⼀⾏只能出现⼀次。
2、数字 1-9 在每⼀列只能出现⼀次。
3、数字 1-9 在每⼀个以粗实线分隔的 3x3 宫内只能出现⼀次。
数独部分空格内已填⼊了数字,空⽩格⽤ '.' 表示。
leetcode: 37. 解数独
input:board = [["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]]
output:[["5","3","4","6","7","8","9","1","2"],
["6","7","2","1","9","5","3","4","8"],
["1","9","8","3","4","2","5","6","7"],
["8","5","9","7","6","1","4","2","3"],
["4","2","6","8","5","3","7","9","1"],
["7","1","3","9","2","4","8","5","6"],
["9","6","1","5","3","7","2","8","4"],
["2","8","7","4","1","9","6","3","5"],
["3","4","5","2","8","6","1","7","9"]]
思路:
1. 回溯算法
2. 每个格子遍历1-9,遇到不合法的数字则跳过:
1). 横向数字有重复的
2). 竖向数字有重复的
3). 九个格子内的数字有重复的
3. 如果找到一个合适的数字就找下一个空格
'''
m, n = 9, 9
def backtrace(board, i, j):
if j == m:
# 到最后一列,触发下一行
return backtrace(board, i + 1, 0)
if i == m:
# 到最后一行,触发base case
return True
if board[i][j] != '.':
# 由于先判断j在前,所以不用担心下一个j即j+1会越界
return backtrace(board, i, j + 1)
for ch in range(1, 10):
if not is_valid(board, i, j, str(ch)):
continue
board[i][j] = str(ch)
if backtrace(board, i, j + 1):
return True
board[i][j] = '.'
return False
def is_valid(board, row, col, n):
for i in range(9):
if board[row][i] == n:
return False
if board[i][col] == n:
return False
if board[(row / 3) * 3 + i / 3][(col / 3) * 3 + i % 3] == n:
return False
return True
backtrace(board, 0, 0)
return board
def subsets(nums):
'''
输入一个不包含重复数字的数组,要求算法输出这些数字的所有子集。
解集不能包含重复的⼦集。你可以按任意顺序返回解集
leetcode: 78. ⼦集
input:nums = [1,2,3]
output:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
思路:
1. [1,2,3]的子集是
2.
3.
'''
import copy
path, res = [], []
def backtrack(start, trace):
# 由于python是取的标签,需要copy下
t = copy.copy(trace)
res.append(t)
for i in range(start, len(nums)):
trace.append(nums[i])
backtrack(i + 1, trace)
trace.pop()
backtrack(0, path)
print(res)
return res
def combine(n, k):
'''
输入两个数字 n, k,算法输出 [1..n] 中 k 个数字的所有组合。
leetcode: 77. 组合
input: n = 4, k = 2
output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
思路:
1. 回溯
2. 注意是从1开始的,因此range的时候尾部+1,即range(start, n + 1)
3. 更新start部分
'''
import copy
res, path = [], []
def backtrace(start, path):
if len(path) == k:
p = copy.copy(path)
res.append(p)
return
for i in range(start, n + 1):
path.append(i)
backtrace(i + 1, path)
path.pop()
backtrace(1, path)
print(res)
return res
def permute(nums):
'''
输入一个不包含重复数字的数组 nums,返回这些数字的全部排列。
leetcode:46. 全排列
input:[1,2,3]
output:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路:
1. 回溯法
2. 需要判断提出无效排列,比如[1,1,1], [1,2,1]
3.
'''
import copy
res, path, n = [], [], len(nums)
def backtrace(path):
if len(path) == n:
p = copy.copy(path)
res.append(p)
return
for i in range(n):
# 排除的条件
if nums[i] in path:
continue
path.append(nums[i])
backtrace(path)
path.pop()
backtrace(path)
print(res)
return res
def solveNQueens(n):
'''
给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
攻击:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
leetcode: 51. N 皇后
input: n
output:
思路:
1. 加入判断条件的回溯法,本质上是一个排列问题
2. is_valid函数用来判断Q的位置是否合适,判断同一列、左上、右上是否有Q
3. 同一行不需要判断是因为有撤销选择,撤销后,该行不存在Q
'''
import copy
board = [['.'] * n for _ in range(n)]
res = []
def backtrace(board, row):
if row == n:
b = copy.deepcopy(board)
res.append(b)
return
for col in range(n):
# 排除的条件
if not is_valid(board, row, col):
continue
board[row][col] = 'Q'
backtrace(board, row + 1)
board[row][col] = '.'
def is_valid(board, row, col):
# 同一列
for i in range(n):
if board[i][col] == 'Q':
return False
# 右上方
i, j = row - 1, col + 1
while i >= 0 and j < n: # 注意循环条件是带0的
if board[i][j] == 'Q':
return False
i -= 1
j += 1
# 左上方
i, j = row - 1, col - 1
while i >= 0 and j >= 0: # 注意循环条件是带0的
if board[i][j] == 'Q':
return False
i -= 1
j -= 1
return True
backtrace(board, 0)
print(res)
print(len(res))
return board
def findTargetSumWays(nums, target):
'''
给你⼀个整数数组 nums 和⼀个整数 target,向数组中的每个整数前添加 '+' 或 '-',然后串联起所有整数,可以构造⼀个表达式。
例如,nums = [2, 1],可以在 2 之前添加 '+',在 1 之前添加 '-',然后串联起来得到表达式 "+2-1"。
返回可以通过上述⽅法构造的、运算结果等于 target 的不同表达式的数⽬。
leetcode: 494. ⽬标和
input:nums = [1,1,1,1,1], target = 3
output:5
⼀共有 5 种⽅法让最终⽬标和为 3。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
思路:
1. 回溯法
1) 本质上是组合问题,给定一个start值
2) 一次遍历需要做两次选择,一次是选择"+",另一次是选择"-"
3) base case是start达到数组的长度和s(路径或者本题的运算值)值与target相等
2. 动态规划
1). sum(A)-sum(B)=target => sum(A)=target+sum(B)
2). 2sum(A) = target+sum(B)+sum(A) => sum(A)=(target+sum(nums))/2
3). 转换为求subsets(nums, sum(A)), 即nums中存在几个子集,使得其和为(target+sum(nums))/2
4). 转换为有一个背包的容量为sum(A), 有N个物品,每个物品重量为nums[i-1],每个物品只放一次,有多少种不同的方法存放?
5). 状态:背包的容量和可选择的物品
选择:装背包和不装背包
dp数组:dp[i][j]代表前i个物品中,背包容量为j,有dp[i][j]种方法存放
base case:dp[0][.]代表没有物品,自然存放方法为0;dp[.][0]代表载重为0,那么什么都不放也是一种选择,因此为1
状态
'''
class Track:
def __init__(self, nums, target):
self.res = 0
self.nums_size = len(nums)
self.target = target
def backtrace(self, i, s):
if i == self.nums_size:
if s == self.target:
self.res += 1
return
# 选择"+"
s += nums[i]
self.backtrace(i + 1, s)
s -= nums[i]
# 选择"-"
s -= nums[i]
self.backtrace(i + 1, s)
s += nums[i]
class DP:
def __init__(self, nums, target):
self.nums = nums
self.target = target
self._sum = 0
self._target = 0
def find_ways(self):
for num in self.nums:
self._sum += num
# 剔除目标值大于全是和值的情况 以及 和值与目标值加起来为奇数的情况
if self._sum < self.target or (self._sum + target) % 2 == 1:
return 0
# 根据公式得到新的target
self._target = (self._sum + self.target) // 2
return self.subsets()
def subsets(self):
n = len(self.nums)
dp = [[0] * (self._target + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = 1
for i in range(1, n + 1):
for j in range(self._target + 1):
# 背包容量足
if j >= self.nums[i - 1]:
# dp[i - 1][j]表示不把物品放入背包,结果取决于上一个j容量时的状态
# dp[i - 1][j - self.nums[i - 1]]表示把物品放入背包,结果取决于上一个容量为j - self.nums[i - 1]容量时的状态
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - self.nums[i - 1]]
# 背包容量不足
else:
dp[i][j] = dp[i - 1][j]
print(dp)
return dp[n][self._target]
t = Track(nums, target)
t.backtrace(0, 0)
print(t.res)
dp = DP(nums, target)
res = dp.find_ways()
# res = dp.subsets()
print(res)
return res
def canPartitionKSubsets(nums, k):
'''
给定⼀个整数数组 nums 和⼀个正整数 k,找出是否有可能把这个数组分成 k 个⾮空⼦集,其总和都相等。
leetcode: 698. 划分为 k 个相等的⼦集
input:nums = [4, 3, 2, 3, 5, 2, 1], k = 4
output:True
思路:
1.
2.
3.
'''
class Trace:
def __init__(self, nums, target, k):
self.bucket = [0] * k
self.k = k
self.nums = nums
self.target = target
def backtrace(self, index):
if len(self.nums) == index:
for i in range(self.k):
if self.bucket[i] != self.target:
return False
return True
for i in range(self.k):
if self.bucket[i] > self.target:
return False
self.bucket[i] += self.nums[index]
if self.backtrace(index + 1):
return True
self.bucket[i] -= self.nums[index]
return False
if len(nums) < k:
return False
s = 0
for num in nums:
s += num
if s % k != 0:
return False
target = s // k
t = Trace(nums, target, k)
res = t.backtrace(0)
print(res)
return res
if __name__ == "__main__":
# generateParenthesis(3)
# subsets([1, 2, 3])
# combine(4, 2)
# permute([1, 2, 3])
# solveNQueens(8)
# findTargetSumWays([1, 1, 1, 1, 1], 3)
canPartitionKSubsets([4, 3, 2, 3, 5, 2, 1], 4)
canPartitionKSubsets([4, 3, 2, 3, 5, 2, 5], 4)