回溯算法学习笔记


回溯算法
组合问题:N个数里面按一定规则找出k个数的集合
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
组合、子集、排序问题的思想都是每次选一个数。

分割问题:一个字符串按一定规则有几种切割方式
分割问题的思想是每次选一个切割点切一刀。比如在s[0]后切一刀,分成s[0]和s[1:]

路径问题:矩阵中的路径

棋盘问题:N皇后,解数独等等

组合问题和分割问题都是收集树的叶子节点
子集问题是找树的所有节点
有的回溯只需记录最后的叶子结点[[1,2,3]],有的需要记录路径[[1],[1,2],[1,2,3]]。
回溯算法通用模板

def bactracking(参数cur):#每次递归代表树形结构的一个子节点 参数代表剩下的
	if 终止条件:
		存放结果
		return #一定要return          
	for (选择 本层集合中的元素):#树形结构的宽度 本层集合中的元素由上边的参数标记
	    处理结点            
		bactracking(参数new)
		#新的参数有可能跟上边的参数cur有关(例如电话号码的组合)
		#也有可能跟for的循环变量有关(大多数情况)
		回溯 撤销处理结果

组合问题(无序 顺序无关)

无重复元素时,元素是否可以重复使用

216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 VS 每个数字可以 多次使用
  • 返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
    在这里插入图片描述
    元素能不能重复使用,决定的参数是backtracking传入的参数。数字能不能重复使用,就看选了之后还能不能选,即这层递归选i以后,下次递归还从不从i开始选。从i开始:可以重复使用。从i+1开始:不可重复使用。所以说,决定的参数是backtracking传入的参数。
class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        if not candidates:return []
        candidates.sort()
        path,res=[],[]
        n=len(candidates)
        print(candidates)
        def backtracking(idx,t):
            if t==0:
                res.append(path[:])
                return
            for i in range(idx,n):
                if i>idx and candidates[i]==candidates[i-1]:continue
                if candidates[i]<=t:
                    path.append(candidates[i])
                    backtracking(i+1,t-candidates[i])#每个数字只使用一次
                    #backtracking(i,t-candidates[i])#每个数字可重复使用
                    path.pop()
                else:
                    break
        backtracking(0,target)
        return res

有重复元素时,解集不能包含重复结果

有重复元素时,每个元素只能用一次,并且解集不能包含重复结果
40. 组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

  • candidates 中的每个数字在每个组合中只能使用 一次 。
  • 注意:解集不能包含重复的组合。
    例如下面例子,有两个1元素,11但是结果中只能有一个[1,2,5],而不是[1,2,5]和[1,2,5]

同一树层去重,一定要先对数组排序!
同一树层去重有两个方法

  1. 每层设置一个flag数组,flag[i]表示 数值i 在这层用没用。需要知道i的取值开数组,flag数组的长度是数值i的取值范围,每次调用递归都会有一个新的flag,在递归内层定义
  2. 因为已经排序了,直接if i>idx and candidates[i]==candidates[i-1]:continue

方法2首选

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

在这里插入图片描述
可以看到,去重问题是横向判断问题,同一树层,选了1以后,循环的下一个如果还是1,就剪枝,避免出现两个[1,2,5]。并且纵向却可以保证出现[1,1,5]。
去重问题是横向判断的,所以是在for循环选择时判断。
if i>idx and candidates[i]==candidates[i-1]:continue

class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        if not candidates:return []
        candidates.sort()
        path,res=[],[]
        n=len(candidates)
        print(candidates)
        def backtracking(idx,t):
            if t==0:
                res.append(path[:])
                return
            for i in range(idx,n):
                if i>idx and candidates[i]==candidates[i-1]:continue
                #一定是i>idx
                if candidates[i]<=t:
                    path.append(candidates[i])
                    backtracking(i+1,t-candidates[i])
                    #元素不能重复使用
                    path.pop()
                else:
                    break
        backtracking(0,target)
        return res

用flag

class Solution(object):
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        if not candidates:return []
        path,res=[],[]
        n=len(candidates)
        candidates.sort()
        def backtracking(idx,t):
            if t==0:
                res.append(path[:])
                return
            flag=[False]*51
            for i in range(idx,n):
                if flag[candidates[i]]:continue
                if candidates[i]<=t:
                    path.append(candidates[i])
                    flag[candidates[i]]=True
                    backtracking(i+1,t-candidates[i])
                    path.pop()
        backtracking(0,target)
        return res

子集问题(要去重 无顺序)

子集问题需要记录路径同样也需要去重
关于子集的去重问题一定要排序
去重方式与上文一样
90. 子集 II VS 491. 递增子序列

90. 子集 II

90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
其中:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10


子集问题:先排序
去重:树的横向要去重,纵向可以有重复。两种去重方式。
1、因为排完序了,可以直接用当前与前一个比较,相等就不要了。
2、每次递归使用一个usage_list ,记录当前值有没有被用过,同上边组合问题flag的定义一样!!!
解法一

