332.重新安排行程(可跳过)
这道题目有几个难点:
- 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
- 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
- 使用回溯法的话,那么终止条件是什么呢?
- 搜索的过程中,如何遍历一个机场所对应的所有机场。
3.问题解答
3.1 如何理解死循环
对于死循环,我来举一个有重复机场的例子:
为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环。
3.2 该记录映射关系
字母序靠前排在前面,代码如下:
//按字母序排序
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
用一个used数组代表哪条航线是否使用过,代码如下:
//used记录哪条航线是否使用过,true为使用过,false为未使用
boolean[] used = new boolean[tickets.size()];
4.代码实现
1.确定递归函数的参数和返回值:
递归函数定义:
参数:tickets,表示有多少个航班(终止条件会用上)。
used数组(记录哪条航班使用过)。
返回值:boolean
说明:因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线。如下图所示:
代码如下:
//332.重新安排行程
List
LinkedList
private boolean backTracking(ArrayList> tickets,
boolean[] used) {}
2.确定终止条件:
拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。
所以终止条件是:我们回溯遍历的过程中,遇到的机场(path)个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。
//递归终止条件:遇到的机场(path)个数,如果达到了(航班数量+1),
//那么我们就找到了一个行程,把所有航班串在一起了。
if (path.size() == tickets.size() + 1) {
ans = new LinkedList(path);
return true;
}
3.确定单层递归的逻辑:
当前航线没有使用过,并且当前航线的起始机场等于path的最后一个机场,说明存在一条航线可以从path的尾部指向该航线。
举例:假设path里只有"JFK",当前航线["JFK", "MUC"],这时就有JFK->MUC。
import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; public class ReconstructItinerary { Listans; LinkedList path = new LinkedList<>(); public List findItinerary(List > tickets) { //按字母序排序 Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1))); //从JFK出发 path.add("JFK"); //used记录哪条航线是否使用过,true为使用过,false为未使用 boolean[] used = new boolean[tickets.size()]; backTracking((ArrayList) tickets, used); return ans; } //注意函数返回值我用的是bool! //我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢? //因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线。 private boolean backTracking(ArrayList
> tickets, boolean[] used){ //递归终止条件:遇到的机场(temp)个数,如果达到了(航班-数量+1), //那么我们就找到了一个行程,把所有航班串在一起了。 if(path.size() == tickets.size() + 1){ ans = new LinkedList<>(path); return true; } for (int i = 0; i < tickets.size(); i++){ //当前航线没有使用过,并且当前航线的起始机场等于path的最后一个机场, //说明存在一条航线可以从path的尾部指向该航线。 if (!used[i] && tickets.get(i).get(0) .equals( path.get( path.size() - 1))) { path.add(tickets.get(i).get(1)); used[i] = true;//当前航线已使用 if(backTracking(tickets, used)){ return true; } //回溯 used[i] = false; path.removeLast(); } } return false; } }
第51题. N皇后
import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class NQueens { List> res = new ArrayList<>(); public List
> solveNQueens(int n) { //字符二维数组:棋盘n*n char[][] chessBoard = new char[n][n]; for(char[] chars : chessBoard){ Arrays.fill(chars, '.');//向数组中填充元素 } backTracking(chessBoard, n, 0); return res; } /** * @param chessBoard * @param n 为输入的棋盘大小 * @param row 是当前递归到棋盘的第几行了 */ public void backTracking(char[][] chessBoard, int n, int row){ //当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。 if(row == n){ res.add(ArrayToList(chessBoard)); return; } for(int col = 0; col < n; col++){ if(isValid(row, col, n, chessBoard)){// 验证合法就可以放 chessBoard[row][col] = 'Q';// 放置皇后 backTracking(chessBoard, n, row + 1); chessBoard[row][col] = '.';// 回溯,撤销皇后 } } } //将字符棋盘转为题目需要的List
private List ArrayToList(char[][] chessBoard){ ArrayList list = new ArrayList<>(); for(char[] c : chessBoard){ list.add(String.copyValueOf(c)); } return list; } //检测棋盘是否合法 public boolean isValid(int row, int col, int n, char[][] chessBoard){ // 检查col列 for(int i = 0; i < row; i++){ if(chessBoard[i][col] == 'Q'){ return false; } } // 检查 45度角是否有皇后 for(int i = row -1, j = col - 1; i>= 0 && j >= 0; i--, j--){ if(chessBoard[i][j] == 'Q'){ return false; } } // 检查 135度角是否有皇后 for(int i = row - 1, j = col + 1; i >=0 && j < n; i--, j++){ if(chessBoard[i][j] == 'Q'){ return false; } } return true; } }
37. 解数独
public class SudokuSolver { public void solveSudoku(char[][] board) { backTracking(board); } private boolean backTracking(char[][] board){ //「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列, // 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」 for(int i = 0; i < 9; i++){// 遍历行 for (int j = 0; j < 9; j++){ // 遍历列 if (board[i][j] != '.'){// 跳过原始数字 continue; } for(char k = '1'; k <= '9'; k++){// (i, j) 这个位置放k是否合适 if(isValidSudoku(i, j, k, board)){ board[i][j] = k;// 放置k if (backTracking(board)){ return true;// 如果找到合适一组立刻返回 } board[i][j] = '.';// 回溯 } } // 9个数都试完了,都不行,那么就返回false return false; // 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! // 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」 } } // 遍历完没有返回false,说明找到了合适棋盘位置了 return true; } /** * 判断棋盘是否合法有如下三个维度: * 同行是否重复 * 同列是否重复 * 9宫格里是否重复 */ private boolean isValidSudoku(int row, int col, char val, char[][] board){ // 同行是否重复 for (int i = 0; i< 9; i++){ if(board[row][i] == val){ return false; } } // 同列是否重复 for (int j =0; j < 9; j++){ if(board[j][col] == val){ return false; } } // 9宫格里是否重复 int startRow = (row / 3) * 3; int startCol = (col / 3) * 3; for(int i = startRow; i< startRow + 3; i++){ for (int j = startCol; j < startCol + 3; j++){ if(board[i][j] == val){ return false; } } } return true; } }