携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
一、前言
解决一个回溯问题:实际上就是一个 决策树 的遍历问题,即多叉树遍历问题。
主要思考 3 个问题:
- 路径:已经做出的选择
- 选择列表:当前可以做的选择
- 结束条件:到达决策树底层,无法再做选择的条件
回溯算法框架:
result = []; // 结果
void backtrack(路径, 选择列表) {
if (满足结束条件) {
result.add(路径);
return;
}
for (选择 : 选择列表) {
// 1. 做选择
// 2. 调用
backtrack(路径, 选择列表);
// 3. 撤销选择
}
}
二、排列组合问题
(1)全排列
题目
题目:给你一个整数数组,并且数组中没有重复元素,你要返回这个数组所有可能的排列。
# 比如说给你的数组是:
[0, 1, 2]
# 你要返回的所有排列是:
0, 1, 2
0, 2, 1
1, 0, 2
1, 2, 0
2, 0, 1
2, 1, 0
思路
思路:遍历一颗决策树。
- 路径:已经做出的选择
- 选择列表:当前可以做的选择
- 结束条件:到达决策树底层,无法再做选择的条件
AC
题解:
class Solution {
// Time: O(n*n!), Space: O(n)
public List<List<Integer>> permute(int[] nums) {
if (nums == null || nums.length == 0) return Collections.emptyList();
List<List<Integer>> result = new ArrayList<>();
List<Integer> list = new ArrayList<>();
for (int num: nums) list.add(num);
permuteRec(list, 0, result);
return result;
}
private void permuteRec(List<Integer> list, int start, List<List<Integer>> result) {
// 结束条件:最后节点了
if (start == list.size()) {
result.add(new ArrayList<>(list));
return;
}
// 选择列表
for (int i = start; i < list.size(); ++i) {
Collections.swap(list, i , start); // 选择
permuteRec(list, start + 1, result); // 调用
Collections.swap(list, start, i); // 撤销选择
}
}
}
(2)组合总数
题目
题目:给你一个正整数数组,数组中不包含重复元素,同时给你一个正整数目标值,你要找到数组中和为目标值的所有组合。
- 另外,数组中每个元素都可以使用无限多次,并且答案中不能包含重复组合。
# 比如说,给你的数组是:
[4, 2, 8]
# 给你的目标值是 6。数组中和为 6 的组合有:
[4, 2]
[2, 2, 2]
思路
同上,回溯问题。
AC
题解:
class Solution {
// Time: O(n ^ (target / min)), Space: O(target / min)
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0) return null;
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates);
combSum(candidates, target, 0, new ArrayList<>(), result);
return result;
}
private void combSum(int [] nums, int target, int start, List<Integer> elem,
List<List<Integer>> result) {
// 结束条件:最后节点了
if (target == 0) {
result.add(new ArrayList<>(elem));
return;
}
// 结束条件:超出目标值
if (target < 0) return;
for (int i = start; i < nums.length; ++i) {
if (nums[i] > target) break; // 剪枝叶:超出目标值
// 选择
elem.add(nums[i]);
// 调用
combSum(nums, target - nums[i], i, elem, result);
// 撤销选择
elem.remove(elem.size() - 1); // T: O(1)
}
}
}
(3)下一个排列
题目
题目:给你一个整数数组,每一个元素是一个 0 到 9 的整数,数组的排列形成了一个有效的数字。
- 你要找到数组的下一个排列,使它形成的数字是大于当前排列的第一个数字。
- 如果当前排列表示的已经是最大数字,则返回这个数组的最小排列。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
思路
思路:
- 从后往前找第一个数
p
:nums[p] < nums[p + 1]
- 从后往前找第一个数
i
:nums[i] > nums[p]
,再对nums[i]
与nums[j]
进行交换 - 对
[p + 1, 数组长度)
进行升序:这里只需要两两交换就能满足
举个栗子:[2, 1, 8, 4, 2, 1]
AC
题解:
class Solution {
// Time: O(n), Space: O(1)
public void nextPermutation(int[] nums) {
if (nums == null || nums.length < 2) return;
int n = nums.length;
int p = n - 2;
// 1. 从后往前找第一个数 p:nums[p] < nums[p + 1]
while (p >= 0 && nums[p] >= nums[p + 1]) --p;
// 2. 从后往前找第一个数 i:nums[i] > nums[p]
if (p >= 0) {
int i = n - 1;
while (i > p && nums[i] <= nums[p]) --i;
swap(nums, i, p);
}
// 3. 对 [p + 1, 数组长度) 进行升序:这里只需要两两交换就能满足
for (int i = p + 1, j = n - 1; i < j; ++i, --j)
swap(nums, i, j);
}
// 元素交换方法:
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
三、N皇后问题
N皇后是经典算法:
- 棋盘中皇后可以攻击同一行、同一列、左上、左下、右上、右下的任意单位
- 现给你一个 N * N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击
这个问题本质上跟全排列问题差不多,决策树 的每一层表示棋盘上的每一行。
- 路径:放置好的皇后列表
- 选择列表:在某一行任意一列放置一个皇后
- 结束条件:棋盘上放置满了皇后
模板如下:
- 主干:回溯
- 判断此位置是否可放置皇后
// 函数找到一个答案后就返回 true
boolean backtrack(int[][] board, int n, int row) {
if (row == n) {
result.add(buildList(board));
return;
}
for (int col = 0; col < n; col++) {
if (!isValid(board, row, col)) continue;
// 做选择
board[row][col] = 'Q';
// 进入下一行决策
if (backtrack(board, n, row + 1)) return true;
// 撤销选择
board[row][col] = '.';
}
return false;
}
boolean isValid(int[][] board, int row, int col) {
int n = board.size();
// 检查列中是否有皇后互相冲突
for (int i = 0; i < row; ++i) {
if (board[i][col] == 'Q') return false;
}
// 检查右上方是否有皇后互相冲突
for (int i = row - 1, j = col + 1; i >=0 & j >= 0; i--, j--) {
if (board[i][j] == 'Q') return false;
}
// 检查左上方是否有皇后互相冲突
for (int i = row - 1, j = col - 1; i >=0 & j >= 0; i--, j--) {
if (board[i][j] == 'Q') return false;
}
return true;
}
(1)N皇后
题目
题目:给你一个整数 n,你要返回 n 皇后问题的所有解。其中,每个解是一个棋盘布局,用字符 'Q' 表示一个皇后,用字符 '.' 表示一个空位置。
n 皇后问题的定义是,你要把 n 个皇后放到一个 n x n 的棋盘上,使得任意两个皇后之间都不能互相攻击,也就是说任意两个皇后不能位于同一行、同一列以及同一斜线。
# 比如说,给你的 n 等于 4。
# 4 皇后问题有以下两个解:
[
[
".Q..",
"...Q",
"Q...",
"..Q."
],
[
"..Q.",
"Q...",
"...Q",
".Q.."
]
]
题解
可以直接套上面模板。
这里做了个小优化:备忘录法,空间换时间,避免重复判断
# visited = new boolean[3][2*n]
# 这块判断时间复杂度从 O(n) 降为 O(1)
1. 判断每一列是否冲突:col, [0][col]
2. 主对角线各是否冲突:(row - col + n),[1][row - col + n]
3. 副对角线各是否冲突:(row + col), [2][row + col]
AC
题解:
public class LeetCode_51 {
// Time: O(n!), Space: O(n^2), Faster: 99.80%
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
// 记录是否访问过:列、主对角线、副对角线
boolean[][] visited = new boolean[3][2*n];
// 画棋盘
char[][] board = new char[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
board[i][j] = '.';
}
}
solve(0, n, result, board, visited);
return result;
}
private void solve(int row, int n, List<List<String>> result,
char[][] board, boolean[][] visited) {
if (row == n) {
result.add(buildList(board));
return;
}
for (int col = 0; col < n; ++col) {
// 列、主对角线、副对角线
if (!visited[0][col] && !visited[1][row-col+n] && !visited[2][row+col]) {
board[row][col] = 'Q';
visited[0][col] = visited[1][row-col+n] = visited[2][row+col] = true;
solve(row+1, n, result, board, visited);
visited[0][col] = visited[1][row-col+n] = visited[2][row+col] = false;
board[row][col] = '.';
}
}
}
private List<String> buildList(char[][] board) {
List<String> list = new ArrayList<>();
for (char[] row: board) {
list.add(new String(row));
}
return list;
}
}