class Solution(object):
    def subsetsWithDup(self, nums):
        if not nums:return [[]]
        nums.sort()
        self.res=[[]]
        self.path=[]
        self.n=len(nums)
        def backtracking(sid):
            if sid==self.n:
                return
            for i in range(sid,self.n):
                if i>sid and nums[i]==nums[i-1] :continue
                #一定是i>sid
                self.path.append(nums[i])
                self.res.append(self.path[:])
                #进入下一层递归,可以取取过的值,所以设置为没用过False
                backtracking(i+1)
                #下层递归完成,进入横向同层下一个树枝,不能重复设置为使用过True
                self.path.pop()
        backtracking(0)
        return self.res

解法二

class Solution(object):
    def subsetsWithDup(self, nums):
        if not nums:return [[]]
        nums.sort()
        self.res=[[]]
        self.path=[]
        self.n=len(nums)
        def backtracking(sid):
            if sid==self.n:               
                return
            usage_list = [False] * 21#数值范围-10到10
            for i in range(sid,self.n):             
                if usage_list[nums[i]+10] == True:
                    continue
                self.path.append(nums[i])
                self.res.append(self.path[:])
                backtracking(i+1)
                usage_list[nums[i]+10] =True#在backtracking前后都行,是为了在横向遍历i时,记录已经使用过了。而每次纵向都会产生一个新的usage_list 数组
                self.path.pop()
        backtracking(0)
        return self.res

解法三
used数组加排序 不看也罢

class Solution(object):
    def subsetsWithDup(self, nums):
        if not nums:return [[]]
        nums.sort()
        self.res=[[]]
        self.path=[]
        self.n=len(nums)
        self.use=[False]*self.n
        def backtracking(sid):
            if sid==self.n:
                return
            for i in range(sid,self.n):
                if i>0 and nums[i]==nums[i-1] and self.use[i-1] :continue
                self.path.append(nums[i])
                self.res.append(self.path[:])
                self.use[i]=False
                #进入下一层递归,可以取取过的值,所以设置为没用过False
                backtracking(i+1)
                self.use[i]=True
                #下层递归完成,进入横向同层下一个树枝,不能重复设置为使用过True
                self.path.pop()
        backtracking(0)
        return self.res

491. 递增子序列

491. 递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
其中

  • 1 <= nums.length <= 15
  • -100 <= nums[i] <= 100

与上边问题不同的是本问题不能排序,但是要去重,不排序去重导致的结果会出现[1,4]和[4,1],所以会导致结果重复,但是本问题要找的是递增子序列,递增条件会自动去掉[4,1],所以不排序的情况下也可以使用一个树根的同层不能重复使用相同的
因为没有排序,所以不能用if i>idx and candidates[i]==candidates[i-1]:continue,只能用flag数组,同样flag[i]表示该层的数值i有没有被用过

class Solution(object):
    def findSubsequences(self, nums):
        self.res=[]
        self.path=[]
        self.n=len(nums)
        def backtracking(sid):
            if sid==self.n:return
            usage_list = [False] * 201#范围-100-100
            for i in range(sid,self.n):
                if usage_list[nums[i]+100] == True:
                    continue
                if not self.path or self.path[-1]<=nums[i]:
                    self.path.append(nums[i])
                    if len(self.path)>1:
                        self.res.append(self.path[:])
                    usage_list[nums[i]+100] = True
                    backtracking(i+1)
                    self.path.pop()
        backtracking(0)
        return self.res

最长递增子序列

300. 最长递增子序列
虽然看起来和上边差不多,但是不能使用回溯的方法,首先数值很大,flag数组开销大
其次,没让输出最长递增子序列,只让输出长度

全排列问题(有顺序)

与组合和子集不同的是,排列问题中[1,2]和[2,1]是不同的
首先看一个简单的例子,无重复数据的情况下

46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
在这里插入图片描述

class Solution(object):
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        path,res=[],[]
        n=len(nums)
        flag=[False]*n
        def backtracking():
            if len(path)==n:
                res.append(path[:])
                return
            for i in range(n):
                if flag[i]:continue
                path.append(nums[i])
                flag[i]=True
                backtracking()
                path.pop()
                flag[i]=False
        backtracking()
        return res

此时的flag是在递归外层,所有递归都用同一个flag,并且flag[i]表示,nums[i]有没有被选走,与组合和子集的flag数组含义完全不同!为了区别,分别记为flago(outside)和flagi,并且在同一树层用的flagi,如果nums排序了,可以用nums[i]==nums[i-1]代替(目前不能被代替的情况就是上边的递增子序列)。

