回溯法解数独 python leetcode 回溯法_递归

回溯法也可以叫做回溯搜索法,它是一种搜索的方式

回溯是递归的副产品,只要有递归就会有回溯。所以回溯函数也就是递归函数,指的都是一个函数

因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

回溯法解决的问题都可以抽象为树形结构

回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度

回溯法解数独 python leetcode 回溯法_搜索_02

for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了。

  • 搜索叶子节点就是找到组合或排列(需要所有元素)。
  • 搜索所有节点就是找到子集(只需要部分元素)。

组合问题

(1)77. 组合

回溯法解数独 python leetcode 回溯法_搜索_03


可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
优化过程如下:

已经选择的元素个数:path.size();

还需要的元素个数为: k - path.size();

在集合n中至多可以从该起始位置 : n - (k - path.size()) + 1,开始遍历

为什么有个+1呢,因为包括起始位置和终止位置的n,是左闭右闭的区间。(python中是+2,因为右边是开区间)

举个例子,n = 5,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 5 - ( 3 - 0) + 1 = 3。

从3开始搜索都是合理的,可以是组合[3, 4, 5]。

res = []  #存放符合条件结果的集合
        path = []  #用来存放符合条件结果      
        def backtrack(n, k, startIdx):
            if len(path) == k:
                res.append(path[:])
                return
            for i in range(startIdx, n - (k - len(path)) + 2): # 可选数字的个数少于k就剪枝
                path.append(i)  #处理节点
                backtrack(n, k, i + 1)  #递归
                path.pop() #回溯,撤销处理的节点
        backtrack(n, k, 1)
        return res
(2)216. 组合总和 III

注意:与77. 组合 区别之一是本题集合固定的就是9个数[1,...,9],所以for循环固定i<=9

回溯法解数独 python leetcode 回溯法_搜索_04


剪枝:

  • 已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。
  • for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。
class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        res = []
        path = []
        def backtrack(k, n, sum_, startIndx):
            if len(path) == k: # len(path)==k时不管sum是否等于n都会返回
                if sum_ == n:
                    res.append(path[:])
                return
            for i in range(startIndx, 9 - (k - len(path)) + 2): # 可选数字的个数少于k就剪枝
                sum_ += i
                if sum_ > n: # 当前的和已经超出也剪枝
                    return
                path.append(i)
                backtrack(k, n, sum_, i + 1)
                path.pop()
                sum_ -= i
        backtrack(k, n, 0, 1)
        return res
(3)17. 电话号码的字母组合

注意:本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而77. 组合 和216.组合总和III 都是是求同一个集合中的组合!
参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。

注意这个index可不是 77.组合和216.组合总和III 中的startIndx了。

而是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度

class Solution:
    def __init__(self):
        self.res = []
        self.path = ''
        self.letter_map = {
            '2': 'abc',
            '3': 'def',
            '4': 'ghi',
            '5': 'jkl',
            '6': 'mno',
            '7': 'pqrs',
            '8': 'tuv',
            '9': 'wxyz',
        }
    def letterCombinations(self, digits: str) -> List[str]:
        self.res.clear()
        if not digits:
            return []
        self.backtrack(digits, 0)
        return self.res

    def backtrack(self, digits, idx):
        if idx == len(digits):  # 当遍历穷尽后的下一层时
            self.res.append(self.path)
            return
        # 单层递归逻辑 
        letters = self.letter_map[digits[idx]]
        for letter in letters:
            self.path += letter  #处理节点
            self.backtrack(digits, idx + 1)  #递归
            self.path = self.path[: -1]  #回溯,撤销处理的节点
(4)39. 组合总和

注意:与 77.组合,216.组合总和III 的区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        path = []
        n = len(candidates)
        def backtrack(candidates, target, sum_, startIndx):
            if sum_ == target:
                res.append(path[:])
                return
            # if sum_ > target:
            #     return
            for i in range(startIndx, n):
                sum_ += candidates[i]
                if sum_ > target:  # 剪枝
                    return
                path.append(candidates[i])
                backtrack(candidates, target, sum_, i) # 因为无限制重复选取,所以不是i+1
                path.pop()
                sum_ -= candidates[i]
        candidates.sort() #剪枝需要先排序
        backtrack(candidates, target, 0, 0)
        return res
