Leetcode回溯

理论

1. 回溯三步曲

  • 回溯函数模板返回值以及参数
  • 回溯函数终止条件
  • 回溯搜索的遍历过程

在这里插入图片描述

2. 代码模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}
// for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历

3. 应用场景

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

组合问题

77. 组合

在这里插入图片描述
分析
在这里插入图片描述

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        res = []
        path = []
        def backtrack(start, end, knum):
            if len(path) == k:
                res.append(path[:])  # path[:]            
                return
            
            for i in range(start, n+1):
                path.append(i)
                backtrack(i+1, n, knum-1)  # 从i+1开始        
                path.pop()
        
        backtrack(1,n,k)  # 开始 终止 选k个
        return res

代码优化(剪枝):
若开始start到结尾n序列长度(n-start+1)小于需要的长度(knum),则无需遍历了
即要求:n-start+1>=knum, 所以start<=n-knum+1 range左闭右开,所以剪枝操作为for i in range(start, n-knum+2)

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        res = []
        path = []
        def backtrack(start, end, knum):
            if len(path) == k:
                res.append(path[:])  # path[:]            
                return
            
            for i in range(start, n-knum+2):  # 优化(剪枝)
                path.append(i)
                backtrack(i+1, n, knum-1)  # 从i+1开始        
                path.pop()
        
        backtrack(1,n,k)  # 开始 终止 选k个
        return res

17. 电话号码的字母组合

在这里插入图片描述
在这里插入图片描述

class Solution:
    def __init__(self):
        self.answer = ''  # 这里是字符串不是列表  最终结果answers才是列表形式
        self.answers = []
        self.map = {
            '2':'abc',
            '3':'def',
            '4':'ghi',
            '5':'jkl',
            '6':'mno',
            '7':'pqrs',
            '8':'tuv',
            '9':'wxyz'        
            }

    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:  # 空
            return []           
        self.backtrack(digits, 0)
        return self.answers

    def backtrack(self, digits, index):
        if index == len(digits):  # 不是len(digits)-1  否则最后输出长度少1
            self.answers.append(self.answer[:])
            return
        
        for letter in self.map[digits[index]]:
            self.answer += letter  # 字符串增加
            self.backtrack(digits, index+1)
            self.answer = self.answer[:-1]  # 字符串去除最后一个元素  切片      

注意字符串和列表的处理不同

39. 组合总和

在这里插入图片描述
终止条件:当和大于target直接跳出,若和等于target,该结果保留

class Solution:
    def __init__(self):
        self.answer = []
        self.answers = []           
    
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:    
        n = len(candidates)
        self.backtracking(0, n, candidates, target, 0)
        return self.answers
    
    def backtracking(self, startindex, n, candidates, target, add):
        if add>target:
            return
        elif add == target:
            self.answers.append(self.answer[:])
            return
            
        for i in range(startindex, n):
            self.answer.append(candidates[i])
            add += candidates[i]
            self.backtracking(i, n, candidates, target, add)  ## 下一次递归的起始为这一次放入answer中的索引  不是startindex
            add -= candidates[i]  ## 当前和也要回溯
            self.answer.pop()

注意三个bug:
self.answers.append(self.answer[:])一定是有[:]
self.backtracking(i, n, candidates, target, add)递归时起点和当前处理的节点一致 i
add -= candidates[i]不仅answer回溯,和也要回溯

简化写法:

class Solution:
    def __init__(self):
        self.path = []
        self.answers = []

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:        
        candidates.sort()
        self.backtracking(candidates, target)
        return self.answers
    
    def backtracking(self, candidates, target):
        if target==0:
            self.answers.append(self.path[:])
            return 

        for index, num in enumerate(candidates):
            if num<=target:
                self.path.append(num)
                self.backtracking(candidates[index:], target-num)  # 为结果不重复,每次递归时候不遍历当前索引之前的元素
                self.path = self.path[:-1]
            else:
                break   # 如果不排序的话这里不能直接跳出,要继续遍历

40. 组合总和 II

在这里插入图片描述
注:和上一题区别,元素可以重复使用,一个元素可能出现多次,但结果要去重(涉及到同一层出现过的元素就不能再用)
在这里插入图片描述

