前言
今天是学习回溯算法的第五天,我们继续来一起学习回溯算法蕴含的逻辑处理,希望博主记录的内容能够对大家有所帮助 ,一起加油吧朋友们!💪💪💪
重新安排行程
LeetCode题目链接
这道题给一个航线列表,其中每个航线相当于一张机票从这个地点到那个地点,现在就是机票的起始机场JFK确定了,要你在所有行程中选出机场名的字典排序最小的行程(每个机票用且只能用一次)🤔

我们来思路梳理
- 问题给出的航线可以看作图的边,机场是图的顶点。我们需要从 “JFK” 出发,找出一条遍历所有航线的路径,并确保路径是按字典序最小的🤔🤔🤔 将航线列表视作有向图的边,构建邻接表表示图,且每个节点的相邻节点按字典序排序,方便后续按顺序遍历🤔从 “JFK” 开始进行深度优先搜索,优先访问字典序最小的机场。每次访问过一条航线后,将其移除,确保每条航线只用一次🤔 DFS 完成后,将访问过的节点逆序记录,因为我们在回溯过程中先完成的路径会被最后添加🤔
我们进一步来梳理回溯三要素
1 2 3 4 5 6 7 8
|
Map<String, PriorityQueue<String>> graph = new HashMap<>(); List<String> itinerary = new LinkedList<>(); private void backtracking(String airport){}
|
- 单层搜索逻辑(在每层搜索时,从当前机场出发,优先按字典序遍历所有可能的目的地。DFS会优先递归到字典序较小的路径,保证路径的最小字典序🤔 在递归访问过某个目的地后,立即将该航线从图中移除,确保不会重复使用🤔)
1 2 3 4 5
| while (graph.containsKey(airport) && !graph.get(airport).isEmpty()) { String nextAirport = graph.get(airport).poll(); backtracking(nextAirport); } itinerary.add(airport);
|
完整的回溯代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution { Map<String, PriorityQueue<String>> graph = new HashMap<>(); List<String> itinerary = new LinkedList<>(); public List<String> findItinerary(List<List<String>> tickets) { for(List<String> ticket : tickets){ String from = ticket.get(0); String to = ticket.get(1); graph.putIfAbsent(from, new PriorityQueue<>()); graph.get(from).offer(to); }
backtracking("JFK");
Collections.reverse(itinerary); return itinerary; }
private void backtracking(String airport){ while(graph.containsKey(airport) && !graph.get(airport).isEmpty()){ String nextAirport = graph.get(airport).poll(); backtracking(nextAirport); } itinerary.add(airport); } }
|
- 难以理解的部分
- 首先这个优先队列的使用就是说一个机场当它有多个可达地我们把这多个可达地的字符串按字典顺序排列,优先队列会自动保证字典顺序最小的机场在前,那我们递归遍历图路径的时候就是优先得到字典顺序最小的路径🤔
- 这里的回溯部分不是很明显,这里回溯的体现是当我们递归进入一个机场时,我们poll出字典序最小的目的地,然后目的地就被移除了因为我们要保证每条航线只使用一次。这其实是回溯的一个特性:逐步探索,且不走回头路🤔当我们完成某一条路径的探索后,我们并不会马上返回之前的状态,而是通过递归继续探索其他分支。当所有递归结束后,才会回溯🤔
- 这里递归时直接poll不会影响其他递归分支的正确性。原因是:poll() 的操作是在当前递归层中发生的,它意味着我们在探索某条特定的路径,并移除该路径。当这条路径结束后,我们不会返回到之前的路径再进行探索,而是直接处理其他分支。这就是回溯的核心:当某条路径探索完成后,不会回到之前的状态🤔
N皇后
LeetCode题目链接
经典问题n皇后啊,这道题就是经典中的经典哈哈,这道题的问题就是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击

