代码随想录训练营 Day25打卡 回溯算法part04 491. 递增子序列 46. 全排列 47. 全排列 II 51. N皇后 37. 解数独

代码随想录训练营 Day25打卡 回溯算法part04

一、 力扣491. 递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例
输入: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]]

本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。
用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:

在这里插入图片描述

实现思路

  1. 初始化:

    result:用于存储所有符合条件的子序列的结果列表。
    path:用于存储当前探索的路径(子序列)。
    调用 backtracking 方法,开始回溯算法。

  2. 回溯函数 backtracking:

    如果当前路径的长度大于 1,则将当前路径的副本添加到结果列表 result 中。
    使用集合 uset 来记录本层已经使用过的元素,避免在同一层中出现重复的元素。
    循环遍历数组 nums,从 startIndex 开始,以避免重复子序列。
    检查当前元素是否小于路径中的最后一个元素,或是否在本层中已经使用过,如果是,则跳过该元素。
    对于每个元素:
        将当前元素添加到路径 path 中。
        标记当前元素为已使用 uset.add(nums[i])。
        递归调用 backtracking,探索以当前元素为起点的所有子序列。
        回溯操作,取消当前元素的使用标记,并从路径中移除最后一个元素 path.pop()。

代码实现

class Solution:
    def findSubsequences(self, nums):
        result = []  # 用于存放所有符合条件的子序列的结果列表
        path = []    # 当前探索的路径(子序列)
        self.backtracking(nums, 0, path, result)  # 调用回溯函数
        return result  # 返回最终的结果列表
    
    def backtracking(self, nums, startIndex, path, result):
        if len(path) > 1:  # 当前路径的长度大于1,表示至少有两个元素,符合子序列条件
            result.append(path[:])  # 使用切片将当前路径的副本加入结果集
            # 注意这里不要加 return,因为我们还需要继续递归,取树上的节点
        
        uset = set()  # 使用集合对本层元素进行去重
        for i in range(startIndex, len(nums)):
            # 如果当前路径不为空且当前元素小于路径中的最后一个元素,或者当前元素在本层已经使用过,则跳过
            if (path and nums[i] < path[-1]) or nums[i] in uset:
                continue
            
            uset.add(nums[i])  # 记录这个元素在本层用过了,本层后面不能再用了
            path.append(nums[i])  # 将当前元素添加到路径中
            # 递归调用,探索以当前元素为起点的所有子序列
            self.backtracking(nums, i + 1, path, result)
            path.pop()  # 回溯,移除路径中的最后一个元素

# 示例
sol = Solution()
print(sol.findSubsequences([4, 6, 7, 7]))  # 示例输出

力扣题目链接
题目文章讲解
题目视频讲解

二、力扣46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

本题以[1,2,3]为例,抽象成树形结构如下:
在这里插入图片描述

实现思路

首先,初始化结果列表 result 和用于标记元素是否被使用的数组 used,并调用回溯函数 backtracking 开始递归。
回溯函数 backtracking 的基本操作是检查当前路径的长度是否等于输入数组的长度,如果是,则将当前路径添加到结果列表中,表示找到一个完整的排列。
然后,遍历数组 nums 中的每个元素,对于每个未被使用的元素,将其标记为已使用并添加到路径中,递归调用 backtracking 继续构建排列。
在递归调用结束后,回溯操作会取消当前元素的使用标记,并从路径中移除该元素,从而探索其他可能的排列组合。这个过程反复进行,直到遍历完所有可能的排列。

代码实现

class Solution:
    def permute(self, nums):
        result = []  # 用于存放所有排列的结果列表
        self.backtracking(nums, [], [False] * len(nums), result)  # 调用回溯函数
        return result  # 返回最终的结果列表

    def backtracking(self, nums, path, used, result):
        if len(path) == len(nums):  # 当路径长度等于输入数组长度时,表示找到一个完整的排列
            result.append(path[:])  # 使用切片将当前路径的副本加入结果集
            return  # 结束当前递归
        for i in range(len(nums)):
            if used[i]:  # 如果当前元素已经被使用过,则跳过
                continue
            used[i] = True  # 标记当前元素为已使用
            path.append(nums[i])  # 将当前元素添加到路径中
            # 递归调用,继续探索以当前元素为起点的所有排列
            self.backtracking(nums, path, used, result)
            path.pop()  # 回溯,移除路径中的最后一个元素
            used[i] = False  # 回溯,取消当前元素的使用标记