class Solution:
    def __init__(self):
        self.answer = []
        self.answers = []

    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        n = len(candidates)
        candidates.sort()  # 对candidate进行排序  方便后面跳过重复的  实现去重
        self.backtracking(0, n, candidates, target, 0)
        return self.answers

    def backtracking(self, startindex, n, candidates, target, sum_):
        if target<sum_:
            return
        if target==sum_:
            self.answers.append(self.answer[:])
            return 

        for i in range(startindex, n):
            if candidates[i] == candidates[i-1] and i>startindex:  ## i>startindex 要对同一树层使用过的元素进行跳过
                continue
            self.answer.append(candidates[i])
            sum_ += candidates[i]
            self.backtracking(i+1, n, candidates, target, sum_)
            self.answer.pop()
            sum_ -= candidates[i]

注意三个bug:
self.backtracking(i+1, n, candidates, target, sum_)递归时起点在当前处理的节点后1
if candidates[i] == candidates[i-1] and i>startindex:同一数层,前面已经使用过的元素就不能再用了,否则会和前面的结果重复(如[10,1,2,7,6,1,5], target=8若不跳过第二个1,就会出现
[[1,1,6],[1,2,5],[1,7],[1,2,5],[1,7],[2,6]]),因为一个元素可用多次,为方便得出之前有无出现,要先对candidate排序

216. 组合总和 III

在这里插入图片描述

class Solution:
    def __init__(self):
        self.path = []
        self.answer = []

    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        self.backtracking(1, k, n, 0)  # 最后的0是当前的和
        return self.answer

    def backtracking(self, start, k, n, sum_):
        if sum_ > n or len(self.path)>k:
            return 
        if sum_ == n and len(self.path)==k:
            self.answer.append(self.path[:])
            return

        for i in range(start, 10-(k-len(self.path))+1): # 优化 若起始点到最后一个元素的长度要大于等于(k-len(self.path))        # for i in range(start, 10):
            sum_ += i
            self.path.append(i)
            self.backtracking(i+1, k, n, sum_)
            sum_ -= i
            self.path.pop()

分割问题

131. 分割回文串

在这里插入图片描述

在这里插入图片描述
在上图做一点小改变,已分割过的之后不再出现,即,第二层为ab,b,[],这样不需要起始索引,只要递归的时候输入的s变化即可

class Solution:
    def __init__(self):
        self.path = []
        self.answer = []

    def partition(self, s: str) -> List[List[str]]:
        self.backtracking(s)
        return self.answer
    
    def backtracking(self, s):
        if not s:  # 当传入的s为空时,说明遍历到了结尾,path结果添加到answer
            self.answer.append(self.path[:])
            return 

        for i in range(len(s)): 
            if self.isPalindrome(s[:i+1]): # 若是回文则添加 
                self.path.append(s[:i+1])
            else:
                continue
            
            self.backtracking(s[i+1:])  # 每次待分割的s都会去掉之前已分割的部分
            self.path.pop()

    def isPalindrome(self, s):  # 判断是否回文
        i = 0
        j = len(s)-1 
        while i<=j:
            if s[i]==s[j]:
                i += 1
                j -= 1
            else:
                return False
        return True

优化:使用动态规划保留以i起始,j结尾的字符串是否为回味子串,避免在判断回文时做一些重复操作

93. 复原 IP 地址

在这里插入图片描述
在这里插入图片描述

分析:
单层操作:对字符串s分割,前1个元素,前2个元素,……,如果遍历到前四个元素,则直接跳出这一层循环
若分割出来的字符串满足要求,则添加其至path中,并添加’.',然后进行递归操作,注意递归函数传的s是去掉已经使用过的部分,之后回溯(添加的点也要去除)

终止条件:如果遍历到第四段(所以要用一个splitnum记录已分割的段数),并且剩下的s也满足要求,则将已有的path加上s添加到answer中(注:由于没有真正地将s添加至path,所以这一步不需要回溯),return
在这里插入图片描述

class Solution:
    def __init__(self):
        self.path= ''
        self.answer = []

    def restoreIpAddresses(self, s: str) -> List[str]:
        self.backtracking(s, 0)
        return self.answer

    def backtracking(self, s, splitnum):
    	if len(s)>(4-splitnum)*3:  # 剪枝(可加可不加)
            return 
        if splitnum == 3: 
            if self.isValid(s):
                self.answer.append(self.path[:] + s)
                return
            else:
                return
        
        for i in range(len(s)):
        	if i>2:  #此时有四个元素  直接跳出这一层  剪枝
                break
            if self.isValid(s[:i+1]):               
                self.path += s[:i+1] 
                self.path += '.'
                self.backtracking(s[i+1:], splitnum + 1)
                self.path = self.path[:-(i+1)-1]

    def isValid(self, s):      
        if len(s)>1 and s[0] == '0':
            return False    
        if s and 0<=int(s[:])<=255:            
            return True
        return False