(5)40. 组合总和 II

和39.组合总和 如下区别:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而39.组合总和 是无重复元素的数组candidates

本题的难点在于区别2中:集合(数组candidates)有重复元素,但输出的结果却不能有重复的组合

如果把所有组合求出来,再用set或者map去重,这么做很容易超时!

所以要在搜索的过程中就去掉重复组合

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        path = []
        n = len(candidates)
        def backtrack(candidates, target, sum_, startIndx):
            if sum_ == target:
                res.append(path[:])
                return
            for i in range(startIndx, n):
                if i > startIndx and candidates[i] == candidates[i - 1]:
                    continue  # 去重
                sum_ += candidates[i]
                if sum_ > target:
                    return
                path.append(candidates[i])
                # 每个数字在每个组合中只能使用 一次 ,所以是i + 1
                backtrack(candidates, target, sum_, i + 1)  
                path.pop()
                sum_ -= candidates[i]
        candidates.sort()
        backtrack(candidates, target, 0, 0)
        return res

分割

(6)131. 分割回文串

切割问题类似组合问题
例如对于字符串abcdef:

  • 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中选取第三个.....。
  • 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中切割第三段.....。

回溯法解数独 python leetcode 回溯法_回溯法解数独 python_05


在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线

注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。

递归用于纵向遍历,for循环用于横向遍历,当切割线迭代至字符串末尾,说明找到一种方法

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res = []
        path = []
        n = len(s)
        def backtrack(s, startIndx):
            if startIndx == n:  # 当遍历完字符串中所有的字符
                res.append(path[:])
                return
            for i in range(startIndx, n):
                tmp = s[startIndx: i + 1]
                if tmp == tmp[::-1]:  # 判断是否为回文串
                    path.append(tmp)
                    backtrack(s, i + 1)
                    path.pop()
                #else:
                #   continue
        backtrack(s, 0)
        return res
(7)93. 复原 IP 地址

注意:终止条件和131.分割回文串 的区别是本题明确要求分成4段,所以终止条件包括分割的段数和切割线切到最后
还要对不合法的情况都剪枝
大范围:[0,255]

  • 如果是0,则只能是一个0
  • 如果不是0,开头不能是0
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        res = []
        path = []
        n = len(s)
        def backtrack(s, startIndx):
            if len(path) > 4:  # 如果搜索路径大于4,剪枝
                return
            if len(path) == 4 and startIndx == n:  # 终止条件包括分割的段数和切割线切到最后
                res.append('.'.join(path))  # 用.连接列表里的数字串
                return
            for i in range(startIndx, n):
                tmp = int(s[startIndx: i + 1])
                if 0 <= tmp <= 255:  # 不合法的情况都剪枝
                    if tmp != 0 and s[startIndx] == '0':  # 不能以0开头
                        continue
                    if tmp == 0 and startIndx != i:  # 不能出现多个0
                        continue
                    path.append(s[startIndx: i + 1])
                    backtrack(s, i + 1)
                    path.pop()
                
        backtrack(s, 0)
        return res

子集

(8)78. 子集

组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!

其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。

那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for循环就要从startIndex开始,而不是从0开始

而求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合。

回溯法解数独 python leetcode 回溯法_递归_06

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = [[]]  # 开始就存入空集
        path = []
        n = len(nums)
        def backtrack(nums, startIndx):
            for i in range(startIndx, n):
                path.append(nums[i])
                res.append(path[:])  # 存入树的每个节点而不是叶子节点
                backtrack(nums, i + 1)  # 解集 不能 包含重复的子集,所以是i + 1
                path.pop()
                
        backtrack(nums, 0)
        return res
(9)90. 子集 II