# 示例
sol = Solution()
print(sol.permute([1, 2, 3]))  # 示例输出

力扣题目链接
题目文章讲解
题目视频讲解

三、力扣47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例
输入:nums = [1,1,2]
输出
[[1,1,2],
[1,2,1],
[2,1,1]]

我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:
在这里插入图片描述
图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。

一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。

实现思路

该算法通过回溯法生成所有可能的排列,且排列中不允许有重复的组合。首先,对输入数组 nums 进行排序,以便于在回溯过程中去重。初始化结果列表 result 和用于标记元素是否被使用的数组 used,并调用回溯函数 backtracking 开始递归。

在回溯函数 backtracking 中,首先检查当前路径的长度是否等于输入数组的长度,如果是,则将当前路径的副本添加到结果列表中,表示找到一个完整的排列。
然后,遍历数组 nums 中的每个元素,对于每个未被使用且不与前一个相同(即不形成重复排列)的元素,将其标记为已使用并添加到路径中,递归调用 backtracking 继续构建排列。
在递归调用结束后,回溯操作会取消当前元素的使用标记,并从路径中移除该元素,从而探索其他可能的排列组合。这个过程反复进行,直到遍历完所有可能的排列。

代码实现

class Solution:
    def permuteUnique(self, nums):
        nums.sort()  # 排序以便去重
        result = []  # 存放所有唯一排列结果的列表
        self.backtracking(nums, [], [False] * len(nums), result)  # 开始回溯
        return result  # 返回所有唯一排列结果

    def backtracking(self, nums, path, used, result):
        if len(path) == len(nums):  # 如果路径长度等于输入数组长度,找到一个排列
            result.append(path[:])  # 将当前路径的副本添加到结果列表
            return
        for i in range(len(nums)):
            # 跳过重复的元素,避免形成重复排列
            if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]) or used[i]:
                continue
            used[i] = True  # 标记当前元素为已使用
            path.append(nums[i])  # 将当前元素添加到路径中
            # 递归调用,继续探索以当前元素为起点的所有排列
            self.backtracking(nums, path, used, result)
            path.pop()  # 回溯,移除路径中的最后一个元素
            used[i] = False  # 回溯,取消当前元素的使用标记

# 示例
sol = Solution()
print(sol.permuteUnique([1, 1, 2]))  # 示例输出

力扣题目链接
题目文章讲解
题目视频讲解

四、力扣51. N皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例
在这里插入图片描述
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
在这里插入图片描述
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了

实现思路

  1. 初始化和设置:

    solveNQueens 方法初始化棋盘,使用字符串列表表示每一行,初始值为点(‘.’),表示空位置。
    然后从第一行(索引0)开始调用 backtracking 函数。

  2. 回溯过程:

    backtracking 方法尝试在当前行的每一列放置皇后。
    对于每一列,使用 isValid 方法检查在该位置放置皇后是否合法。
    如果合法,则更新棋盘,并递归调用 backtracking 方法处理下一行。
    在尝试所有可能性后,进行回溯,移除之前放置的皇后,尝试下一列。

  3. 合法性检查:

    isValid 方法确保在 (row, col) 位置放置皇后不会与之前放置的皇后冲突。
    它检查当前列和两个对角线(45度和135度)上是否有皇后,以确保没有皇后威胁当前位置。

  4. 返回结果:

    当所有行都成功放置皇后且无冲突时,将当前棋盘配置添加到结果列表中。
    最后,solveNQueens 方法将每个解格式化并返回。

代码实现