小结:这两个分割问题中注意递归函数的待分割对象改变(无需使用startindex标记)

子集问题

78. 子集

在这里插入图片描述

在这里插入图片描述

class Solution:
    def __init__(self):
        self.path = []
        self.answer = [[]]  #空集是任何一个集合的子集

    def subsets(self, nums: List[int]) -> List[List[int]]:
        self.backtracking(nums)
        return self.answer

    def backtracking(self, nums):
        if not nums:  # nums为空,则跳出
            return 

        for i in range(len(nums)):
            self.path.append(nums[i])
            self.answer.append(self.path[:])  # 每个路径都添加到answer中,而不是最后的path
            self.backtracking(nums[i+1:])
            self.path.pop()

90. 子集 II

在这里插入图片描述
分析:和78.子集区别就是集合里有重复元素了,而且求取的子集要去重。画图可以看出,同一树层上重复的数字要过滤掉,同一树枝上就可以重复取,因为同一树枝上元素的集合才是唯一子集!
思考:这里的去重操作和40.数组总和Ⅱ同,先排序,后续遍历到和之前一样的直接跳过不做处理

class Solution:
    def __init__(self):
        self.path = []
        self.answer = [[]]

    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()  # 排序 便于之后去重
        self.backtracking(nums)
        return self.answer
    
    def backtracking(self, nums):
        if not nums:
            return 
        
        for i in range(len(nums)):
            if i>0 and nums[i]==nums[i-1]:  # 若同一层一个分支和前一个相同,则直接跳过,进入下一个分支  去重
                continue
            self.path.append(nums[i])
            self.answer.append(self.path[:])
            self.backtracking(nums[i+1:])
            self.path.pop()

491. 递增子序列

在这里插入图片描述
分析:数组nums有重复元素,但输出要求去重,不能直接利用之前的思路对其排序,同层遍历相邻相同的直接跳过,因为输出要递增,所以可以使用集合/哈希表记录同层已出现的元素
元素添加到path的条件是要小于等于path末尾的元素
path添加到answer的条件是长度大于等于2
在这里插入图片描述

class Solution:
    def __init__(self):
        self.path = []
        self.answer = []

    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        self.backtracking(nums)
        return self.answer
        
    def backtracking(self, nums):
        if not nums:
            return 
        
        # 层遍历
        set_ = set()  # 每层都有一个set_用于记录已经出现过的元素
        for i in range(len(nums)):
            if nums[i] in set_:  # 对一个数层中重复出现的元素直接跳过
                continue 
            if self.path and nums[i]<self.path[-1]:  # path中有值,且当前元素小于path中的值 则不满足递增要求
                continue

            self.path.append(nums[i])
            set_.add(nums[i])  # 使用过的元素放在set_中

            if len(self.path)>=2: 
                self.answer.append(self.path[:])
            self.backtracking(nums[i+1:])
            self.path.pop()

注意用于记录每层出现过的元素的集合set_的设置,在每层for循环前

子集问题小结:

注意画图,每次只选一个元素
去重操作:同层元素不能相同(对一个节点而言)
①输出有序,则不能先对nums排序,只能用集合记录同层已出现的元素实现去重②输出无序,可以先对nums排序,同层中若nums[i]==nums[i-1]则continue

排列问题

46. 全排列

在这里插入图片描述
在这里插入图片描述

注意:这里是纵向去重,同一个树枝不能出现已出现过的元素if nums[i] not in self.path

class Solution:
    def __init__(self):
        self.path = []
        self.answer = []

    def permute(self, nums: List[int]) -> List[List[int]]:
        self.backtracking(nums)
        return self.answer
    
    def backtracking(self, nums):
        if len(self.path)==len(nums):
            self.answer.append(self.path[:])
            return 
        for i in range(len(nums)):
            if nums[i] not in self.path:  # 纵向去重
                self.path.append(nums[i])
                self.backtracking(nums)
                self.path.pop()

另一种写法:
在这里插入图片描述

class Solution:
    def __init__(self):
        self.path = []
        self.answer = []

    def permute(self, nums: List[int]) -> List[List[int]]:
        self.backtracking(nums)
        return self.answer
    
    def backtracking(self, nums):
        if not nums:
            self.answer.append(self.path[:])
            return 
        for i in range(len(nums)):            
            self.path.append(nums[i])
            midnums = nums[:i]
            midnums.extend(nums[i+1:])  # 去掉nums[i]的新数组
            self.backtracking(midnums)
            self.path.pop()

