Leetcode刷题笔记30:回溯6(回溯Hard题-n皇后-数独-行程安排)

123 阅读5分钟

导语

leetcode刷题笔记记录,本篇博客是回溯部分的最后一期,主要记录几个回溯部分的Hard难度的题目,包括:

Leetcode 332 重新安排行程

题目描述

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

示例 1:

输入: tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出: ["JFK","MUC","LHR","SFO","SJC"]

示例 2:

输入: tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出: ["JFK","ATL","JFK","SFO","ATL","SFO"]
解释: 另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。

提示:

  • 1 <= tickets.length <= 300
  • tickets[i].length == 2
  • fromi.length == 3
  • toi.length == 3
  • fromi 和 toi 由大写英文字母组成
  • fromi != toi

解法

这是一个基于图的问题,因此需要创建一个图来表示每个机场和它们的所有可能的目的机场。然后使用回溯法进行深度优先搜索,寻找一个可行的路径。

主要的一些关键细节如下:

  1. 创建图:首先需要知道每个机场可以飞往哪些机场。因此,使用字典来创建一个图,其中键是出发机场,值是它可以到达的机场列表。这个列表按照字母排序,以便我们总是首先考虑字典顺序更小的机场。
  2. 回溯法:从JFK开始,尝试选择一个目的地。选择完一个目的地后,将其从图中的列表中移除,以表示这张机票已经被使用。然后,基于这个选择的目的地继续搜索下一步的选择。
  3. 目标与撤销:如果当前路径长度达到了机票总数 + 1,说明使用了所有的机票并找到了一个有效的行程。否则,我们需要撤销选择并尝试其他目的地。
  4. 结束搜索:一旦找到一个有效的行程,立即结束搜索。这是因为已经按照字典顺序排序了目的地,所以首先找到的行程一定是最小的。

这个题目还有一点别扭之处在于它是找到一个立马结束递归,所以像之前二叉树那边的题目一样,需要一个返回值来承接这一点。返回值在这里是为了方便我们“提前退出”。当我们在某个深度的递归中找到了合法行程,通过返回True来告诉上层的递归:“我已经找到了一个答案,你不需要继续尝试其他选择了。”这样,上层的递归也会返回True,一直向上,直到最外层的递归。如果不使用返回值,即使找到了一个答案,仍然会尝试所有的可能性,这就导致了不必要的计算和延迟。

具体代码如下:

from typing import List
from collections import defaultdict

class Solution:
    def __init__(self):
        # 初始化行程结果和当前路径(始终从JFK开始)
        self.result = []
        self.path = ["JFK"]

    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        # 创建图:这里我们使用字典表示图,key为出发机场,value为可能到达的机场列表
        graph = defaultdict(list)
        # 先对tickets进行排序,这样我们可以保证较小的目的机场先被考虑
        for depart, arrive in sorted(tickets):
            graph[depart].append(arrive)

        # 调用回溯函数,从JFK开始,总机票数+1是因为路径长度包括起点
        self.backtracking(graph, "JFK", len(tickets) + 1)
        return self.result

    def backtracking(self, graph, current_city, ticket_count):
        # 如果当前路径的长度满足机票数+1,那么找到了一个合适的行程
        if len(self.path) == ticket_count:
            self.result = self.path[:]
            return True
        
        # 如果当前城市没有在图中,说明没有下一个目的地可以去
        if current_city not in graph:
            return False

        # 遍历当前机场可以去的所有机场
        for i, next_city in enumerate(graph[current_city]):
            # 选择:选择一个目的地,更新路径,并从graph中移除这个目的地
            self.path.append(next_city)
            graph[current_city].pop(i)
            
            # 进行下一步搜索
            if self.backtracking(graph, next_city, ticket_count):
                return True
            
            # 撤销选择:如果当前选择的目的地没有得到有效的行程,我们撤销选择,恢复graph和路径
            self.path.pop()
            graph[current_city].insert(i, next_city)

        # 如果当前机场的所有可能目的地都没有得到有效的行程,返回False
        return False

Leetcode 51 N 皇后

题目描述

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 **n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例 1:

输入: n = 4
输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释: 如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入: n = 1
输出: [["Q"]]

 

提示:

  • 1 <= n <= 9

解法

这道题目的难点在于如何去回溯遍历一个二维的棋盘,因为之前我们做过的组合、子集、排列都是一维的序列。套用之前的回溯模板,我们可以让for循环代表棋盘的列,而每层回溯则进入下一层。当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回。

image.png

n皇后的回溯部分代码并不难,主要是判断放置位置是否有效比较繁琐,造成心理上以为这道题目很难,实际上,抽离出这部分后,代码还是比较简单。完整代码如下:

class Solution:
    def __init__(self):
        self.result = []     # 三维数组

    def is_valid(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  # 当前位置合法

    def solveNQueens(self, n: int) -> List[List[str]]:
        chessboard = [["."] * n  for i in range(n)]
        self.back_tracking(chessboard, n, 0)

        return self.result

    def back_tracking(self, chessboard, n, row):
        # chessboard为一个二维数组,表示棋盘
        # n代表棋盘大小
        # row代表当前遍历到了棋盘的哪一层
        if row == n:
            # 下面的一行代码是错误的,应该需要深拷贝
            # self.result.append(chessboard[:])
            self.result.append(["".join(row) for row in chessboard])
            return
        for col in range(0, n):
            if self.is_valid(row, col, chessboard):
                chessboard[row][col] = 'Q'
                self.back_tracking(chessboard, n, row+1)
                chessboard[row][col] = "."

Leetcode 37 解数独

题目描述

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

输入: 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"]]
解释: 输入的数独如上图所示,唯一有效的解决方案如下所示:

提示:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

解法

使用回溯法解决,还是使用两层循环来遍历整个矩阵,完整代码如下:

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        self.back_tracking(board)

    def back_tracking(self, board) -> bool:
        """
        使用回溯法解决数独
        """
        for row in range(len(board)):
            for col in range(len(board[0])):
                # 当找到一个空格时,尝试填入1-9的数字
                if board[row][col] == ".":
                    for num in range(1, 10):  # 注意这里应该是从1到9
                        if self.is_valid(row, col, num, board):
                            board[row][col] = str(num)  # 转换为字符串填充
                            if self.back_tracking(board):
                                return True  # 找到一个有效解决方案
                            board[row][col] = '.'  # 回溯
                    return False  # 如果1-9都试过了,还是不行,就返回False
        return True  # 当所有位置都被填满

    def is_valid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool:
        """
        检查在特定位置放一个特定的数字是否是有效的
        """
        # 判断同一行是否冲突
        for i in range(9):
            if board[row][i] == str(val):
                return False
        # 判断同一列是否冲突
        for j in range(9):
            if board[j][col] == str(val):
                return False
        # 判断同一九宫格是否有冲突
        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
        return True