回溯相关问题
回溯r如果要展示所有的内容,三步走:设置现场,dfs,恢复现场。
需要这三步走的原因是因为要把所有的可能性都穷举出来。
写在最前前面,python的指针变量
python的变量都是指针,所以没有数据类型,即使现在能写出来也只是让大家看一下,不是java有实际的意义。变量的=就是指针的指向。
# 这个会真的改变变量,因为c虽然先指向了a,但是后面又指向新的地址[456],所以和a无关了,改变c不会改变a
a=[[1,2],[3,4],[5,6]]
c = a
c = [456]
print(a)
print(c)
# 变化
# 这是指向a里的具体元素作变化,所以a,b都跟着变
a=[[1,2],[3,4],[5,6]]
b=[num for num in a if num[-1]>2]
for e in b :
e.append(999)
print(a)
print(b)
# 如果是func形参也会改变
lst = [1,3,4]
def junge(lst_jun):
lst_jun.append(666)
return lst_jun
print(junge(lst))
print(lst)
组合 lc 77
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
import copy
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
lst = [i for i in range(1,n+1)]
def dfs(start,lst,path,res,length):
if length==k:
res.append(copy.deepcopy(path))
return
for i in range(start,len(lst)):
path.append(lst[i])
dfs(i+1,lst,path,res,len(path))
path.pop()
path = []
res = []
length = 0
start = 0
dfs(start,lst,path,res,length)
return res
组合求和
- 能重复用每一次都是i;每个只能用一次i+1;能重复用但是不能重复数字有一个visited的矩阵;看后面那个排列。
- 加和等于某一个数return,字符串被遍历完return 用index,结果的长度等于某个长度return,子集的不用return直接往里装,直到装完。
组合求和 lc 39
given candidate set [2, 3, 6, 7] and target 7,
A solution set is: [[7],[2, 2, 3]]
import copy
class Solution:
def combinationSum(self,candidates,target):
if not candidates:
return []
def dfs(start_index,candidates,target,path,res):
if target == 0:
res.append(copy.deepcopy(path))
return
for i in range(start_index,len(candidates)):
if candidates[i]>target:
continue
#设置现场
path.append(candidates[i])
#是否能减成0,dfs
dfs(i,candidates,target-candidates[i],path,res)
#恢复现场
path.pop()
path = []
res = []
start_index = 0
dfs(start_index,candidates,target,path,res)
return res
- 注意:有i 还是顺序往下走一次入栈,没有i是所有在都来一次,会出现2,3,3, 3,2,3所谓的顺序
组合2
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
import copy
class Solution:
def combinationSum2(self,candidates,target):
#有重复数字,不重复的组合要县排序
#40
if not candidates:
return []
def dfs(start_index, candidates, target, path, res):
if target == 0:
res.append(copy.deepcopy(path))
return
for i in range(start_index, len(candidates)):
if candidates[i] > target:
continue
if i>start_index and candidates[i]==candidates[i-1]:
continue
# 设置现场
path.append(candidates[i])
# 是否能减成0,dfs
dfs(i+1, candidates, target - candidates[i], path, res)
# 恢复现场
path.pop()
path = []
res = []
start_index = 0
candidates.sort()
dfs(start_index, candidates, target, path, res)
return res
- 剪枝方法都一样,先排序,目标是同一层,就是for那个位置,相同的后面在递归结果一样,剪了continue。
组合求和3 lc 216
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
import copy
class Solution:
def combinationSum3(self,k, n):
lst = [i for i in range(1,10)]
def dfs(start_index,lst,path,res,length,sum_val):
if length == k and sum_val==n:
res.append(copy.deepcopy(path))
return
for i in range(start_index,len(lst)):
path.append(lst[i])
dfs(i+1,lst,path,res,len(path),sum_val+lst[i])
path.pop()
start_index=0
path = []
res =[]
length =0
sum_val = 0
dfs(start_index,lst,path,res,length,sum_val)
return res
排列
全排列 lc 46
import copy
class Solution:
def permute(self,nums):
visited = [False]*len(nums)
def dfs(nums,visited,path,res,detph):
#每一次都要有一个走到最后的标识符
if detph == len(nums):
res.append(copy.deepcopy(path))
return
for i in range(len(nums)):
if visited[i] == False:
visited[i] = True
path.append(nums[i])
dfs(nums,visited,path,res,detph+1)
visited[i] = False
path.pop()
path = []
res = []
detph = 0
dfs(nums, visited, path, res, detph)
return res
- 与上面的不同是带标记的,如果dfs里有satart,,每次走start+1则是一直往下走,这里是每次都从头走,但是走过的不能再用。
- -这个题非常好,与前面的两个类型有鲜明的对比。
全排列 II lc 47
数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。
import copy
class Solution:
def permuteUnique(self,nums):
visited = [False]*len(nums)
def dfs(nums,visited,path,res,detph):
#每一次都要有一个走到最后的标识符
if detph == len(nums):
#没必要写set直接看是否在res中,不在才添加
if path not in res:
res.append(copy.deepcopy(path))
return
for i in range(len(nums)):
if visited[i] == False:
visited[i] = True
path.append(nums[i])
dfs(nums,visited,path,res,detph+1)
visited[i] = False
path.pop()
path = []
res = []
detph = 0
dfs(nums, visited, path, res, detph)
return res
#剪枝版本
import copy
class Solution:
def permuteUnique(self,nums):
visited = [False]*len(nums)
def dfs(nums,visited,path,res,detph):
#每一次都要有一个走到最后的标识符
if detph == len(nums):
#没必要写set直接看是否在res中,不在才添加
res.append(copy.deepcopy(path))
return
for i in range(len(nums)):
#相同层里,和后面的递归是一样的
if i>0 and nums[i] == nums[i-1] and not visited[i-1]:
continue
if visited[i] == False:
visited[i] = True
path.append(nums[i])
dfs(nums,visited,path,res,detph+1)
visited[i] = False
path.pop()
nums.sort()
path = []
res = []
detph = 0
dfs(nums, visited, path, res, detph)
return res
- 剪枝要排序,第一层拿了1,第二层就不要拿1了,因为是层数往里走才会找到最后的答案越来越长,但是同层可以拿。同层是不同的列表,相当于是一个表格的感觉。
子集
子集 lc 78
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
import copy
class Solution:
def subsets(self,nums):
#这个和之前的不同,条件是依靠for到头了之后跳出来,不是独立的条件让之停止循环,就往里放就好了
if not nums:
return
def dfs(start_index,nums,path,res):
# if len(path)<=len(nums):
# res.append(copy.deepcopy(path))
# return
res.append(copy.deepcopy(path))
for i in range(start_index,len(nums)):
path.append(nums[i])
dfs(i+1,nums,path,res)
path.pop()
res = []
start_index=0
path = []
dfs(start_index,nums,path,res)
return res
- 这是关键,和之前不一样的是这没有return,是依靠for遍历结束就停下了。
子集2
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。
import copy
class Solution:
def subsets2(self,nums):
#90
#这个和之前的不同,条件是依靠for到头了之后跳出来,不是独立的条件让之停止循环,就往里放就好了
if not nums:
return
def dfs(start_index,nums,path,res):
# if len(path)<=len(nums):
# res.append(copy.deepcopy(path))
# return
res.append(copy.deepcopy(path))
for i in range(start_index,len(nums)):
if i>start_index and nums[i]==nums[i-1]:
continue
path.append(nums[i])
dfs(i+1,nums,path,res)
path.pop()
res = []
start_index=0
path = []
nums.sort()
dfs(start_index,nums,path,res)
return res
- ==一样的方法去剪枝 if i>start_index and nums[i]nums[i-1]:
分割等和子集 lc 461
Input: [1, 5, 11, 5] Output: true Explanation: The array can be
partitioned as [1, 5, 5] and [11].
(1)回溯
class Solution:
def canPartition(self,nums):
nums_sum = sum(nums)
if nums_sum%2!=0:
return False
else:
target = nums_sum/2
def dfs(start_index,nums,target)#,path):
if target == 0:
res[0] = True
return
for i in range(start_index,len(nums)):
if nums[i]>target:
continue
#path.append(nums[i])
dfs(i+1,nums,target-nums[i])#,path)
#path.pop()
res=[False]
path = []
dfs(0,nums,target)#,path)
return res[0]
- 不需要穷举所有结果,只需要有一个正确即可。所以不需要设置现场和恢复现场。
- 因为不正确不代表说没有,即不能停,但是正确是找到了需要停,所以只有找到的地方才有return。
- 回溯的是nums里的元素,即使没有现场的设置恢复,但是仍然是顺序的套路,专门用框架写出来,并且把没用的代码注释掉,对比看下。
- 不能使用重复的元素,所以在dfs往下走的时候需要+1
(2)递归法
class Solution:
def canPartition_(self, nums):
nums_sum=sum(nums)
if nums_sum%2!=0:
return False
else:
nums_sum//=2
rec=[False]
def search(nums,i,s=0):
if i==len(nums) or s>nums_sum:
return
if nums[i]+s==nums_sum:
rec[0]=True
return
else:
search(nums,i+1,s)
search(nums,i+1,s+nums[i])
search(nums,0,0)
return rec[0]
- 这里递归非常的优秀,和最长子串异曲同工,要求在递归函数里的内容必须一样的,所以递归里都是要+当前的数字,在递归的两个分叉路口走不一样的方法。
- 因为有错的时候要退出,所以对错都有return。
硬币找零2 lc 518
找到所有找零的方案。
class Solution:
def change(self,amount, coins):
def dfs(start_index, amount, coins,res):
if amount == 0:
res[0]+=1
return
if amount<0:
return
for i in range(start_index,len(coins)):
dfs(i,amount-coins[i],coins,res)
res = [0]
start_index = 0
dfs(start_index, amount, coins, res)
return res[0]
数字键盘组合 lc 17
Input:Digit string “23”
Output: [“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
import copy
class Solution:
def __init__(self):
self.letterMap = [
' ',
'',
'abc',
'def',
'ghi',
'jkl',
'mno',
'pqrs',
'tuv',
'wxyz'
]
def flash_back(self,digits,index,res,path):
if index==len(digits):
res.append(''.join(copy.deepcopy(path)))
return
char_ = digits[index]
letters = self.letterMap[int(char_)]
for i in range(0,len(letters)):
path.append(letters[i])
self.flash_back(digits,index+1,res,path)
path.pop()
def letterCombinations(self, digits):
res = []
path = []
if not digits:
return res
# 不需要start,因为这个回溯的内容和具体的东西是分离的,只有都是自己才需要start
self.flash_back(digits,0,res,path)
return res
s = Solution()
print(s.letterCombinations('23'))
- 回溯那是进行回溯的内容。
- for的记录是记录要记录的内容,完全可以分离开,甚至不记录内容。
- 这个题目非常好,很灵活的套用回溯的框架。
IP 地址划分 lc 93
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组
成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是
“0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。Given “25525511135”,
return [“255.255.11.135”, “255.255.111.35”].
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def isNum(string):
if 0<=int(string)<=255 and str(int(string)) == string:
return True
return False
def dfs(s,n,index,path,res):
if n==0 and index == len(s):
res.append('.'.join(copy.deepcopy(path)))
return
for i in range(index,len(s)):
if isNum(s[index:i+1]):
path.append(s[index:i+1])
dfs(s,n-1,i+1,path,res)
path.pop()
else:
break
path = []
res = []
if not s or len(s)<4 or len(s)>12:
return res
dfs(s,4,0,path,res)
return res
- 两个条件的回溯。
- 问题解耦,需要遍历到字符串的结尾,需要满足四段,但是每一段是否是符合条件的数字组合。
- 一个数字一个数字加上去不能判断是否是四段,想用这个框架就需要让数字形成一个坨即可。
- 遍历字符串的方法,再循环内部的遍历+1,固定起始位置,让结尾往后推。本题和上一题一样,for循环的都不是仅仅的一个固定的内容,这个是找str
- 首位数字不能为0的标准检查方法。num=num*10+c,s.lstrip(‘0’)
在矩阵中寻找字符串 lc 79
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
For example,
Given board =
[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]
]
word = “ABCCED”, -> returns true,
word = “SEE”, -> returns true,
word = “ABCB”, -> returns false.
棋盘终于和回溯见面了
class Solution:
def __init__(self):
self.dir = [(0, 1), (0, -1), (1, 0), (-1, 0)]
def exist(self,board,word):
if not word or not board:return False
r = len(board)
c = len(board[0])
visited = [[False] * c for _ in range(r)]
res=[False]
for i in range(r):
for j in range(c):
if board[i][j] == word[0]:
self.dfs(board,word,i,j,0,visited,res)
#不管走通没走通,递归出来都要还原现场
visited[i][j] = False
return res[0]
def dfs(self,board,word,i,j,index,visited,res):
#能进入递归说明可行,要设置该值
visited[i][j] = True
if index+1 == len(word):
res[0] = True
return
for r,c in self.dir:
nr = i+r
nc = j+c
if 0<=nr<len(board) and 0<=nc<len(board[0]) and visited[nr][nc]==False and index+1<len(word):
if board[nr][nc]==word[index+1]:
self.dfs(board, word, nr, nc, index+1,visited,res)
if res[0]==True:
return
visited[nr][nc] = False
分割字符串使得每个部分都是回文数 lc131
For example, given s = “aab”,
Return
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
这个题目能想到回溯本身就非常抽象,想到要遍历所有的所有。
import copy
class Solution:
def partition(self,s):
#131
# 一共res要的个数,被加到某个值,字符串被遍历完
#是一个比较复合的事情,所以要拆解开,回文,所有的
if not s:
return
def dfs(start_index,s,path,res):
#直接用这个指针,到底了就说明遍历好了
if start_index==len(s):
res.append(copy.deepcopy(path))
return
for i in range(start_index,len(s)):
#问题拆解在这里,之前每一个解都是可用的,现在是只有符合条件的才能加入
if s[start_index:i+1] == s[start_index:i+1][::-1]:
path.append(s[start_index:i+1])
dfs(i+1,s,path,res)
path.pop()
res = []
start_index = 0
path = []
dfs(start_index, s, path, res)
return res
- 之前的题目都是解空间没问题,去找边界条件,这个题是解耦是每一次先去找到解空间,然后再遍历,很明显解空间是不重复的。
- 字符串还是双指针,一个指向头一个指向尾部,然后往后去循环。
字符是否可以拼接成给定的文本 lc 139
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
输入: s = “applepenapple”, wordDict = [“apple”, “pen”] 输出: true 解释: 返回
true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
class Solution:
'''
之前的想法是顺序思考,不断增长字符串,反向思考,不断去除列表中字符串长度,
直到正好长度为0
'''
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
def dfs(s,res):
if len(s)==0:
res[0]=True
return
for word in wordDict:
if s.startswith(word):
dfs(s[len(word)::],res)
res=[False]
dfs(s,res)
return res[0]
- 和回文串一样,抽象成给定了一个解空间,然后去探索这个解空间能不能完成一个事情。
- startswith是精髓,要不然长度还是非常麻烦的。
- 和电话号码一样,dfs和遍历的内容不一样。