秋招算法备战第30天 | 332.重新安排行程、51. N皇后、37. 解数独、回溯总结

332. 重新安排行程 - 力扣(LeetCode)

Hierholzer算法

Hierholzer算法是一种用于在连通图中寻找欧拉路径或欧拉回路的算法。

欧拉路径是一条路径,它经过图中的每条边恰好一次。如果这样的路径存在,我们就称这个图是半欧拉图。欧拉回路是一条路径,它经过图中的每条边恰好一次并且起点和终点相同。如果这样的回路存在,我们就称这个图是欧拉图。

Hierholzer算法的步骤如下:

  1. 选择任意一个起点,然后沿着任意一条从该点出发的边行走,每次都尽可能选择未曾经过的边。

  2. 因为图中的每个顶点的度数都是偶数(对于欧拉回路)或者除了两个顶点以外的所有顶点的度数都是偶数(对于欧拉路径),所以我们总是可以回到起点。在这个过程中,我们经过的所有边就形成了一个回路。

  3. 如果我们还有未经过的边,我们就从上一步形成的回路中选择一个顶点,然后重复第1步和第2步,直到所有的边都被经过。我们在这个过程中得到的每一个回路都会连接到前一个回路,从而形成一个更长的回路。

  4. 最后,我们得到的这个回路就是欧拉路径或者欧拉回路。

这个算法的时间复杂度是O(E),其中E是图中的边的数量,因为我们需要经过每一条边恰好一次。

题解

这个问题可以用深度优先搜索 (DFS) 或者 Hierholzer 算法来解决,两种方法都需要建立一个图来描述从一个城市可以飞往其他城市的所有可能。为了满足字典序最小的要求,我们需要对每个出发城市的目的地城市列表按照字典序进行排序。

Hierholzer 算法适用于寻找欧拉路径,即通过图中所有的边恰好一次并且行遍所有的顶点的路径。这个问题是欧拉路径的一个实例,因为飞行的性质决定了我们需要经过所有的边恰好一次,这样就形成了一个欧拉路径。

下面是一个 Python 的解决方案:

from collections import defaultdict

def findItinerary(tickets):
    # 使用邻接表存储图
    graph = defaultdict(list)
    for a, b in sorted(tickets)[::-1]:
        graph[a].append(b)
    # DFS
    route = []
    def dfs(a):
        while graph[a]:
            dfs(graph[a].pop())
        route.append(a)
    dfs('JFK')
    return route[::-1]

这个代码首先对 tickets 进行排序,然后建立了一个图的邻接表。然后通过深度优先搜索,找出一个可以用完所有票的路径,最后将这个路径反转并返回。需要注意的是,因为字典排序是按照 ASCII 值从小到大排序,而栈是后进先出,所以我们需要将 tickets 反向排序,这样最小的字典序就会最后进栈,最先出栈。

51. N 皇后 - 力扣(LeetCode)

这个问题可以通过回溯算法解决。我们从第一行开始,逐行放置皇后,每放置一个皇后,就检查当前放置是否合法,也就是检查是否在同一列,同一对角线有其他皇后。如果当前放置合法,就继续放置下一行,如果放置到了最后一行,就说明找到了一个解,并将解加入解集中。如果当前放置不合法或者已经放置到了最后一行但还没有找到解,就回溯到上一行,移动皇后的位置。

由于皇后不能在同一行,我们实际上可以用一个一维数组来存放皇后的位置,数组的索引代表行号,数组的值代表列号。这样,我们可以用一个一维数组来表示棋盘,大大简化了问题。

下面是一个 Python 的解决方案:

