回溯

子集型回溯

在一次 dfs函数里面,如果 path 是 mutable 类型, 一定保证path进入函数和出函数时 状态一定不变。不管有多少个分支,每个分支都要考虑这个问题。

如果 path是immutable,如string,它作为参数传递给dfs,这种情况下根本没有在修改它的状态,而是每次新建一个string,传给子节点。因而不需要恢复状态。LC 257.

模版一:选或不选

从输入的角度思考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution: #LC78题子集
    def subsets(self, nums: List[int]) -> List[List[int]]:
        path=[]
        res=[]
        def dfs(i):
            if i==len(nums):
                res.append(path.copy())
                return
            #选择这个
            path.append(nums[i])
            dfs(i+1)
            #不选这个
            path.pop()
            dfs(i+1)
        dfs(0)
        return res

path.copy()的时间复杂度为O(N), 总体时间复杂度(N*2^N).

求子集的这种做法,一定等到i=n时,遍历完所有元素才添加答案。

LC 90

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        n=len(nums)
        res=[]
        path=[]
        nums.sort()
        def dfs(i):
            if i==n:
                res.append(path.copy())
                return
            #选
            path.append(nums[i])
            dfs(i+1)
            path.pop()
            #不选
            if len(path)==0 or (len(path)>0 and path[-1]!=nums[i]):
                dfs(i+1)
        dfs(0)
        return res

Alt text 以 111 为例,检查当前 path,只有 path 中最后一个元素与当前元素不相等时,才可以不选当前元素。换句话说,如果两个相同的数字挨在一起,并且选了第一个数字,那就也必须选第二个数字。

因为会有00,01,10,11四种可能,表示选或者不选相邻的两个相同元素(比如 2),这样操作可以去掉第三种情况,最后得到的结果是[],[2],[22].

LC 491🌟

输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        n=len(nums)
        res=[]
        path=[]
        def dfs(i, cur):
            if i==n:
                if len(path)>=2:
                    res.append(path.copy())
                return
            #选
            if nums[i]>=cur:
                path.append(nums[i])
                dfs(i+1, nums[i])
                path.pop()
            #不选
            if len(path)==0 or (len(path)>0 and cur!=nums[i]):
                dfs(i+1, cur)
        dfs(0, -101)
        return res

与90题的不同在于,相同的数字不一定相邻,比如1,10,1,1。这种情况就无法用模版二。

模版二:从答案的角度思考

考虑当前问题和子问题,每个节点都是一个答案。 Alt text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res=[]
        path=[]
        def dfs(i):
            res.append(path.copy())
            if i==len(nums):
                return
            for j in range (i, len(nums)):
                #随便找一个数加进去
                path.append(nums[j])
                #必须得从它后面再找数加进去
                dfs(j+1)
                path.pop()
        dfs(0)
        return res

LC 90 Subset II

[1,2,2]找子集,但是[1,2]只能出现一次。 使用模板二,树的某一层中,不能有重复的节点。模版2️⃣的某一层实际上就是 for 循环,只需要在 for 循环里面检查当前选的元素被选了没有。(画图理解)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        n=len(nums)
        path=[]
        res=[]
        def dfs(i):
            res.append(path.copy())
            if i==n:
                return
            for j in range(i, n):
                if j>i and nums[j]==nums[j-1]:
                    continue
                path.append(nums[j])
                dfs(j+1)
                path.pop()
        dfs(0)
        return res

LC 39 组合总和

输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]]

注意递归终点一定需要加上target<0, 否则无限循环。 用模版一亦可。

类似题目:377. 组合总和 Ⅳ. 这题的不同是[2,2,3]和[2,3,2]可以作为两个答案

二叉树回溯

二叉树回溯与普通回溯的区别在于,不能在边界条件把答案加入res. 因为一个leaf,对它的两个子节点判断root==null, 则会把答案加进 res两次。一般在判断是叶子节点之后加入 res. LC 113

二叉树回溯最好把path作为参数传递。