再看有重复元素,需要去重的排列
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
需要做两件事,深度标记以及同层去重
if i>0 and nums[i]==nums[i-1] and not flago[i-1]:continue

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        nums.sort()
        n=len(nums)
        flago=[False]*n
        res,path=[],[]
        def backtracking():
            if len(path)==n:
                res.append(path[:])
                return
            for i in range(n):
                if flago[i]:continue
                if i>0 and nums[i]==nums[i-1] and not flago[i-1]:
                    continue
                flago[i]=True
                path.append(nums[i])
                backtracking()
                path.pop()
                flago[i]=False
        backtracking()
        return res

用两个flag:

class Solution(object):
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        nums.sort()
        n=len(nums)
        flago=[False]*n
        res,path=[],[]
        def backtracking():
            if len(path)==n:
                res.append(path[:])
                return
            flagi=[False]*21
            for i in range(n):
                if flago[i]:#该元素用过了
                    continue
                if flagi[nums[i]]:#该值用过了
                    continue
                flago[i]=True
                flagi[nums[i]]=True
                path.append(nums[i])
                backtracking()
                path.pop()
                flago[i]=False
        backtracking()
        return res

分割问题

回文串

131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
在这里插入图片描述

class Solution(object):
    def partition(self, s):
        """
        :type s: str
        :rtype: List[List[str]]
        """
        def is_palindrome(s):
            n=len(s)
            for i in range(n//2):
                if s[i]!=s[n-i-1]:return False
            return True
        def jude(ss):
            i,j=0,len(ss)-1
            while i<j:
                if ss[i]!=ss[j]:
                    return False
                i+=1
                j-=1
            return True
        res=[]
        path=[]
        n=len(s)
        def backtracking(idx):
            if idx==n:
                res.append(path[:])
            for i in range(idx,n):
                if jude(s[idx:i+1]):
                    path.append(s[idx:i+1])
                    backtracking(i+1)
                    path.pop()
        backtracking(0)
        return res

ip地址

93. 复原 IP 地址
有效 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 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
在这里插入图片描述

class Solution(object):
    def restoreIpAddresses(self, s):
        """
        :type s: str
        :rtype: List[str]
        """
        #相当于分割 割三刀
        res,self.path=[],''
        n=len(s)
        def valid(s):
            if len(s)>1 and s[0]=='0':return False
            if int(s)>255 : return False
            return True
        def backtracking(idx,t):#从这里开始切割
            if idx==n:
                if t==0:
                    res.append(self.path[:-1])
                return
            if t==0 or n-idx>3*t:
                return 
            for i in range(idx,min(idx+3,n)):
                if valid(s[idx:i+1]):#最后一刀以s[i]结尾
                    tmp=self.path
                    self.path+=s[idx:i+1]
                    self.path+='.'
                    backtracking(i+1,t-1)
                    self.path=tmp
        backtracking(0,4)
        return res 
                

电话号码的字母组合

电话号码的字母组合
以上每次递归的for循环的选择 都来自于同一个空间Ω
在这里插入图片描述

class Solution(object):
    def letterCombinations(self, digits):
        """
        :type digits: str
        :rtype: List[str]
        """
        if digits=="":return []
        self.path=''
        res=[]
        n=len(digits)
        dic=["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
        def backtracking(si):
            if si==n:
                res.append(self.path)
                return
            for i in range(len(dic[int(digits[si])])):
                self.path+= dic[int(digits[si])][i]
                backtracking(si+1)
                self.path=self.path[:-1]
        backtracking(0)
        return res

重新安排行程

332. 重新安排行程
1、避免死循环
2、字母续
3、遍历一个机场所能到的所有机场
4、找到第一个最小序就结束 所以函数需要返回值来判断找没找到
出发机场和到达机场是会重复的
在遍历 unordered_map<出发机场, map<到达机场, 航班次数>> targets的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。
如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。

路径问题

剑指 Offer 12. 矩阵中的路径

class Solution(object):
    def exist(self, board, word):
        """
        :type board: List[List[str]]
        :type word: str
        :rtype: bool
        """
        directions=[(0,1),(0,-1),(1,0),(-1,0)]#上下左右四个方向
        def dfs(i,j,k):
             if board[i][j]!=word[k]:return False
             if k==len(word)-1:return True
             board[i][j]=''
             for di,dj in directions:
                 newi,newj=i+di,j+dj
                 if 0<=newi<len(board) and 0<=newj<len(board[0]):
                     if dfs(newi,newj,k+1):
                         return True    
             board[i][j]=word[k]
             return False
        for i in range(len(board)):
            for j in range(len(board[0])):
                if dfs(i,j,0):return True
        return False

N皇后

因为要求所有解
所以找到一个符合的后,不要return,还要接着找

数独

如果是找到一个解就可以 递归一定要有返回值 if backtracking(): return True
如果要得到所有符合条件的解 不用有返回值
填充矩阵方式,直接两个for循环 例如37.解数独

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值