Leetcode Day5 (回溯专题)

子集型回溯模版1

[1, 2] -> [ [], [1], [2], [1, 2] ]

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        ans = []
        
        def dfs(i, path):
            ans.append(path[:])
            for j in range(i, n):
                path.append(nums[j])
                dfs(j+1, path)
                path.pop()
        dfs(0, [])
        return ans

i为目前的位置, j为下一个考虑的位置. 这种情况就应该把每一步都加到ans里, 而不是等到i==n

假设存在dup, 则我们应该在横向去重(for loop里)

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()  # 首先对数组进行排序
        n = len(nums)
        ans = []
        
        def dfs(i, path):
            ans.append(path[:])
            for j in range(i, n):
                if j > i and nums[j] == nums[j-1]:
                    continue  # 跳过重复元素
                path.append(nums[j])
                dfs(j+1, path)
                path.pop()
        
        dfs(0, [])
        return ans

77 组合型回溯模版

n choose k

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        nums = list(range(1, n + 1))
        ans = []
        def dfs(i, path):
            if len(path) == k:
                ans.append(path[:])
                return
            for j in range(i, n):
                path.append(nums[j])
                dfs(j+1, path)
                path.pop()
        dfs(0, [])
        return ans

只能说思路和子集一模一样, 加入ans的条件变了而已

排列(permutation)型

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        ans = []
        n = len(nums)

        def dfs(path):
            if len(path) == n:
                ans.append(path[:])
                return
            for i in range(n):
                if nums[i] in path:
                    continue
                path.append(nums[i])
                dfs(path)
                path.pop()
        dfs([])
        return ans

有重复元素的permutation

同样也是横向去重, 所以我们在for loop前创建一个set来表示已经被用过的元素

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        ans = []
        n = len(nums)
        nums.sort()  # 排序以便于处理重复元素

        def dfs(path, indices):
            if len(path) == n:
                ans.append(path[:])
                return 
            
            used = set()  # 用于跟踪在当前位置使用过的元素
            for index in list(indices):  # 转换为列表以便在循环中修改
                if nums[index] in used:
                    continue  # 跳过在当前位置已经使用过的重复元素
                
                used.add(nums[index])
                path.append(nums[index])
                new_indices = indices.copy()
                new_indices.remove(index)
                dfs(path, new_indices)
                path.pop()

        initial_indices = set(range(n))
        dfs([], initial_indices)
        return ans

力扣知识点: 看是横向遍历还是纵向遍历

好题: 77, 39, 78, 46

i+1和start + 1的根本区别: 考不考虑当前元素重复加进去, 可以看40和78的区别

python知识点

result.append(path[:])和 result.append(path[:])
区别是前者拷贝, 后者引用

能解决的问题

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

通用模版