LC 257 二叉树的所有路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#2025-02-04
class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        path=''
        res=[]
        def dfs(root,path):
            if not root:
                return
            path+=str(root.val)
            #当前节点如果不是leaf, 加入->
            if root.left!=root.right:
                path+="->" # 这里实际上是新建一个path
            #如果是 leaf,加入答案
            else:
                res.append(path)
            dfs(root.left, path)
            dfs(root.right, path)
        dfs(root,path)
        return res

path是个immutable,作为参数在传递. 不需要考虑状态恢复。

LC 437

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
    #cnt数组记录当前节点到根节点的所有路径和,以及它们的个数
    cnt=defaultdict(int)
    cnt[0]=1 #preSum[0]=0
    res=0
    def dfs(root, s):
        nonlocal res
        if not root:
            return
        s+=root.val
        res+=cnt[s-targetSum]
        cnt[s]+=1
        dfs(root.left, s)
        dfs(root.right, s)
        cnt[s]-=1
    dfs(root, 0)
    return res

组合型回溯

与子集型回溯的不同是,path是定长的。

模版一

递归树是一颗二叉树,每个分叉是选/不选

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution: #LC 22
    def generateParenthesis(self, n: int) -> List[str]:
        # 2n个位置里面,选n个位置放左括号。组合型回溯,模版一
        path=[]
        res=[]
        m=2*n
        # open用来记录左括号的数量。只要open<n, 就可以选左括号
        # 右括号的数量为i-open. 只有i-open<open, 才能选右括号
        def dfs(i, open):
            if i==m:
                res.append(''.join(path))
                return
            #选左括号
            if open<n:
                path.append('(')
                dfs(i+1, open+1)
                path.pop()
            #选右括号
            if i-open<open:
                path.append(')')
                dfs(i+1, open)
                path.pop()
        dfs(0, 0)
        return res

模板二

与子集型回溯的模版二类似,答案是选出递归树的某一层。递归树是一颗多叉树,每个分支代表选从起点 i开始的一个数字。 剪枝: 当前 path还需要m个数,但是剩余可选的数字数量<m Alt text

时间复杂度:叶子数量✖️根到叶子的长度,也就是C(n,k)*k 空间复杂度:O(k)

LC 216 Path Sum III

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution:#模版二
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        path=[]
        res=[]
        def dfs(i, target):
            #剪枝1: 还需要m个,剩余可选的的不够 m
            m=k-len(path)
            if m>10-i:
                return
            #剪枝2: 现在的路径和已经超过了 target
            if target<0:
                return
            #剪枝3: 即使全部选最大的数,还是不够 target
            #从 9开始选m个最大的数
            if (9+9-m+1)*m<target*2:
                return
            if len(path)==k:
                if target==0:
                    res.append(path.copy())
                return
            for j in range(i, 10):
                path.append(j)
                dfs(j+1, target-j)
                path.pop()
            
        dfs(1, n)
        return res

排列型回溯

LC 47

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        path=[]
        res=[]
        n=len(nums)
        used=[False]*n
        def dfs():
            if len(path)==n:
                res.append(path.copy())
                return
            for i, x in enumerate(nums):
                if not used[i]:
                    if i>0 and x==nums[i-1] and not used[i-1]:
                        print(x)#如果轮到x'进,但是x没用过,输出
                        continue
                    used[i]=True
                    path.append(x)
                    dfs()
                    used[i]=False
                    path.pop()
        nums.sort()
        dfs()
        return res

全排列需要一个数组来记录某个元素是否在 path上被用过。 Alt text 以[1,1’,2]为例,在for循环上遍历到1’,但是发现1还没有用过时,直接退出递归,就是我画圈的两个地方。最后从控制台输出也可以看出,输出了两遍 1.

LC 51. N皇后

维护一个数组,记录每一行皇后的index. 再用一个set记录当前可以用的列数。每次递归,mark一下用过的index(列号)。

两个皇后在同一斜线上:他们的行列之和/之差相等。