def solveNQueens(n):
    # 定义一个函数,检查在棋盘的指定位置放置皇后是否合法
    def could_place(row, col):
        # 对于棋盘上已经放置的每一行
        for i in range(row):
            # 检查新放置的皇后是否在同一列,同一主对角线,同一副对角线上
            if board[i] == col or \
                board[i] - i == col - row or \
                board[i] + i == col + row:
                # 如果在同一列,同一主对角线,同一副对角线上,返回 False
                return False
        # 如果不在同一列,同一主对角线,同一副对角线上,返回 True
        return True

    # 定义一个函数,用来放置皇后
    def place_queen(row, n):
        # 如果已经放置到了最后一行,说明找到了一个解
        if row == n:
            # 将这个解加入解集
            result.append(board[:])
            return
        # 对于每一列
        for col in range(n):
            # 检查在这个位置放置皇后是否合法
            if could_place(row, col):
                # 如果合法,就放置皇后
                board[row] = col
                # 然后递归地放置下一行的皇后
                place_queen(row + 1, n)

    # 定义一个函数,用来将存储皇后位置的一维数组转换为表示棋盘的二维字符串数组
    def generate_board(board):
        res = []
        for i in board:
            # 对于每一行,将 'Q' 放置在对应的位置,其余位置放置 '.'
            res.append('.' * i + 'Q' + '.' * (n - i - 1))
        return res

    # 初始化解集
    result = []
    # 初始化棋盘
    board = [-1] * n
    # 从第一行开始放置皇后
    place_queen(0, n)
    # 将找到的所有解转换为表示棋盘的二维字符串数组
    return [generate_board(board) for board in result]

这个代码首先定义了一个 could_place 函数,用来检查在棋盘的指定位置放置皇后是否合法。然后定义了一个 place_queen 函数,用来放置皇后,并且如果放置皇后成功,就递归地放置下一行的皇后。如果放置皇后失败或者已经放置到了最后一行,就回溯到上一行,移动皇后的位置。然后定义了一个 generate_board 函数,用来将存储皇后位置的一维数组转换为表示棋盘的二维字符串数组。最后,主函数调用 place_queen 函数,放置皇后,并将找到的所有解通过 generate_board 函数转换为表示棋盘的二维字符串数组,然后返回这些解。

37. 解数独 - 力扣(LeetCode)

解决数独问题的一种常见的解决方案是使用回溯法。通过逐个尝试每个可能的数字,如果当前位置可以放置当前数字,则进入下一位置;如果不能放置,则进行回溯。

代码如下:

def solveSudoku(board):
    def is_valid(board, row, col, num):
        # 检查行是否有重复
        for x in range(9):
            if board[x][col] == num:
                return False
        # 检查列是否有重复
        for y in range(9):
            if board[row][y] == num:
                return False
        # 检查 3 x 3 区域是否有重复
        start_row, start_col = row // 3 * 3, 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] == num:
                    return False
        return True

    def backtrace(board, row, col):
        if col == 9:  # 若当前列超过最后一列,则移到下一行,列从0开始
            return backtrace(board, row + 1, 0)
        if row == 9:  # 若当前行超过最后一行,则找到了一个可行解
            return True
        if board[row][col] != '.':  # 若当前位置已有数,则直接进入下一列
            return backtrace(board, row, col + 1)
        
        for num in '123456789':  # 从1到9尝试
            if not is_valid(board, row, col, num):  # 若当前位置不能放置此数,则进入下一个数的尝试
                continue
            board[row][col] = num  # 若当前位置可以放置此数,则放置此数
            if backtrace(board, row, col + 1):  # 若从当前位置的下一列开始,能找到一个可行解,则返回True
                return True
            board[row][col] = '.'  # 若从当前位置的下一列开始,不能找到一个可行解,则撤销当前放置,进入下一个数的尝试
        return False  # 若1-9都试验完,都没有找到可行解,则返回False

    backtrace(board, 0, 0)

函数backtrace负责从指定的位置开始,寻找一个可行解。如果当前位置已有数字,则直接进入下一列;如果当前位置为空,则尝试放置1-9中的任一数,然后进入下一列。

函数is_valid负责检查在指定的位置放置指定的数字是否符合数独的规则。若当前行、当前列或当前的3 x 3区域中已有同样的数字,则返回False;否则返回True。

在主函数solveSudoku中,我们调用了backtrace,并从第一行第一列开始寻找解。如果能找到一个解,那么棋盘board就被正确填满了;否则,棋盘保持原样(题目保证了存在解,所以不需要考虑找不到解的情况)。

回溯总结篇

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

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

回溯法的模板:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

你可能感兴趣的:(算法,深度优先)