This commit is contained in:
krahets
2023-10-08 04:43:12 +08:00
parent baac2d11a7
commit 2f761d9865
102 changed files with 0 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
---
comments: true
icon: material/map-marker-path
---
# 第 13 章   回溯
<div class="center-table" markdown>
![回溯](../assets/covers/chapter_backtracking.jpg){ width="600" }
</div>
!!! abstract
我们如同迷宫中的探索者,在前进的道路上可能会遇到困难。
回溯的力量让我们能够重新开始,不断尝试,最终找到通往光明的出口。
## 本章内容
- [13.1 &nbsp; 回溯算法](https://www.hello-algo.com/chapter_backtracking/backtracking_algorithm/)
- [13.2 &nbsp; 全排列问题](https://www.hello-algo.com/chapter_backtracking/permutations_problem/)
- [13.3 &nbsp; 子集和问题](https://www.hello-algo.com/chapter_backtracking/subset_sum_problem/)
- [13.4 &nbsp; N 皇后问题](https://www.hello-algo.com/chapter_backtracking/n_queens_problem/)
- [13.5 &nbsp; 小结](https://www.hello-algo.com/chapter_backtracking/summary/)
@@ -0,0 +1,619 @@
---
comments: true
---
# 13.4 &nbsp; N 皇后问题
!!! question
根据国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。给定 $n$ 个皇后和一个 $n \times n$ 大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案。
如图 13-15 所示,当 $n = 4$ 时,共可以找到两个解。从回溯算法的角度看,$n \times n$ 大小的棋盘共有 $n^2$ 个格子,给出了所有的选择 `choices` 。在逐个放置皇后的过程中,棋盘状态在不断地变化,每个时刻的棋盘就是状态 `state`
![4 皇后问题的解](n_queens_problem.assets/solution_4_queens.png)
<p align="center"> 图 13-15 &nbsp; 4 皇后问题的解 </p>
图 13-16 展示了本题的三个约束条件:**多个皇后不能在同一行、同一列、同一对角线**。值得注意的是,对角线分为主对角线 `\` 和次对角线 `/` 两种。
![n 皇后问题的约束条件](n_queens_problem.assets/n_queens_constraints.png)
<p align="center"> 图 13-16 &nbsp; n 皇后问题的约束条件 </p>
### 1. &nbsp; 逐行放置策略
皇后的数量和棋盘的行数都为 $n$ ,因此我们容易得到一个推论:**棋盘每行都允许且只允许放置一个皇后**。
也就是说,我们可以采取逐行放置策略:从第一行开始,在每行放置一个皇后,直至最后一行结束。
如图 13-17 所示,为 $4$ 皇后问题的逐行放置过程。受画幅限制,图 13-17 仅展开了第一行的其中一个搜索分支,并且将不满足列约束和对角线约束的方案都进行了剪枝。
![逐行放置策略](n_queens_problem.assets/n_queens_placing.png)
<p align="center"> 图 13-17 &nbsp; 逐行放置策略 </p>
本质上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出现多个皇后的所有搜索分支。
### 2. &nbsp; 列与对角线剪枝
为了满足列约束,我们可以利用一个长度为 $n$ 的布尔型数组 `cols` 记录每一列是否有皇后。在每次决定放置前,我们通过 `cols` 将已有皇后的列进行剪枝,并在回溯中动态更新 `cols` 的状态。
那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 $(row, col)$ ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,**即对角线上所有格子的 $row - col$ 为恒定值**。
也就是说,如果两个格子满足 $row_1 - col_1 = row_2 - col_2$ ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助图 13-18 所示的数组 `diag1` ,记录每条主对角线上是否有皇后。
同理,**次对角线上的所有格子的 $row + col$ 是恒定值**。我们同样也可以借助数组 `diag2` 来处理次对角线约束。
![处理列约束和对角线约束](n_queens_problem.assets/n_queens_cols_diagonals.png)
<p align="center"> 图 13-18 &nbsp; 处理列约束和对角线约束 </p>
### 3. &nbsp; 代码实现
请注意,$n$ 维方阵中 $row - col$ 的范围是 $[-n + 1, n - 1]$ $row + col$ 的范围是 $[0, 2n - 2]$ ,所以主对角线和次对角线的数量都为 $2n - 1$ ,即数组 `diag1``diag2` 的长度都为 $2n - 1$ 。
=== "Python"
```python title="n_queens.py"
def backtrack(
row: int,
n: int,
state: list[list[str]],
res: list[list[list[str]]],
cols: list[bool],
diags1: list[bool],
diags2: list[bool],
):
"""回溯算法:N 皇后"""
# 当放置完所有行时,记录解
if row == n:
res.append([list(row) for row in state])
return
# 遍历所有列
for col in range(n):
# 计算该格子对应的主对角线和副对角线
diag1 = row - col + n - 1
diag2 = row + col
# 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if not cols[col] and not diags1[diag1] and not diags2[diag2]:
# 尝试:将皇后放置在该格子
state[row][col] = "Q"
cols[col] = diags1[diag1] = diags2[diag2] = True
# 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2)
# 回退:将该格子恢复为空位
state[row][col] = "#"
cols[col] = diags1[diag1] = diags2[diag2] = False
def n_queens(n: int) -> list[list[list[str]]]:
"""求解 N 皇后"""
# 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
state = [["#" for _ in range(n)] for _ in range(n)]
cols = [False] * n # 记录列是否有皇后
diags1 = [False] * (2 * n - 1) # 记录主对角线是否有皇后
diags2 = [False] * (2 * n - 1) # 记录副对角线是否有皇后
res = []
backtrack(0, n, state, res, cols, diags1, diags2)
return res
```
=== "C++"
```cpp title="n_queens.cpp"
/* 回溯算法:N 皇后 */
void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,
vector<bool> &diags1, vector<bool> &diags2) {
// 当放置完所有行时,记录解
if (row == n) {
res.push_back(state);
return;
}
// 遍历所有列
for (int col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state[row][col] = "Q";
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state[row][col] = "#";
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
vector<vector<vector<string>>> nQueens(int n) {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
vector<vector<string>> state(n, vector<string>(n, "#"));
vector<bool> cols(n, false); // 记录列是否有皇后
vector<bool> diags1(2 * n - 1, false); // 记录主对角线是否有皇后
vector<bool> diags2(2 * n - 1, false); // 记录副对角线是否有皇后
vector<vector<vector<string>>> res;
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Java"
```java title="n_queens.java"
/* 回溯算法:N 皇后 */
void backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,
boolean[] cols, boolean[] diags1, boolean[] diags2) {
// 当放置完所有行时,记录解
if (row == n) {
List<List<String>> copyState = new ArrayList<>();
for (List<String> sRow : state) {
copyState.add(new ArrayList<>(sRow));
}
res.add(copyState);
return;
}
// 遍历所有列
for (int col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state.get(row).set(col, "Q");
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state.get(row).set(col, "#");
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
List<List<List<String>>> nQueens(int n) {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
List<List<String>> state = new ArrayList<>();
for (int i = 0; i < n; i++) {
List<String> row = new ArrayList<>();
for (int j = 0; j < n; j++) {
row.add("#");
}
state.add(row);
}
boolean[] cols = new boolean[n]; // 记录列是否有皇后
boolean[] diags1 = new boolean[2 * n - 1]; // 记录主对角线是否有皇后
boolean[] diags2 = new boolean[2 * n - 1]; // 记录副对角线是否有皇后
List<List<List<String>>> res = new ArrayList<>();
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "C#"
```csharp title="n_queens.cs"
/* 回溯算法:N 皇后 */
void Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,
bool[] cols, bool[] diags1, bool[] diags2) {
// 当放置完所有行时,记录解
if (row == n) {
List<List<string>> copyState = new();
foreach (List<string> sRow in state) {
copyState.Add(new List<string>(sRow));
}
res.Add(copyState);
return;
}
// 遍历所有列
for (int col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state[row][col] = "Q";
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
Backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state[row][col] = "#";
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
List<List<List<string>>> NQueens(int n) {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
List<List<string>> state = new();
for (int i = 0; i < n; i++) {
List<string> row = new();
for (int j = 0; j < n; j++) {
row.Add("#");
}
state.Add(row);
}
bool[] cols = new bool[n]; // 记录列是否有皇后
bool[] diags1 = new bool[2 * n - 1]; // 记录主对角线是否有皇后
bool[] diags2 = new bool[2 * n - 1]; // 记录副对角线是否有皇后
List<List<List<string>>> res = new();
Backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Go"
```go title="n_queens.go"
/* 回溯算法:N 皇后 */
func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {
// 当放置完所有行时,记录解
if row == n {
newState := make([][]string, len(*state))
for i, _ := range newState {
newState[i] = make([]string, len((*state)[0]))
copy(newState[i], (*state)[i])
}
*res = append(*res, newState)
}
// 遍历所有列
for col := 0; col < n; col++ {
// 计算该格子对应的主对角线和副对角线
diag1 := row - col + n - 1
diag2 := row + col
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {
// 尝试:将皇后放置在该格子
(*state)[row][col] = "Q"
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true
// 放置下一行
backtrack(row+1, n, state, res, cols, diags1, diags2)
// 回退:将该格子恢复为空位
(*state)[row][col] = "#"
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false
}
}
}
/* 回溯算法:N 皇后 */
func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {
// 当放置完所有行时,记录解
if row == n {
newState := make([][]string, len(*state))
for i, _ := range newState {
newState[i] = make([]string, len((*state)[0]))
copy(newState[i], (*state)[i])
}
*res = append(*res, newState)
}
// 遍历所有列
for col := 0; col < n; col++ {
// 计算该格子对应的主对角线和副对角线
diag1 := row - col + n - 1
diag2 := row + col
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {
// 尝试:将皇后放置在该格子
(*state)[row][col] = "Q"
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true
// 放置下一行
backtrack(row+1, n, state, res, cols, diags1, diags2)
// 回退:将该格子恢复为空位
(*state)[row][col] = "#"
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false
}
}
}
func nQueens(n int) [][][]string {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
state := make([][]string, n)
for i := 0; i < n; i++ {
row := make([]string, n)
for i := 0; i < n; i++ {
row[i] = "#"
}
state[i] = row
}
// 记录列是否有皇后
cols := make([]bool, n)
diags1 := make([]bool, 2*n-1)
diags2 := make([]bool, 2*n-1)
res := make([][][]string, 0)
backtrack(0, n, &state, &res, &cols, &diags1, &diags2)
return res
}
```
=== "Swift"
```swift title="n_queens.swift"
/* 回溯算法:N 皇后 */
func backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {
// 当放置完所有行时,记录解
if row == n {
res.append(state)
return
}
// 遍历所有列
for col in 0 ..< n {
// 计算该格子对应的主对角线和副对角线
let diag1 = row - col + n - 1
let diag2 = row + col
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
// 尝试:将皇后放置在该格子
state[row][col] = "Q"
cols[col] = true
diags1[diag1] = true
diags2[diag2] = true
// 放置下一行
backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
// 回退:将该格子恢复为空位
state[row][col] = "#"
cols[col] = false
diags1[diag1] = false
diags2[diag2] = false
}
}
}
/* 求解 N 皇后 */
func nQueens(n: Int) -> [[[String]]] {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
var state = Array(repeating: Array(repeating: "#", count: n), count: n)
var cols = Array(repeating: false, count: n) // 记录列是否有皇后
var diags1 = Array(repeating: false, count: 2 * n - 1) // 记录主对角线是否有皇后
var diags2 = Array(repeating: false, count: 2 * n - 1) // 记录副对角线是否有皇后
var res: [[[String]]] = []
backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
return res
}
```
=== "JS"
```javascript title="n_queens.js"
/* 回溯算法:N 皇后 */
function backtrack(row, n, state, res, cols, diags1, diags2) {
// 当放置完所有行时,记录解
if (row === n) {
res.push(state.map((row) => row.slice()));
return;
}
// 遍历所有列
for (let col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
const diag1 = row - col + n - 1;
const diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state[row][col] = 'Q';
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state[row][col] = '#';
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
function nQueens(n) {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
const state = Array.from({ length: n }, () => Array(n).fill('#'));
const cols = Array(n).fill(false); // 记录列是否有皇后
const diags1 = Array(2 * n - 1).fill(false); // 记录主对角线是否有皇后
const diags2 = Array(2 * n - 1).fill(false); // 记录副对角线是否有皇后
const res = [];
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "TS"
```typescript title="n_queens.ts"
/* 回溯算法:N 皇后 */
function backtrack(
row: number,
n: number,
state: string[][],
res: string[][][],
cols: boolean[],
diags1: boolean[],
diags2: boolean[]
): void {
// 当放置完所有行时,记录解
if (row === n) {
res.push(state.map((row) => row.slice()));
return;
}
// 遍历所有列
for (let col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
const diag1 = row - col + n - 1;
const diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state[row][col] = 'Q';
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state[row][col] = '#';
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
function nQueens(n: number): string[][][] {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
const state = Array.from({ length: n }, () => Array(n).fill('#'));
const cols = Array(n).fill(false); // 记录列是否有皇后
const diags1 = Array(2 * n - 1).fill(false); // 记录主对角线是否有皇后
const diags2 = Array(2 * n - 1).fill(false); // 记录副对角线是否有皇后
const res: string[][][] = [];
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Dart"
```dart title="n_queens.dart"
/* 回溯算法:N 皇后 */
void backtrack(
int row,
int n,
List<List<String>> state,
List<List<List<String>>> res,
List<bool> cols,
List<bool> diags1,
List<bool> diags2,
) {
// 当放置完所有行时,记录解
if (row == n) {
List<List<String>> copyState = [];
for (List<String> sRow in state) {
copyState.add(List.from(sRow));
}
res.add(copyState);
return;
}
// 遍历所有列
for (int col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state[row][col] = "Q";
cols[col] = true;
diags1[diag1] = true;
diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state[row][col] = "#";
cols[col] = false;
diags1[diag1] = false;
diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
List<List<List<String>>> nQueens(int n) {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
List<List<String>> state = List.generate(n, (index) => List.filled(n, "#"));
List<bool> cols = List.filled(n, false); // 记录列是否有皇后
List<bool> diags1 = List.filled(2 * n - 1, false); // 记录主对角线是否有皇后
List<bool> diags2 = List.filled(2 * n - 1, false); // 记录副对角线是否有皇后
List<List<List<String>>> res = [];
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Rust"
```rust title="n_queens.rs"
/* 回溯算法:N 皇后 */
fn backtrack(row: usize, n: usize, state: &mut Vec<Vec<String>>, res: &mut Vec<Vec<Vec<String>>>,
cols: &mut [bool], diags1: &mut [bool], diags2: &mut [bool]) {
// 当放置完所有行时,记录解
if row == n {
let mut copy_state: Vec<Vec<String>> = Vec::new();
for s_row in state.clone() {
copy_state.push(s_row);
}
res.push(copy_state);
return;
}
// 遍历所有列
for col in 0..n {
// 计算该格子对应的主对角线和副对角线
let diag1 = row + n - 1 - col;
let diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
// 尝试:将皇后放置在该格子
state.get_mut(row).unwrap()[col] = "Q".into();
(cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state.get_mut(row).unwrap()[col] = "#".into();
(cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);
}
}
}
/* 求解 N 皇后 */
fn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
let mut state: Vec<Vec<String>> = Vec::new();
for _ in 0..n {
let mut row: Vec<String> = Vec::new();
for _ in 0..n {
row.push("#".into());
}
state.push(row);
}
let mut cols = vec![false; n]; // 记录列是否有皇后
let mut diags1 = vec![false; 2 * n - 1]; // 记录主对角线是否有皇后
let mut diags2 = vec![false; 2 * n - 1]; // 记录副对角线是否有皇后
let mut res: Vec<Vec<Vec<String>>> = Vec::new();
backtrack(0, n, &mut state, &mut res, &mut cols, &mut diags1, &mut diags2);
res
}
```
=== "C"
```c title="n_queens.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Zig"
```zig title="n_queens.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n$、$n-1$、$\dots$、$2$、$1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。
数组 `state` 使用 $O(n^2)$ 空间,数组 `cols`、`diags1` 和 `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。
@@ -0,0 +1,919 @@
---
comments: true
---
# 13.2 &nbsp; 全排列问题
全排列问题是回溯算法的一个典型应用。它的定义是在给定一个集合(如一个数组或字符串)的情况下,找出这个集合中元素的所有可能的排列。
表 13-2 列举了几个示例数据,包括输入数组和对应的所有排列。
<p align="center"> 表 13-2 &nbsp; 数组与链表的效率对比 </p>
<div class="center-table" markdown>
| 输入数组 | 所有排列 |
| :---------- | :----------------------------------------------------------------- |
| $[1]$ | $[1]$ |
| $[1, 2]$ | $[1, 2], [2, 1]$ |
| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
</div>
## 13.2.1 &nbsp; 无相等元素的情况
!!! question
输入一个整数数组,数组中不包含重复元素,返回所有可能的排列。
从回溯算法的角度看,**我们可以把生成排列的过程想象成一系列选择的结果**。假设输入数组为 $[1, 2, 3]$ ,如果我们先选择 $1$、再选择 $3$、最后选择 $2$ ,则获得排列 $[1, 3, 2]$ 。回退表示撤销一个选择,之后继续尝试其他选择。
从回溯代码的角度看,候选集合 `choices` 是输入数组中的所有元素,状态 `state` 是直至目前已被选择的元素。请注意,每个元素只允许被选择一次,**因此 `state` 中的所有元素都应该是唯一的**。
如图 13-5 所示,我们可以将搜索过程展开成一个递归树,树中的每个节点代表当前状态 `state` 。从根节点开始,经过三轮选择后到达叶节点,每个叶节点都对应一个排列。
![全排列的递归树](permutations_problem.assets/permutations_i.png)
<p align="center"> 图 13-5 &nbsp; 全排列的递归树 </p>
### 1. &nbsp; 重复选择剪枝
为了实现每个元素只被选择一次,我们考虑引入一个布尔型数组 `selected` ,其中 `selected[i]` 表示 `choices[i]` 是否已被选择,并基于它实现以下剪枝操作。
- 在做出选择 `choice[i]` 后,我们就将 `selected[i]` 赋值为 $\text{True}$ ,代表它已被选择。
- 遍历选择列表 `choices` 时,跳过所有已被选择过的节点,即剪枝。
如图 13-6 所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1 和元素 3 的分支。
![全排列剪枝示例](permutations_problem.assets/permutations_i_pruning.png)
<p align="center"> 图 13-6 &nbsp; 全排列剪枝示例 </p>
观察图 13-6 发现,该剪枝操作将搜索空间大小从 $O(n^n)$ 降低至 $O(n!)$ 。
### 2. &nbsp; 代码实现
想清楚以上信息之后,我们就可以在框架代码中做“完形填空”了。为了缩短代码行数,我们不单独实现框架代码中的各个函数,而是将他们展开在 `backtrack()` 函数中。
=== "Python"
```python title="permutations_i.py"
def backtrack(
state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]
):
"""回溯算法:全排列 I"""
# 当状态长度等于元素数量时,记录解
if len(state) == len(choices):
res.append(list(state))
return
# 遍历所有选择
for i, choice in enumerate(choices):
# 剪枝:不允许重复选择元素
if not selected[i]:
# 尝试:做出选择,更新状态
selected[i] = True
state.append(choice)
# 进行下一轮选择
backtrack(state, choices, selected, res)
# 回退:撤销选择,恢复到之前的状态
selected[i] = False
state.pop()
def permutations_i(nums: list[int]) -> list[list[int]]:
"""全排列 I"""
res = []
backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)
return res
```
=== "C++"
```cpp title="permutations_i.cpp"
/* 回溯算法:全排列 I */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
// 当状态长度等于元素数量时,记录解
if (state.size() == choices.size()) {
res.push_back(state);
return;
}
// 遍历所有选择
for (int i = 0; i < choices.size(); i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i]) {
// 尝试:做出选择,更新状态
selected[i] = true;
state.push_back(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.pop_back();
}
}
}
/* 全排列 I */
vector<vector<int>> permutationsI(vector<int> nums) {
vector<int> state;
vector<bool> selected(nums.size(), false);
vector<vector<int>> res;
backtrack(state, nums, selected, res);
return res;
}
```
=== "Java"
```java title="permutations_i.java"
/* 回溯算法:全排列 I */
void backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {
// 当状态长度等于元素数量时,记录解
if (state.size() == choices.length) {
res.add(new ArrayList<Integer>(state));
return;
}
// 遍历所有选择
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i]) {
// 尝试:做出选择,更新状态
selected[i] = true;
state.add(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.remove(state.size() - 1);
}
}
}
/* 全排列 I */
List<List<Integer>> permutationsI(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);
return res;
}
```
=== "C#"
```csharp title="permutations_i.cs"
/* 回溯算法:全排列 I */
void Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {
// 当状态长度等于元素数量时,记录解
if (state.Count == choices.Length) {
res.Add(new List<int>(state));
return;
}
// 遍历所有选择
for (int i = 0; i < choices.Length; i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i]) {
// 尝试:做出选择,更新状态
selected[i] = true;
state.Add(choice);
// 进行下一轮选择
Backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.RemoveAt(state.Count - 1);
}
}
}
/* 全排列 I */
List<List<int>> PermutationsI(int[] nums) {
List<List<int>> res = new();
Backtrack(new List<int>(), nums, new bool[nums.Length], res);
return res;
}
```
=== "Go"
```go title="permutations_i.go"
/* 回溯算法:全排列 I */
func backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {
// 当状态长度等于元素数量时,记录解
if len(*state) == len(*choices) {
newState := append([]int{}, *state...)
*res = append(*res, newState)
}
// 遍历所有选择
for i := 0; i < len(*choices); i++ {
choice := (*choices)[i]
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if !(*selected)[i] {
// 尝试:做出选择,更新状态
(*selected)[i] = true
*state = append(*state, choice)
// 进行下一轮选择
backtrackI(state, choices, selected, res)
// 回退:撤销选择,恢复到之前的状态
(*selected)[i] = false
*state = (*state)[:len(*state)-1]
}
}
}
/* 全排列 I */
func permutationsI(nums []int) [][]int {
res := make([][]int, 0)
state := make([]int, 0)
selected := make([]bool, len(nums))
backtrackI(&state, &nums, &selected, &res)
return res
}
```
=== "Swift"
```swift title="permutations_i.swift"
/* 回溯算法:全排列 I */
func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {
// 当状态长度等于元素数量时,记录解
if state.count == choices.count {
res.append(state)
return
}
// 遍历所有选择
for (i, choice) in choices.enumerated() {
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if !selected[i] {
// 尝试:做出选择,更新状态
selected[i] = true
state.append(choice)
// 进行下一轮选择
backtrack(state: &state, choices: choices, selected: &selected, res: &res)
// 回退:撤销选择,恢复到之前的状态
selected[i] = false
state.removeLast()
}
}
}
/* 全排列 I */
func permutationsI(nums: [Int]) -> [[Int]] {
var state: [Int] = []
var selected = Array(repeating: false, count: nums.count)
var res: [[Int]] = []
backtrack(state: &state, choices: nums, selected: &selected, res: &res)
return res
}
```
=== "JS"
```javascript title="permutations_i.js"
/* 回溯算法:全排列 I */
function backtrack(state, choices, selected, res) {
// 当状态长度等于元素数量时,记录解
if (state.length === choices.length) {
res.push([...state]);
return;
}
// 遍历所有选择
choices.forEach((choice, i) => {
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i]) {
// 尝试:做出选择,更新状态
selected[i] = true;
state.push(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.pop();
}
});
}
/* 全排列 I */
function permutationsI(nums) {
const res = [];
backtrack([], nums, Array(nums.length).fill(false), res);
return res;
}
```
=== "TS"
```typescript title="permutations_i.ts"
/* 回溯算法:全排列 I */
function backtrack(
state: number[],
choices: number[],
selected: boolean[],
res: number[][]
): void {
// 当状态长度等于元素数量时,记录解
if (state.length === choices.length) {
res.push([...state]);
return;
}
// 遍历所有选择
choices.forEach((choice, i) => {
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i]) {
// 尝试:做出选择,更新状态
selected[i] = true;
state.push(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.pop();
}
});
}
/* 全排列 I */
function permutationsI(nums: number[]): number[][] {
const res: number[][] = [];
backtrack([], nums, Array(nums.length).fill(false), res);
return res;
}
```
=== "Dart"
```dart title="permutations_i.dart"
/* 回溯算法:全排列 I */
void backtrack(
List<int> state,
List<int> choices,
List<bool> selected,
List<List<int>> res,
) {
// 当状态长度等于元素数量时,记录解
if (state.length == choices.length) {
res.add(List.from(state));
return;
}
// 遍历所有选择
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i]) {
// 尝试:做出选择,更新状态
selected[i] = true;
state.add(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.removeLast();
}
}
}
/* 全排列 I */
List<List<int>> permutationsI(List<int> nums) {
List<List<int>> res = [];
backtrack([], nums, List.filled(nums.length, false), res);
return res;
}
```
=== "Rust"
```rust title="permutations_i.rs"
/* 回溯算法:全排列 I */
fn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {
// 当状态长度等于元素数量时,记录解
if state.len() == choices.len() {
res.push(state);
return;
}
// 遍历所有选择
for i in 0..choices.len() {
let choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if !selected[i] {
// 尝试:做出选择,更新状态
selected[i] = true;
state.push(choice);
// 进行下一轮选择
backtrack(state.clone(), choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.remove(state.len() - 1);
}
}
}
/* 全排列 I */
fn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {
let mut res = Vec::new(); // 状态(子集)
backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);
res
}
```
=== "C"
```c title="permutations_i.c"
/* 回溯算法:全排列 I */
void backtrack(vector *state, vector *choices, vector *selected, vector *res) {
// 当状态长度等于元素数量时,记录解
if (state->size == choices->size) {
vector *newState = newVector();
for (int i = 0; i < state->size; i++) {
vectorPushback(newState, state->data[i], sizeof(int));
}
vectorPushback(res, newState, sizeof(vector));
return;
}
// 遍历所有选择
for (int i = 0; i < choices->size; i++) {
int *choice = malloc(sizeof(int));
*choice = *((int *)(choices->data[i]));
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
bool select = *((bool *)(selected->data[i]));
if (!select) {
// 尝试:做出选择,更新状态
*((bool *)selected->data[i]) = true;
vectorPushback(state, choice, sizeof(int));
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
*((bool *)selected->data[i]) = false;
vectorPopback(state);
}
}
}
/* 全排列 I */
vector *permutationsI(vector *nums) {
vector *iState = newVector();
int select[3] = {false, false, false};
vector *bSelected = newVector();
for (int i = 0; i < nums->size; i++) {
vectorPushback(bSelected, &select[i], sizeof(int));
}
vector *res = newVector();
// 前序遍历
backtrack(iState, nums, bSelected, res);
return res;
}
```
=== "Zig"
```zig title="permutations_i.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
## 13.2.2 &nbsp; 考虑相等元素的情况
!!! question
输入一个整数数组,**数组中可能包含重复元素**,返回所有不重复的排列。
假设输入数组为 $[1, 1, 2]$ 。为了方便区分两个重复元素 $1$ ,我们将第二个 $1$ 记为 $\hat{1}$ 。
如图 13-7 所示,上述方法生成的排列有一半都是重复的。
![重复排列](permutations_problem.assets/permutations_ii.png)
<p align="center"> 图 13-7 &nbsp; 重复排列 </p>
那么如何去除重复的排列呢?最直接地,考虑借助一个哈希表,直接对排列结果进行去重。然而这样做不够优雅,**因为生成重复排列的搜索分支是没有必要的,应当被提前识别并剪枝**,这样可以进一步提升算法效率。
### 1. &nbsp; 相等元素剪枝
观察图 13-8 ,在第一轮中,选择 $1$ 或选择 $\hat{1}$ 是等价的,在这两个选择之下生成的所有排列都是重复的。因此应该把 $\hat{1}$ 剪枝掉。
同理,在第一轮选择 $2$ 之后,第二轮选择中的 $1$ 和 $\hat{1}$ 也会产生重复分支,因此也应将第二轮的 $\hat{1}$ 剪枝。
本质上看,**我们的目标是在某一轮选择中,保证多个相等的元素仅被选择一次**。
![重复排列剪枝](permutations_problem.assets/permutations_ii_pruning.png)
<p align="center"> 图 13-8 &nbsp; 重复排列剪枝 </p>
### 2. &nbsp; 代码实现
在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希表 `duplicated` ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝。
=== "Python"
```python title="permutations_ii.py"
def backtrack(
state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]
):
"""回溯算法:全排列 II"""
# 当状态长度等于元素数量时,记录解
if len(state) == len(choices):
res.append(list(state))
return
# 遍历所有选择
duplicated = set[int]()
for i, choice in enumerate(choices):
# 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if not selected[i] and choice not in duplicated:
# 尝试:做出选择,更新状态
duplicated.add(choice) # 记录选择过的元素值
selected[i] = True
state.append(choice)
# 进行下一轮选择
backtrack(state, choices, selected, res)
# 回退:撤销选择,恢复到之前的状态
selected[i] = False
state.pop()
def permutations_ii(nums: list[int]) -> list[list[int]]:
"""全排列 II"""
res = []
backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)
return res
```
=== "C++"
```cpp title="permutations_ii.cpp"
/* 回溯算法:全排列 II */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
// 当状态长度等于元素数量时,记录解
if (state.size() == choices.size()) {
res.push_back(state);
return;
}
// 遍历所有选择
unordered_set<int> duplicated;
for (int i = 0; i < choices.size(); i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i] && duplicated.find(choice) == duplicated.end()) {
// 尝试:做出选择,更新状态
duplicated.emplace(choice); // 记录选择过的元素值
selected[i] = true;
state.push_back(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.pop_back();
}
}
}
/* 全排列 II */
vector<vector<int>> permutationsII(vector<int> nums) {
vector<int> state;
vector<bool> selected(nums.size(), false);
vector<vector<int>> res;
backtrack(state, nums, selected, res);
return res;
}
```
=== "Java"
```java title="permutations_ii.java"
/* 回溯算法:全排列 II */
void backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {
// 当状态长度等于元素数量时,记录解
if (state.size() == choices.length) {
res.add(new ArrayList<Integer>(state));
return;
}
// 遍历所有选择
Set<Integer> duplicated = new HashSet<Integer>();
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i] && !duplicated.contains(choice)) {
// 尝试:做出选择,更新状态
duplicated.add(choice); // 记录选择过的元素值
selected[i] = true;
state.add(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.remove(state.size() - 1);
}
}
}
/* 全排列 II */
List<List<Integer>> permutationsII(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);
return res;
}
```
=== "C#"
```csharp title="permutations_ii.cs"
/* 回溯算法:全排列 II */
void Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {
// 当状态长度等于元素数量时,记录解
if (state.Count == choices.Length) {
res.Add(new List<int>(state));
return;
}
// 遍历所有选择
ISet<int> duplicated = new HashSet<int>();
for (int i = 0; i < choices.Length; i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i] && !duplicated.Contains(choice)) {
// 尝试:做出选择,更新状态
duplicated.Add(choice); // 记录选择过的元素值
selected[i] = true;
state.Add(choice);
// 进行下一轮选择
Backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.RemoveAt(state.Count - 1);
}
}
}
/* 全排列 II */
List<List<int>> PermutationsII(int[] nums) {
List<List<int>> res = new();
Backtrack(new List<int>(), nums, new bool[nums.Length], res);
return res;
}
```
=== "Go"
```go title="permutations_ii.go"
/* 回溯算法:全排列 II */
func backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {
// 当状态长度等于元素数量时,记录解
if len(*state) == len(*choices) {
newState := append([]int{}, *state...)
*res = append(*res, newState)
}
// 遍历所有选择
duplicated := make(map[int]struct{}, 0)
for i := 0; i < len(*choices); i++ {
choice := (*choices)[i]
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if _, ok := duplicated[choice]; !ok && !(*selected)[i] {
// 尝试:做出选择,更新状态
// 记录选择过的元素值
duplicated[choice] = struct{}{}
(*selected)[i] = true
*state = append(*state, choice)
// 进行下一轮选择
backtrackI(state, choices, selected, res)
// 回退:撤销选择,恢复到之前的状态
(*selected)[i] = false
*state = (*state)[:len(*state)-1]
}
}
}
/* 全排列 II */
func permutationsII(nums []int) [][]int {
res := make([][]int, 0)
state := make([]int, 0)
selected := make([]bool, len(nums))
backtrackII(&state, &nums, &selected, &res)
return res
}
```
=== "Swift"
```swift title="permutations_ii.swift"
/* 回溯算法:全排列 II */
func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {
// 当状态长度等于元素数量时,记录解
if state.count == choices.count {
res.append(state)
return
}
// 遍历所有选择
var duplicated: Set<Int> = []
for (i, choice) in choices.enumerated() {
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if !selected[i], !duplicated.contains(choice) {
// 尝试:做出选择,更新状态
duplicated.insert(choice) // 记录选择过的元素值
selected[i] = true
state.append(choice)
// 进行下一轮选择
backtrack(state: &state, choices: choices, selected: &selected, res: &res)
// 回退:撤销选择,恢复到之前的状态
selected[i] = false
state.removeLast()
}
}
}
/* 全排列 II */
func permutationsII(nums: [Int]) -> [[Int]] {
var state: [Int] = []
var selected = Array(repeating: false, count: nums.count)
var res: [[Int]] = []
backtrack(state: &state, choices: nums, selected: &selected, res: &res)
return res
}
```
=== "JS"
```javascript title="permutations_ii.js"
/* 回溯算法:全排列 II */
function backtrack(state, choices, selected, res) {
// 当状态长度等于元素数量时,记录解
if (state.length === choices.length) {
res.push([...state]);
return;
}
// 遍历所有选择
const duplicated = new Set();
choices.forEach((choice, i) => {
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i] && !duplicated.has(choice)) {
// 尝试:做出选择,更新状态
duplicated.add(choice); // 记录选择过的元素值
selected[i] = true;
state.push(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.pop();
}
});
}
/* 全排列 II */
function permutationsII(nums) {
const res = [];
backtrack([], nums, Array(nums.length).fill(false), res);
return res;
}
```
=== "TS"
```typescript title="permutations_ii.ts"
/* 回溯算法:全排列 II */
function backtrack(
state: number[],
choices: number[],
selected: boolean[],
res: number[][]
): void {
// 当状态长度等于元素数量时,记录解
if (state.length === choices.length) {
res.push([...state]);
return;
}
// 遍历所有选择
const duplicated = new Set();
choices.forEach((choice, i) => {
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i] && !duplicated.has(choice)) {
// 尝试:做出选择,更新状态
duplicated.add(choice); // 记录选择过的元素值
selected[i] = true;
state.push(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.pop();
}
});
}
/* 全排列 II */
function permutationsII(nums: number[]): number[][] {
const res: number[][] = [];
backtrack([], nums, Array(nums.length).fill(false), res);
return res;
}
```
=== "Dart"
```dart title="permutations_ii.dart"
/* 回溯算法:全排列 II */
void backtrack(
List<int> state,
List<int> choices,
List<bool> selected,
List<List<int>> res,
) {
// 当状态长度等于元素数量时,记录解
if (state.length == choices.length) {
res.add(List.from(state));
return;
}
// 遍历所有选择
Set<int> duplicated = {};
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if (!selected[i] && !duplicated.contains(choice)) {
// 尝试:做出选择,更新状态
duplicated.add(choice); // 记录选择过的元素值
selected[i] = true;
state.add(choice);
// 进行下一轮选择
backtrack(state, choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.removeLast();
}
}
}
/* 全排列 II */
List<List<int>> permutationsII(List<int> nums) {
List<List<int>> res = [];
backtrack([], nums, List.filled(nums.length, false), res);
return res;
}
```
=== "Rust"
```rust title="permutations_ii.rs"
/* 回溯算法:全排列 II */
fn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {
// 当状态长度等于元素数量时,记录解
if state.len() == choices.len() {
res.push(state);
return;
}
// 遍历所有选择
let mut duplicated = HashSet::<i32>::new();
for i in 0..choices.len() {
let choice = choices[i];
// 剪枝:不允许重复选择元素 且 不允许重复选择相等元素
if !selected[i] && !duplicated.contains(&choice) {
// 尝试:做出选择,更新状态
duplicated.insert(choice); // 记录选择过的元素值
selected[i] = true;
state.push(choice);
// 进行下一轮选择
backtrack(state.clone(), choices, selected, res);
// 回退:撤销选择,恢复到之前的状态
selected[i] = false;
state.remove(state.len() - 1);
}
}
}
/* 全排列 II */
fn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {
let mut res = Vec::new();
backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);
res
}
```
=== "C"
```c title="permutations_ii.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Zig"
```zig title="permutations_ii.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
假设元素两两之间互不相同,则 $n$ 个元素共有 $n!$ 种排列(阶乘);在记录结果时,需要复制长度为 $n$ 的列表,使用 $O(n)$ 时间。**因此时间复杂度为 $O(n!n)$** 。
最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。`selected` 使用 $O(n)$ 空间。同一时刻最多共有 $n$ 个 `duplicated` ,使用 $O(n^2)$ 空间。**因此空间复杂度为 $O(n^2)$** 。
### 3. &nbsp; 两种剪枝对比
请注意,虽然 `selected` 和 `duplicated` 都用作剪枝,但两者的目标是不同的。
- **重复选择剪枝**:整个搜索过程中只有一个 `selected` 。它记录的是当前状态中包含哪些元素,作用是避免某个元素在 `state` 中重复出现。
- **相等元素剪枝**:每轮选择(即每个开启的 `backtrack` 函数)都包含一个 `duplicated` 。它记录的是在遍历中哪些元素已被选择过,作用是保证相等元素只被选择一次。
图 13-9 展示了两个剪枝条件的生效范围。注意,树中的每个节点代表一个选择,从根节点到叶节点的路径上的各个节点构成一个排列。
![两种剪枝条件的作用范围](permutations_problem.assets/permutations_ii_pruning_summary.png)
<p align="center"> 图 13-9 &nbsp; 两种剪枝条件的作用范围 </p>
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
---
comments: true
---
# 13.5 &nbsp; 小结
### 1. &nbsp; 重点回顾
- 回溯算法本质是穷举法,通过对解空间进行深度优先遍历来寻找符合条件的解。在搜索过程中,遇到满足条件的解则记录,直至找到所有解或遍历完成后结束。
- 回溯算法的搜索过程包括尝试与回退两个部分。它通过深度优先搜索来尝试各种选择,当遇到不满足约束条件的情况时,则撤销上一步的选择,退回到之前的状态,并继续尝试其他选择。尝试与回退是两个方向相反的操作。
- 回溯问题通常包含多个约束条件,它们可用于实现剪枝操作。剪枝可以提前结束不必要的搜索分支,大幅提升搜索效率。
- 回溯算法主要可用于解决搜索问题和约束满足问题。组合优化问题虽然可以用回溯算法解决,但往往存在更高效率或更好效果的解法。
- 全排列问题旨在搜索给定集合的所有可能的排列。我们借助一个数组来记录每个元素是否被选择,剪枝掉重复选择同一元素的搜索分支,确保每个元素只被选择一次。
- 在全排列问题中,如果集合中存在重复元素,则最终结果会出现重复排列。我们需要约束相等元素在每轮中只能被选择一次,这通常借助一个哈希表来实现。
- 子集和问题的目标是在给定集合中找到和为目标值的所有子集。集合不区分元素顺序,而搜索过程会输出所有顺序的结果,产生重复子集。我们在回溯前将数据进行排序,并设置一个变量来指示每一轮的遍历起点,从而将生成重复子集的搜索分支进行剪枝。
- 对于子集和问题,数组中的相等元素会产生重复集合。我们利用数组已排序的前置条件,通过判断相邻元素是否相等实现剪枝,从而确保相等元素在每轮中只能被选中一次。
- $n$ 皇后旨在寻找将 $n$ 个皇后放置到 $n \times n$ 尺寸棋盘上的方案,要求所有皇后两两之间无法攻击对方。该问题的约束条件有行约束、列约束、主对角线和副对角线约束。为满足行约束,我们采用按行放置的策略,保证每一行放置一个皇后。
- 列约束和对角线约束的处理方式类似。对于列约束,我们利用一个数组来记录每一列是否有皇后,从而指示选中的格子是否合法。对于对角线约束,我们借助两个数组来分别记录该主、副对角线是否存在皇后;难点在于找处在到同一主(副)对角线上格子满足的行列索引规律。
### 2. &nbsp; Q & A
!!! question "怎么理解回溯和递归的关系?"
总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。
- 回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。
- 递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。