摘要
本文主要介绍了LeetCode回溯算法的几个题目,其中包括332.重新安排行程、51. N皇后以及37. 解数独,并在最后对回溯算法进行了总结。
1、332.重新安排行程 *
1.1 思路
- 你首先对输入的
tickets列表按目的地进行排序,以便按字典序访问目的地城市。 - 你使用一个
boolean数组used来跟踪哪些飞行路线已经使用过。 - 你初始化一个路径
path,并将起始城市 "JFK" 添加到路径中,表示从 "JFK" 出发。 - 你使用递归的方式实现深度优先搜索(DFS)。在每个递归步骤中,你检查路径的长度是否等于
tickets的总数加1。如果是,说明已经找到一条满足条件的路径,将其添加到结果中,并返回true。 - 然后,你遍历
tickets列表,查找下一个目的地城市与路径中的最后一个城市匹配的飞行路线。 - 如果找到了匹配的飞行路线并且该路线未被使用过,你将目的地城市添加到路径中,将该飞行路线标记为已使用。
- 然后,你递归调用
backTracking函数,继续搜索下一个城市。 - 如果递归搜索成功,返回
true,否则,回溯,将已使用的飞行路线标记为未使用,并从路径中删除目的地城市。 - 最终,当递归搜索结束时,你得到一条满足条件的路径,它包含了所有的城市。
1.2 代码
private LinkedList<String> res;
public List<String> findItinerary(List<List<String>> tickets) {
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
boolean[] used = new boolean[tickets.size()];
LinkedList<String> path = new LinkedList<>();
path.add("JFK");
backTracking(path, (ArrayList) tickets, used);
return res;
}
public boolean backTracking(LinkedList<String> path, List<List<String>> tickets, boolean[] used) {
if (path.size() == tickets.size() + 1) {
res = new LinkedList(path);
return true;
}
for (int i = 0; i < tickets.size(); i++) {
if(used[i]) {
continue;
}
if(!tickets.get(i).get(0).equals(path.getLast())) {
continue;
}
path.add(tickets.get(i).get(1));
used[i] = true;
if (backTracking(path, tickets, used)) {
return true;
}
used[i] = false;
path.removeLast();
}
return false;
}
2、51. N皇后
2.1 思路
- 棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了
2.2 代码
public List<List<String>> solveNQueens(int n) {
char[][] arr = new char[n][n];
for (char[] c : arr) {
Arrays.fill(c, '.');
}
List<List<String>> list = new ArrayList<>();
doSolveNQueens(list, arr, n, 0);
return list;
}
public void doSolveNQueens(List<List<String>> list, char[][] arr, int n, int row) {
if(row == n) {
list.add(arrayToList(arr));
return;
}
for(int i=0; i<arr.length; i++) {
if(!isValid(arr, n, row, i)) {
continue;
}
arr[row][i] = 'Q';
doSolveNQueens(list, arr, n, row+1);
arr[row][i] = '.';
}
}
public boolean isValid(char[][] arr, int n, int row, int col) {
// 检查列
for (int i=0; i<row; ++i) { // 相当于剪枝
if (arr[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if (arr[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (arr[i][j] == 'Q') {
return false;
}
}
return true;
}
public List<String> arrayToList(char[][] arr) {
List<String> list = new ArrayList<>();
for (char[] c : arr) {
list.add(String.copyValueOf(c));
}
return list;
}
3、37. 解数独
3.1 思路
- 一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
3.2 代码
public void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private boolean solveSudokuHelper(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)){
continue;
}
board[i][j] = k;
if (solveSudokuHelper(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;
}
4、总结
1、回溯算法的核心思想
回溯算法的核心思想是通过递归方式探索问题的解空间,根据问题的性质,生成候选解并检查它们是否满足问题的约束。如果不满足,就回溯到前一步,选择下一个候选解,直到找到所有可能的解或确定问题无解。
2、回溯的基本流程
回溯算法通常遵循以下基本流程:
- 选择路径:从决策树的根节点开始,选择一个路径以生成候选解。
- 前进:根据选择,前进到决策树的下一个节点。这表示继续生成候选解的一部分。
- 约束检查:在前进之前,检查所选择的路径是否满足问题的约束。如果不满足,回溯到前一步。
- 解的处理:在达到问题的叶子节点时,处理生成的候选解。这可以是将其存储、输出或进行其他操作。
- 回退:如果处理完候选解后,回溯到上一步,选择下一个候选解并重复前面的步骤。
3、组合问题
组合问题涉及从给定元素集合中选择若干元素,以构成满足一定条件的组合。
- 组合 (77) :从1到n的数字中选择k个数的所有组合。
- 组合总和 III (216) :在1到9的数字中,找到所有和为n的k个数的组合。
4、字母组合问题
字母组合问题涉及将字母映射到数字的电话键盘上,以生成各种可能的字母组合。
- 电话号码的字母组合 (17) :给定数字字符串,生成所有可能的字母组合。
5、排列问题
排列问题涉及元素的不同排列,通常需要遍历所有可能的排列。
- 全排列 (46) :给定不同的数字,返回它们的所有排列。
- 全排列 II (47) :给定包含重复数字的数组,返回它们的唯一排列。
6、子集问题
子集问题涉及在给定元素集合上生成所有可能的子集,包括空集和全集。
- 子集 (78) :给定一个包含不同整数的数组,返回该数组的所有可能子集。
- 子集 II (90) :给定包含重复元素的数组,返回该数组的唯一子集。
7、组合总和问题
组合总和问题涉及从给定元素集合中选择若干元素,以满足特定总和的条件。
- 组合总和 (216) :从1到9的数字中,找到所有和为n的k个数的组合。
8、IP地址问题
IP地址问题涉及将数字字符串分割成有效的IP地址。
- 复原IP地址 (93) :给定一个包含数字的字符串,复原它成为IP地址的形式。
9、旅行问题
旅行问题涉及在给定的行程表中找到符合要求的旅行路线。
- 重新安排行程 (332) :给定一组机票,按照字典序重新安排行程,从"JFK"机场出发。
10、数独问题
数独问题涉及解决数独游戏中的填数问题,要求每一行、每一列和每个九宫格都包含1到9的数字。
- 解数独 (37) :解决数独游戏,填写缺失的数字。
11、N皇后问题
N皇后问题涉及在N×N的棋盘上放置N个皇后,使得它们互不攻击。
- N皇后 (51) :在N×N的棋盘上放置N个皇后,使得它们互不攻击。