代码随想录算法训练营第三十天 | 332. 重新安排行程、51. N 皇后、37. 解数独、回溯总结

134 阅读5分钟

332. 重新安排行程

代码随想录文章讲解

Greedy + 回溯

  • 通过回溯来打破死循环
  • 通过贪心算法选择最小的lexical order(如果能找到完整的itinerary,那么一定是lexical order最小的)
class Solution:
    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        res = ['JFK']
        from_to = defaultdict(list)
        for ticket in tickets:
            from_to[ticket[0]].append(ticket[1])
        
        def _backtracking(departure):
            # itinerary is compelete
            if len(res) == len(tickets) + 1:
                return True
            
            # sort by ascending lexical order
            from_to[departure].sort()
            for _ in from_to[departure]:
                destination = from_to[departure].pop(0)
                res.append(destination)
                # if find a compelete itinerary, we do not search further
                if _backtracking(destination):
                    return True
                res.pop()
                from_to[departure].append(destination)
        
        _backtracking('JFK')
        return res

51. N 皇后

代码随想录文章讲解

  • The queen (♕, ♛) is the most powerful piece in the game of chess, able to move any number of squares vertically, horizontally or diagonally, combining the power of the rook and bishop.
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        if not n: 
            return []
        # n * n board
        board = [['.'] * n for _ in range(n)]
        res = []
        
        # cannot in the same row, in the same column, and at the diagonal
        # 因为是从上往下搜索,所以只需要判断左上角和右上角是否冲突
        def is_valid(board, row, col):
            #判断同一列是否冲突
            for i in range(n):
                if board[i][col] == 'Q':
                    return False
            # 判断左上角是否冲突
            i = row -1
            j = col -1
            while i>=0 and j>=0:
                if board[i][j] == 'Q':
                    return False
                i -= 1
                j -= 1
            # 判断右上角是否冲突
            i = row - 1
            j = col + 1
            while i>=0 and j < len(board):
                if board[i][j] == 'Q':
                    return False
                i -= 1
                j += 1
            return True
        
        def backtracking(board, row, n):
            # 如果走到最后一行,说明已经找到一个解
            if row == n:
                temp_res = []
                for temp in board:
                    temp_str = "".join(temp)
                    temp_res.append(temp_str)
                res.append(temp_res)
                
            for col in range(n):
                # 如果在(row,col)处冲突,则剪枝
                if not is_valid(board, row, col):
                    continue
                board[row][col] = 'Q'
                # 搜索下一层
                backtracking(board, row+1, n)
                # 回溯
                board[row][col] = '.'
                
        backtracking(board, 0, n)
        return res

37. 解数独

代码随想录文章讲解

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        def is_valid(board, num, row, col):
            # row constraint
            if num in board[row]:
                return False
            
            # column constraint
            for i in range(9):
                if num == board[i][col]:
                    return False
            
            # grid constraint
            start_row = 3 * (row // 3)
            start_col = 3 * (col // 3)
            for i in range(start_row, start_row + 3):
                for j in range(start_col, start_col + 3):
                    if board[i][j] == num:
                        return False
            
            return True
        
        def backtracking(board):
            for i in range(9): # 遍历行
                for j in range(9):  # 遍历列
                    # 若空格内已有数字,跳过
                    if board[i][j] != '.': 
                        continue
                    # 尝试填入 1-9
                    for num in range(1, 10):
                        if not is_valid(board, str(num), i, j):
                            continue
                        board[i][j] = str(num)
                        if backtracking(board):
                            return True
                        board[i][j] = '.'
                    return False
            return True # 有解
        
        backtracking(board)

回溯总结

回溯法理论基础

  • 回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。

  • 回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。

  • 回溯算法能解决如下问题:

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

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

组合问题

  • for循环横向遍历,递归纵向遍历,回溯不断调整结果集
  • 收集叶子节点的结果
  • 剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了

切割问题

  • 切割问题其实类似组合问题
  • 如何模拟那些切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 收集叶子节点的结果

子集问题

  • 在树形结构中子集问题是要收集所有节点的结果

    result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉结果
    if (startIndex >= nums.size()) { // 终止条件可以不加
        return;
    }
    

排列问题

  • 每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了

性能分析

子集问题分析:

  • 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

排列问题分析:

  • 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 nn1n2.....1=n!n * n-1 * n-2 * ..... 1 = n!
  • 空间复杂度:O(n),和子集问题同理。

组合问题分析:

  • 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
  • 空间复杂度:O(n),和子集问题同理。

N皇后问题分析:

  • 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n(n1)....1n * (n-1) * .... * 1
  • 空间复杂度:O(n),和子集问题同理。

解数独问题分析:

  • 时间复杂度:O(9^m) , m是'.'的数目。
  • 空间复杂度:O(n^2),递归的深度是n^2

一般说到回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!