注意:与 78. 子集 的区别是给出的数组中会有重复值,但输出的结果却不能有重复的组合。
所以与 40. 组合总和 II 类似要在搜索的过程中就去掉重复组合

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        res = [[]]
        path = []
        n = len(nums)
        def backtrack(nums, startIndx):
            for i in range(startIndx, n):
                if i > startIndx and nums[i] == nums[i - 1]: #遍历到重复元素时剪枝
                    continue
                path.append(nums[i])
                res.append(path[:])
                backtrack(nums, i + 1)
                path.pop()
        nums.sort() # 要先排序
        backtrack(nums, 0)
        return res
(10)491. 递增子序列

注意:与90. 子集 II 的区别是不能先排序,要进行分别进行“树枝去重”和“树层去重”
递归的参数是path, startIndx

class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []
        n = len(nums)
        def backtrack(path, startIndx):  # 这里传的是path
            if len(path) >= 2:
                if path[-1] >= path[-2]:  # “树枝去重”
                    res.append(path[:])
                else:
                    return
            for i in range(startIndx, n):
                # if i > startIndx and nums[i] <= nums[i - 1]: 
                if nums[i] in nums[startIndx:i]: # “树层去重”
                    continue
                path.append(nums[i])
                backtrack(path, i + 1)
                path.pop()    
        backtrack(path, 0)
        return res

排列

(11)46. 全排列

注意:排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。

可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题for循环就要从0开始,而不是从startIndex开始!。

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []
        n = len(nums)
        def backtrack(nums, startIndx):
            if len(path) == n:
                res.append(path[:])
                return
            for i in range(n):
                if nums[i] not in path: # 树层去重,之前取过的元素就不能再取
                    path.append(nums[i])
                    backtrack(nums, i + 1)
                    path.pop()
        backtrack(nums, 0)
        return res
(12)47. 全排列 II

注意:与 46. 全排列 的区别是给定数组会有重复元素
递归的参数是nums, path backtrack(nums[: i] + nums[i + 1:], path + [nums[i]])
取下一个元素时只能在去掉当前元素的数里面遍历

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []
        n = len(nums)
        def backtrack(nums, path):
            # if len(path) == n:  都可以
            if not nums:
                res.append(path[:])
                return
            for i in range(len(nums)):  # 改为当前数组的长度
                # if nums[i] in path:
                if i > 0 and nums[i] == nums[i - 1]:
                    continue
                # path.append(nums[i])
                # backtrack(nums[: i] + nums[i + 1:], path )  # 展开的回溯 
                backtrack(nums[: i] + nums[i + 1:], path + [nums[i]]) # 隐藏的回溯
                # path.pop()
        nums.sort()
        backtrack(nums, [])
        return res
(12)332. 重新安排行程
class Solution:
    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        tickets_dic = defaultdict(list) 
        for item in tickets:
            tickets_dic[item[0]].append(item[1])
        '''
        tickets_dict里面的内容是这样的
         {'JFK': ['SFO', 'ATL'], 'SFO': ['ATL'], 'ATL': ['JFK', 'SFO']})
        '''
        path = ['JFK']
        def backtrack(startPoint):  # 返回值是bool类型
            if len(path) == len(tickets) + 1:
                return True
            tickets_dic[startPoint].sort()
            # print(tickets_dic)
            for _ in tickets_dic[startPoint]:
                #必须及时删除,避免出现死循环
                endPoint = tickets_dic[startPoint].pop(0)
                path.append(endPoint)
                # 只要找到一个就可以返回了
                if backtrack(endPoint):
                    return True
                path.pop()
                tickets_dic[startPoint].append(endPoint)  # 恢复当前节点
            
        backtrack('JFK')
        return path
(13)51. N 皇后
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        # 回溯法
        res = []
        s = '.' * n
        def backtrack(path=[], i=0, col_selected=[], z_diag=set(), f_diag=set()):
            if i == n:
                res.append(path)
                return 
            for j in range(n):
                if j not in col_selected and i-j not in z_diag and i+j not in f_diag:
                    backtrack(path+[s[:j]+'Q'+s[j+1:]], i+1, col_selected+[j], z_diag|{i-j}, f_diag|{i+j})
            
        backtrack()
        return res
(14)37. 解数独