我们来思路梳理
- 我们通过递归在每一行放置皇后,确保在当前行放置皇后后,检查它是否与之前放置的皇后冲突(同一列或对角线)。如果不冲突,则继续递归放置下一行的皇后🤔如果在某一行找不到合适的位置,则进行回溯,撤销之前的操作,尝试其他可能的位置🤔当所有皇后都成功放置(即递归到最后一行时),将当前的棋盘方案加入结果集中🤔
我们来梳理回溯三要素
1 2 3 4 5 6 7 8
|
private void backtracking(int n, int row, Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2, char[][] board, List<List<String>> result){}
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| if(row == n){ result.add(constructBoard(board)); return; }
private List<String> constructBoard(char[][] board){ List<String> boardConfig = new ArrayList<>(); for(char[] row : board){ boardConfig.add(new String(row)); } return boardConfig; }
|
- 单层搜索过程(在当前行
row,依次尝试在每一列放置皇后🤔 每次放置时,需要检查该列以及两个对角线上是否有皇后🤔 如果安全,将该位置标记为已放置皇后,并递归放置下一行的皇后🤔当回溯时,撤销上一步的操作,继续尝试其他可能的放置🤔)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| for(int col = 0; col < n; col++){ if(columns.contains(col) || diagonals1.contains(row - col) || diagonals2.contains(row + col)){ continue; } board[row][col] = 'Q'; columns.add(col); diagonals1.add(row - col); diagonals2.add(row + col); backtracking(n, row + 1, columns, diagonals1, diagonals2, board, result); board[row][col] = '.'; columns.remove(col); diagonals1.remove(row - col); diagonals2.remove(row + col); }
|
回溯的完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| class Solution { public List<List<String>> solveNQueens(int n) { List<List<String>> result = new ArrayList<>(); Set<Integer> columns = new HashSet<>(); Set<Integer> diagonals1 = new HashSet<>(); Set<Integer> diagonals2 = new HashSet<>(); char[][] board = new char[n][n]; for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ board[i][j] = '.'; } } backtracking(n, 0, columns, diagonals1, diagonals2, board, result); return result; }
private void backtracking(int n, int row, Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2, char[][] board, List<List<String>> result){ if(row == n){ result.add(constructBoard(board)); }
for(int col = 0; col < n; col++){ if(columns.contains(col) || diagonals1.contains(row - col) || diagonals2.contains(row + col)){ continue; } board[row][col] = 'Q'; columns.add(col); diagonals1.add(row - col); diagonals2.add(row + col); backtracking(n, row + 1, columns, diagonals1, diagonals2, board, result); board[row][col] = '.'; columns.remove(col); diagonals1.remove(row - col); diagonals2.remove(row + col); } }
private List<String> constructBoard(char[][] board){ List<String> boardConfig = new ArrayList<>(); for(char[] row : board){ boardConfig.add(new String(row)); } return boardConfig; } }
|
- 难以理解的部分
- 这里的回溯函数的参数实际上是可以把棋盘board、列columns、两个对角线标记(diagonals1和diagonals2)以及结果集(result)作为全局变量🤔,不用太过纠结
- 对于右上到左下的对角线, 它们的行号与列号之和是相同的。 因此用
row + col 可以唯一标识棋盘上的这一对角线🤔同样对于左上到右下方向的对角线,它们的行号与列号之差是相同的。因此用 row - col 可以唯一标识棋盘上的主对角线🤔


解数独
LeetCode题目链接
就是给一个二维字符数组作为数独题,通过修改其中的’.’来填数独字,数独的解法规则
- 数字1-9在每行每列只能出现一次
- 数字1-9在每个3x3小方块里只能出现一次

我们来梳理思路
- 逐个遍历棋盘上的空格(用
'.' 表示),对每一个空格依次尝试填入数字 1-9🤔 对于每个填入的数字,需要检查该数字在当前行、列、以及 3x3 小方块内是否已经存在🤔 如果某个数字放置成功,递归继续处理下一个空格。如果某个数字导致后续位置无法继续合法填入数字,则回退到上一步,尝试其他数字🤔当所有空格都成功填入合法的数字时,数独解答完成
我们进一步来梳理回溯三要素
1 2 3
|
private boolean backtrack(char[][] board) {}
|
1 2 3 4 5 6 7
|
for(循环条件){ 填充处理 return false; } return true;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| for (char num = '1'; num <= '9'; num++) { if (isValid(board, row, col, num)) { board[row][col] = num; if (backtrack(board)) { return true; } board[row][col] = '.'; } } return false;
private boolean isValid(char[][] board, int row, int col, char num) { for (int i = 0; i < 9; i++) { if (board[row][i] == num) { return false; } } for (int i = 0; i < 9; i++) { if (board[i][col] == num) { return false; } } int startRow = (row / 3) * 3; int startCol = (col / 3) * 3; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[startRow + i][startCol + j] == num) { return false; } } } return true; }
|
完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class Solution { public void solveSudoku(char[][] board) { backtracking(board); }
private boolean backtracking(char[][] board){ for(int row = 0; row < 9; row++){ for(int col = 0; col < 9; col++){ if(board[row][col] != '.')continue;
for(char num = '1'; num <= '9'; num++){ if(isValid(board, row, col, num)){ board[row][col] = num; if(backtracking(board))return true; board[row][col]='.'; } } return false; } } return true; } private boolean isValid(char[][] board, int row, int col, char num){ for(int i = 0; i < 9; i++){ if(board[row][i] == num) return false; } for(int i = 0; i < 9; i++){ if(board[i][col] == num)return false; } int startRow = (row / 3) * 3; int startCol = (col / 3) * 3; for(int i = 0; i < 3; i++){ for(int j = 0; j < 3; j++){ if(board[startRow + i][startCol + j] == num)return false; } } return true; } }
|
总结
今天对回溯中经典的数独、N皇后以及一道行程安排题进行学习,这几道题都蛮硬的,大家可以多啃几遍,明天开始贪心算法的学习,大家一起加油✊✊✊