前言
回溯算法的一系列经典题:
- 组合总和
- 不同路径
- 单词拆分
- 单词搜索
1、组合总和
leet上39题
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
典型的回溯题
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
res = []
n = len(candidates)
def backtrack(i, tmp, num):
if i == n or num > target:
return
if num == target:
res.append(tmp)
return
backtrack(i, tmp + [candidates[i]], num + candidates[i])
backtrack(i+1, tmp, num)
backtrack(0, [], 0)
return res
变种1
candidates 中的数字只能用一次
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
if not candidates: return []
candidates.sort()
n = len(candidates)
res = []
def backtrack(i, num, tmp):
if num == target:
res.append(tmp)
return
for j in range(i, n):
if num + candidates[j] > target:
break
if j > i and candidates[j] == candidates[j - 1]:
continue
backtrack(j + 1, num + candidates[j], tmp + [candidates[j]])
backtrack(0, 0, [])
return res
变种2
在变种1的基础上限定 candidates = [1, 2, 3, 4, 5, 6, 7, 8, 9]
且每种组合的长度设定为k
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
if n <= 0 or n > (k*9): return []
res = []
def backtrack(k, n, i, tmp):
if k == 0:
if n == 0:
res.append(tmp)
return
for j in range(i, 10):
if n-j < 0: break
backtrack(k-1, n-j, j+1, tmp+[j])
backtrack(k, n, 1, [])
return res
变种3
顺序不同的序列算不同组合
求组合数
这样一改
再用回溯时间就会超时
又只求组合数
就用动态规划了
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
from collections import defaultdict
dp = defaultdict(int)
dp[0] = 1
for i in range(1, target + 1):
for num in nums:
dp[i] += dp[i - num]
return dp[target]
2、不同路径
leet上62题
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
这当然可以用回溯
但会超时
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def backtrack(x,y):
num1 = 0
num2 = 0
if x == m and y == n:
return 1
if y < n:
num1 = backtrack(x, y+1)
if x < m :
num2 = backtrack(x+1, y)
return num1 + num2
return backtrack(1, 1)
这里用动态规划
# 动态规划
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[-1]
变种
设置了障碍
1 表示起始方格,且只有一个起始方格
2 表示结束方格,且只有一个结束方格
0 表示我们可以走过的空方格
-1 表示我们无法跨越的障碍
且不再是从左上到右下,只能向右向下
而是从起始到结束,要通过每个空方格
这就是标准的回溯了
class Solution:
def uniquePathsIII(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
direct = [(0, 1), (1, 0), (0, -1), (-1, 0)]
self.res = 0
start = end = None
empty = set()
for i in range(m):
for j in range(n):
if grid[i][j] == 1:
start = (i, j)
elif grid[i][j] == 2:
end = (i, j)
elif grid[i][j] == 0:
empty.add((i, j))
def backtrack(x, y):
for dx, dy in direct:
tmp_x, tmp_y = x+dx, y+dy
if not empty:
if (tmp_x, tmp_y) == end:
self.res += 1
elif (tmp_x, tmp_y) in empty:
empty.remove((tmp_x, tmp_y))
backtrack(tmp_x, tmp_y)
empty.add((tmp_x, tmp_y))
backtrack(start[0],start[1])
return self.res
3、单词拆分
leet上139题
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict
判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词
拆分时可以重复使用字典中的单词
你可以假设字典中没有重复的单词
暴力回溯会超时
拿[aaaab]来举例,它可分为[“a”,“a”,“aab”],[“aa”,“aab”],两个都包含了"aab",所以就产生了重复的递归
于是[aaaaaa…aaab]这样的例子就超时了
要记忆化
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
if not wordDict: return not s
from functools import lru_cache
@lru_cache(None)
def backtrack(s):
if not s:
return True
res = False
for i in range(len(s)+1):
if s[:i] in wordDict:
res = backtrack(s[i:]) or res
return res
return backtrack(s)
当然这题用动态规划记录更好
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
if not wordDict: return not s
n = len(s)
dp = [False] * (n+1)
dp[0] = True
for i in range(1, n+1):
for j in range(i-1, -1, -1):
if dp[j] and s[j:i] in wordDict:
dp[i] = True
break
return dp[-1]
变种
改为列出所有可能性
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
if not wordDict: return []
wordDict = set(wordDict)
n = len(s)
from functools import lru_cache
@lru_cache(None)
def backtrack(i):
if i == n:
return [[]]
res = []
for j in range(i+1, n+1):
word = s[i:j]
if word in wordDict:
tmp = backtrack(j)
for item in tmp:
res.append(item.copy()+[word])
return res
res_list = backtrack(0)
return [" ".join(words[::-1]) for words in res_list]
4、单词搜索
leet上79题
给定一个二维网格和一个单词,找出该单词是否存在于网格中
单词必须按照字母顺序,通过相邻的单元格内的字母构成
其中“相邻”单元格是那些水平相邻或垂直相邻的单元格
同一个单元格内的字母不允许被重复使用
就是回溯
就是枚举
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m, n = len(board), len(board[0])
lenth = len(word)
direct = [(-1, 0), (1, 0), (0, 1), (0, -1)]
def backtrack(x, y, k, tmp):
if k == lenth:
return True
for dx, dy in direct:
tmp_x, tmp_y = dx+x, dy+y
if 0 <= tmp_x < m and 0 <= tmp_y < n and (tmp_x, tmp_y) not in tmp and board[tmp_x][tmp_y] == word[k]:
tmp.add((tmp_x, tmp_y))
if backtrack(tmp_x, tmp_y, k+1, tmp):
return True
tmp.remove((tmp_x, tmp_y))
return False
for i in range(m):
for j in range(n):
if board[i][j] == word[0] and backtrack(i, j, 1, {(i,j)}):
return True
return False
变种
用一个列表的words替换word
找出words中在表格中的单词
直接沿用就是了
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
ans = []
for word in words:
if self.exist(board, word):
ans.append(word)
return ans
def exist(self, board: List[List[str]], word: str) -> bool:
m, n = len(board), len(board[0])
lenth = len(word)
direct = [(-1, 0), (1, 0), (0, 1), (0, -1)]
def backtrack(x, y, k, tmp):
if k == lenth:
return True
for dx, dy in direct:
tmp_x, tmp_y = dx+x, dy+y
if 0 <= tmp_x < m and 0 <= tmp_y < n and (tmp_x, tmp_y) not in tmp and board[tmp_x][tmp_y] == word[k]:
tmp.add((tmp_x, tmp_y))
if backtrack(tmp_x, tmp_y, k+1, tmp):
return True
tmp.remove((tmp_x, tmp_y))
return False
for i in range(m):
for j in range(n):
if board[i][j] == word[0] and backtrack(i, j, 1, {(i,j)}):
return True
return False
结语
四篇下来
应该对回溯有个感觉了