void backtracking(当前参数已经选了哪些) {
    if (终止条件) {
        存放结果;
        return;
    }

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

77 组合 (经典中的经典)

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        result = []  # 存放结果集
        self.backtracking(n, k, 1, [], result)
        return result
    def backtracking(self, n, k, startIndex, path, result):
        if len(path) == k:
            # 注意这个地方不能path, 得path[:], 相当于copy了一次
            result.append(path[:])
            return
        for i in range(startIndex, n + 1):  # 需要优化的地方
            path.append(i)  # 处理节点
            self.backtracking(n, k, i + 1, path, result)
            path.pop()  # 回溯,撤销处理的节点

在这里插入图片描述

22 有效的括号组合

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        ans = []

        def dfs(l, r, path):
            if l == r:
                if len(path) == 2*n:
                    ans.append("".join(path))
                    return
            if r > l or l > n or r > n:
                return
            # add ')'
            path.append(")")
            dfs(l, r+1, path)
            path.pop()

            # add (
            path.append("(")
            dfs(l + 1, r, path)
            path.pop()
        dfs(0,0,[])
        return ans

216 组合总和III

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

只使用数字1到9
每个数字 最多使用一次

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        res = []
        def back(cur_ans, cur_num):
            if len(cur_ans) == k and sum(cur_ans) == n:
                res.append(cur_ans[:])
                return
            if len(cur_ans) >= k or cur_num > 9:
                return
            for i in range(cur_num, 10):
                cur_ans.append(i)
                back(cur_ans, i + 1)
                cur_ans.pop()
        back([], 1)
        return res

唯一的不同就是放进去res的条件不同, 注意是两个哦

17 电话号码的字母组合

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if digits == "":
            return []
        res = []

        def back(cur_answer, cur_index):
            if cur_index == len(digits):
                res.append(cur_answer)
                return
            for letter in self.letterMap[int(digits[cur_index])]:
                cur_answer = cur_answer + letter
                back(cur_answer, cur_index + 1)

        back("", 0)
        return res

一定一定注意不能向上面这样写, 因为状态持续性: 当您这样修改cur_answer时,这个变化在递归调用返回后仍然存在。这意味着在循环的下一次迭代中,您会从一个包含了上一次迭代字母的cur_answer开始。所以我们只能创建新变量

如果不创建新的话, 共用一个cur的话, 那么撤销这一步是必须的

39 组合总和

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

关键的不同之处就在于可以选择重复的数字了, 直觉上最明显的改变应该在从哪里开始遍历这个问题上

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        def back(cur):
            if (sum(cur) > target):
                return
            if (sum(cur) == target):
                res.append(cur[:])
            for i in range(len(candidates)):
                cur.append(candidates[i])
                back(cur)
                cur.pop()
        back([])
        return res

这样的话给出的结果是:
[[2,2,3],[2,3,2],[3,2,2],[7]]
问题就出在出现了重复项, 原因是考虑3的时候就不应该考虑2了. 证明我们的back停止条件还不完善
我比较鸡贼, 把前面改为了
加了一个条件:
if len(cur) >= 2 and cur[-1] < cur[-2]: return
如果倒数第一比倒数第二还要小, 那么证明我们往回看了

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        n = len(candidates)
        candidates.sort()
        res, temp = [], []
        def dfs(i, cur_sum):
            if cur_sum > target:
                return
            elif cur_sum == target:
                res.append(temp[:])
            else:
                for j in range(i, n):
                    temp.append(candidates[j])
                    dfs(j, cur_sum + candidates[j])
                    temp.pop()
        dfs(0, 0)
        return res

学习一下标答
关键的一步就是在back中引入i来表明目前考虑了第几个index,
for j in range(i, n):这一步就是从每个index开始尝试, 且index之前的东西都不考虑了

40 组合总和II, 上一题但无法选重复的, 每个元素只能用一次

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        def dfs(cur_ans, cur_index):
            if sum(cur_ans) == target:
                res.append(cur_ans[:])
                return
            if sum(cur_ans) > target:
                return
            for i in range(cur_index, len(candidates)):
                cur_ans.append(candidates[i])
                dfs(cur_ans, cur_index+1)
                cur_ans.pop()
        dfs([], 0)

        return res

这样做会出现[1,1,1,1,1]的情况

精髓在于:回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。

在这里插入图片描述
所以关键就是如何对同一层的数据进行去重

class Solution:

    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def dfs(begin, path, residue):
            if residue == 0:
                res.append(path[:])
                return

            for index in range(begin, size):
                if candidates[index] > residue:
                    break

                if index > begin and candidates[index - 1] == candidates[index]:
                    continue

                path.append(candidates[index])
                dfs(index + 1, path, residue - candidates[index])
                path.pop()

        size = len(candidates)
        if size == 0:
            return []

        candidates.sort()
        res = []
        dfs(0, [], target)
        return res

妙啊!因为题目要求每个元素只使用一次 当需要{1, 2, 2}的时候, 第二个2的 下标一定是 cur == begin; 当出现了一个{1,2,5}的时候,进行到第二次{1,2,5}到{1,2}的时候,第二个未完成的{1,2,5}的2的下标一定是cur>begin的,直接跳过就不会产生相同的{1,2,5}了,同理如果有第3个2也可以跳过!!!这个剪枝真的牛扳plus。。。兄弟作揖敬谢!

78 求所有子集

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        def dfs(cur, start):
            res.append(cur[:])
            for i in range(start, len(nums)):
                cur.append(nums[i])
                dfs(cur, i+1)
                cur.pop()
        dfs([], 0)
        return res

还是那句话, 注意dfs那一步是从i+1还是start+1开始

131 求所有回文子串

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        ans = []
        n = len(s)

        def dfs(i, path):
            if i == n:
                ans.append(path[:])
            for j in range(i, n):
                substring = s[i:j+1]
                if substring == substring[::-1]:
                    path.append(substring)
                    dfs(j+1, path)
                    path.pop()
        dfs(0, []) 
        return ans

90 求所有子集, 但原集合会有重复元素

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        res = []
        def dfs(cur, start):
            res.append(cur[:])
            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i-1]:
                    continue
                cur.append(nums[i])
                dfs(cur, i + 1)
                cur.pop()


        dfs([], 0)
        return res

舒服了直接秒杀, 就是上两道题的结合体