from typing import List

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        result = []  # 初始化一个列表用于存储最终结果,结果是二维字符串数组

        # 初始化棋盘,所有位置都用 '.' 表示为空
        chessboard = ['.' * n for _ in range(n)]
        
        # 从第一行开始进行回溯
        self.backtracking(n, 0, chessboard, result)
        
        # 将每个解转换为所需的字符串格式并返回结果
        return [[''.join(row) for row in solution] for solution in result]

    def backtracking(self, n: int, row: int, chessboard: List[str], result: List[List[str]]) -> None:
        if row == n:
            # 如果所有行都已经填满,将当前棋盘的副本添加到结果中
            result.append(chessboard[:])
            return

        for col in range(n):
            # 检查在 (row, col) 位置放置皇后是否有效
            if self.isValid(row, col, chessboard):
                # 在 (row, col) 位置放置皇后
                chessboard[row] = chessboard[row][:col] + 'Q' + chessboard[row][col+1:]
                
                # 递归调用回溯函数处理下一行
                self.backtracking(n, row + 1, chessboard, result)
                
                # 回溯:移除 (row, col) 位置的皇后
                chessboard[row] = chessboard[row][:col] + '.' + chessboard[row][col+1:]

    def isValid(self, row: int, col: int, chessboard: List[str]) -> bool:
        # 检查当前列是否有皇后
        for i in range(row):
            if chessboard[i][col] == 'Q':
                return False  # 当前列已经有皇后,不合法

        # 检查 45 度角(左上方向)是否有皇后
        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:
            if chessboard[i][j] == 'Q':
                return False  # 左上方向已经有皇后,不合法
            i -= 1
            j -= 1

        # 检查 135 度角(右上方向)是否有皇后
        i, j = row - 1, col + 1
        while i >= 0 and j < len(chessboard):
            if chessboard[i][j] == 'Q':
                return False  # 右上方向已经有皇后,不合法
            i -= 1
            j += 1

        # 当前位置合法
        return True

力扣题目链接
题目文章讲解
题目视频讲解

五、力扣37. 解数独

编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例
在这里插入图片描述
输入:board = [[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”],[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”],[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”],[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”],[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”],[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”],[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”],[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”],[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:[[“5”,“3”,“4”,“6”,“7”,“8”,“9”,“1”,“2”],[“6”,“7”,“2”,“1”,“9”,“5”,“3”,“4”,“8”],[“1”,“9”,“8”,“3”,“4”,“2”,“5”,“6”,“7”],[“8”,“5”,“9”,“7”,“6”,“1”,“4”,“2”,“3”],[“4”,“2”,“6”,“8”,“5”,“3”,“7”,“9”,“1”],[“7”,“1”,“3”,“9”,“2”,“4”,“8”,“5”,“6”],[“9”,“6”,“1”,“5”,“3”,“7”,“2”,“8”,“4”],[“2”,“8”,“7”,“4”,“1”,“9”,“6”,“3”,“5”],[“3”,“4”,“5”,“2”,“8”,“6”,“1”,“7”,“9”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
在这里插入图片描述
本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
因为这个树形结构太大了,我抽取一部分,如图所示:
在这里插入图片描述
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!

代码实现

from typing import List

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        不返回任何内容,而是修改输入的数独棋盘。
        """
        self.backtracking(board)

    def backtracking(self, board: List[List[str]]) -> bool:
        """
        回溯算法核心函数。如果找到解,返回 True;如果无解,返回 False。
        """
        # 遍历数独棋盘的每一行
        for i in range(len(board)):
            # 遍历数独棋盘的每一列
            for j in range(len(board[0])):
                # 如果当前位置已有数字,跳过
                if board[i][j] != '.': continue
                
                # 尝试将数字 1-9 填入当前空格
                for k in range(1, 10):
                    # 检查数字 k 放在 (i, j) 位置是否有效
                    if self.is_valid(i, j, k, board):
                        # 如果有效,放置数字 k
                        board[i][j] = str(k)
                        # 递归调用回溯函数,继续填下一个空格
                        if self.backtracking(board): return True
                        # 如果不能成功填完,回溯,撤销放置的数字
                        board[i][j] = '.'
                # 如果 1-9 都不能成功填入当前空格,返回 False,无解
                return False
        # 如果所有空格都成功填入数字,返回 True,有解
        return True

    def is_valid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool:
        """
        检查在 (row, col) 位置放置数字 val 是否有效。
        """
        # 检查当前行是否已有数字 val
        for i in range(9):
            if board[row][i] == str(val):
                return False
        # 检查当前列是否已有数字 val
        for j in range(9):
            if board[j][col] == str(val):
                return False
        # 检查 3x3 子宫格内是否已有数字 val
        start_row = (row // 3) * 3
        start_col = (col // 3) * 3
        for i in range(start_row, start_row + 3):
            for j in range(start_col, start_col + 3):
                if board[i][j] == str(val):
                    return False
        # 如果上述三种情况都没有冲突,返回 True,表示可以放置数字 val
        return True

力扣题目链接
题目文章讲解
题目视频讲解

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值