python语法:两个数组合并 a.extend(b),注意a改变

a = [1,2,3,4,7,5,6]
b = ['a','b']
c = ['h',12,'c']
a.extend(b)
a.extend(c)
print(a)

#结果:[1, 2, 3, 4, 7, 5, 6, 'a', 'b', 'h', 12, 'c']

47. 全排列 II

在这里插入图片描述
分析:46. 全排列基础上同数层去重(可用哈希表也可对原nums排序(因为结果可按任意顺序))

class Solution:
    def __init__(self):
        self.path = []
        self.answer = []

    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        self.backtracking(nums)
        return self.answer

    def backtracking(self, nums):
        if not nums:
            self.answer.append(self.path[:])
            return 
        set_ = set()
        for i in range(len(nums)):
            if nums[i] not in set_:
                set_.add(nums[i])
                self.path.append(nums[i])
                midnums = nums[:i]
                midnums.extend(nums[i+1:])
                self.backtracking(midnums)
                self.path.pop()
            else:
                continue

棋盘问题(有点复杂,hot100无)

51. N皇后

在这里插入图片描述

在这里插入图片描述

class Solution:
    def __init__(self):
        self.path = []  # 存放每行皇后所在的位置
        self.answer = []

    def solveNQueens(self, n: int) -> List[List[str]]:
        if n==1:
            return [["Q"]]
        elif n==2 or n==3:
            return []                
        self.backtracking(n)
        return self.answer
    
    def backtracking(self, n):
        if n==len(self.path):
            self.answer.append([f"{'.'*(col)}Q{'.'*(n-col-1)}" for col in self.path])
            return

        for i in range(n):
            # 去掉不满足要求的
            if i in self.path:  # 去掉同列的
                continue


            ### 这个不在同一斜线上的判断很重要!!!易写错
            pos = i # 当前所处的列
            sign = True
            # 判断当前列的左45°是否有皇后            
            for row in range(len(self.path)-1, -1, -1): # pos-1 是上一行的皇后不能在的列
                if pos-1==self.path[row]:
                    sign = False
                    break                    
                else:
                    pos -= 1
            if sign == False:
                continue   

            pos = i # 当前所处的列   # 注意上面已改变pos
            # 判断当前列的右45°是否有皇后
            for row in range(len(self.path)-1, -1, -1): 
                if pos+1==self.path[row]:
                    sign = False
                    break
                else:
                    pos += 1                    
            if sign == False:
                continue
                        
            self.path.append(i)
            self.backtracking(n)
            self.path.pop()

代码bug:对是否在同一斜线的判断,输出answer的表示

52. N皇后 II

在这里插入图片描述

与51不同的是不要求返回排列,只要求返回满足要求的解决方案的数量,与上题基本误差,answer是数目(甚至这道题更简单)

37. 解数独(待做,hot100无)


其他

79. 单词搜索

在这里插入图片描述

class Solution:
    def __init__(self):
        self.res = False

    def exist(self, board: List[List[str]], word: str) -> bool:
        m = len(board)
        n = len(board[0]) 
        matrix = [[0]*n for _ in range(m)]       
        for i in range(m):
            for j in range(n):
                self.backtracking(board, word, 0, i, j, matrix)  # 以board[i][j]为起点
        return self.res

    def backtracking(self, board, word, index, i, j, matrix):        
        if word[index] != board[i][j]:  # board[i][j]和word[index]不匹配
            return
        if index == len(word)-1:  # 最后一个word也已匹配
            self.res = True
            return
        
        matrix[i][j] = 1  # 使用过的位置的matrix设为1
        if i-1>=0 and matrix[i-1][j] != 1:  # 上面节点未超过边界且未被加入path
            self.backtracking(board, word, index+1, i-1, j, matrix)
            if self.res == True:  # 可不加
                return 
        if j+1<len(board[0]) and matrix[i][j+1] != 1:  # 右面节点未超过边界且未被加入path
            self.backtracking(board, word, index+1, i,j+1, matrix)
            if self.res == True:
                return
        if i+1<len(board) and matrix[i+1][j] != 1:  # 下面节点未超过边界且未被加入path
            self.backtracking(board, word, index+1, i+1, j, matrix)
            if self.res == True:
                return
        if j-1>=0 and matrix[i][j-1] != 1:  # 左面节点未超过边界且未被加入path          
            self.backtracking(board, word, index+1, i,j-1, matrix)
            if self.res == True:
                return
        matrix[i][j] = 0  # 回溯

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值