class Solution:
    def subsetsWithDup(self, nums):
        result = []
        path = []
        nums.sort()  # 去重需要排序
        self.backtracking(nums, 0, path, result)
        return result

    def backtracking(self, nums, startIndex, path, result):
        result.append(path[:])  # 收集子集
        uset = set()
        for i in range(startIndex, len(nums)):
            if nums[i] in uset:
                continue
            uset.add(nums[i])
            path.append(nums[i])
            self.backtracking(nums, i + 1, path, result)
            path.pop()

这种用集合思想的方法也可以好好学习

记住for loop里的东西是同一层的, 只有递归那一步(dfs)会增加深度

这个地方你可以看到uset的创立后接的是for loop, 而这个set就是服务于这一层的, 所以最后不用返回之前的状态, 也不可以!!!

为什么不需要移除:
当我们从递归调用返回时,当前的 uset 就会被丢弃。
下一次循环会使用同一个 uset,但这正是我们想要的行为,因为我们想在同一层避免重复。

46 全排列

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        def dfs(cur, indices_left):
            if len(cur) == len(nums):
                res.append(cur[:])
                return
            for i in indices_left:
                cur.append(nums[i])
                indices_left.remove(i)
                dfs(cur, indices_left)
                cur.pop()
                indices_left.append(i)
        dfs([], list(range(len(nums))))
        return res

我一开始的代码是这样的, 忘记了一个关键问题, indices_left对于每一个cur是独一无二的, 所以我们每一次都得复制一遍new_indices_left = indices_left[:] new_indices_left.remove(i)

47 全排列II

全排列1但是有重复

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        nums.sort()  # Sort to group duplicates together
        
        def dfs(cur, indices_left):
            if len(cur) == len(nums):
                res.append(cur[:])
                return
            
            prev = None
            for i in range(len(indices_left)):
                if nums[indices_left[i]] == prev:
                    continue
                
                new_indices = indices_left[:]
                new_indices.remove(indices_left[i])
                cur.append(nums[indices_left[i]])
                dfs(cur, new_indices)
                cur.pop()
                
                prev = nums[indices_left[i]]
        
        dfs([], list(range(len(nums))))
        return res

这里给我们揭示了另外一种同层去重的方法, keep track of
prev
在这里插入图片描述

79 单词搜索

在这里插入图片描述

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m = len(board)
        n = len(board[0])
        visited = [[False for _ in range(n)] for _ in range(m)]

        def dfs(i, j, k):
            if k == len(word):
                return True
            if i < 0 or j < 0 or i >= m or j >= n or visited[i][j] or board[i][j] != word[k]:
                return False
            
            visited[i][j] = True
            
            res = (dfs(i-1, j, k+1) or
                   dfs(i+1, j, k+1) or
                   dfs(i, j-1, k+1) or
                   dfs(i, j+1, k+1))
            
            visited[i][j] = False
            
            return res

        for i in range(m):
            for j in range(n):
                if dfs(i, j, 0):
                    return True
        
        return False

51 N皇后

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        vertical = set()
        diag1 = set()
        diag2 = set()
        res = []
        def dfs(cur_row, path):
            if len(path) == n:
                res.append(path[:])
                return
            for pos in range(n):
                if pos in vertical:
                    continue
                if pos + cur_row in diag1 or pos - cur_row in diag2:
                    continue
                vertical.add(pos)
                diag1.add(pos + cur_row)
                diag2.add(pos - cur_row)
                path.append(pos)
                dfs(cur_row + 1, path)
                path.pop()
                vertical.remove(pos)
                diag1.remove(pos + cur_row)
                diag2.remove(pos - cur_row)
        dfs(0, [])
        ans = []
        for l in res:
            temp = [["." for _ in range(n)] for _ in range(n)]
            for i in range(n):
                temp[i][l[i]] = 'Q'
                temp[i] = "".join(temp[i])
            ans.append(temp)
        return ans

虽然写的有点丑, 但是能过就行了

37 解数独

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        self.solve(board)
    
    def solve(self, board):
        for i in range(9):
            for j in range(9):
                if board[i][j] == '.':
                    for num in '123456789':
                        if self.is_valid(board, i, j, num):
                            board[i][j] = num
                            if self.solve(board):
                                return True
                            board[i][j] = '.'
                    return False
        return True
    
    def is_valid(self, board, row, col, num):
        # Check row
        for x in range(9):
            if board[row][x] == num:
                return False
        
        # Check column
        for x in range(9):
            if board[x][col] == num:
                return False
        
        # Check 3x3 box
        start_row = (row // 3) * 3
        start_col = (col // 3) * 3
        for i in range(3):
            for j in range(3):
                if board[i + start_row][j + start_col] == num:
                    return False
        
        return True
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值