This commit is contained in:
krahets
2025-10-17 05:33:23 +08:00
parent 9278f3c659
commit 68bb9afb16
113 changed files with 35936 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
---
comments: true
icon: material/map-marker-path
---
# 第 13 章   バックトラッキング
![バックトラッキング](../assets/covers/chapter_backtracking.jpg){ class="cover-image" }
!!! abstract
迷路の探検家のように、私たちは前進する道で障害に遭遇することがあります。
バックトラッキングの力は、私たちに新しく始めること、試し続けること、そして最終的に光への出口を見つけることを可能にします。
## 章の内容
- [13.1   バックトラッキングアルゴリズム](backtracking_algorithm.md)
- [13.2   順列問題](permutations_problem.md)
- [13.3   部分集合和問題](subset_sum_problem.md)
- [13.4   Nクイーン問題](n_queens_problem.md)
- [13.5   まとめ](summary.md)
@@ -0,0 +1,296 @@
---
comments: true
---
# 13.4   Nクイーン問題
!!! question
チェスのルールによると、クイーンは同じ行、列、または対角線上の駒を攻撃できます。$n$ 個のクイーンと $n \times n$ のチェスボードが与えられた場合、2つのクイーンが互いに攻撃できない配置を見つけてください。
以下の図に示すように、$n = 4$ の場合、2つの解があります。バックトラッキングアルゴリズムの観点から、$n \times n$ のチェスボードには $n^2$ 個のマスがあり、すべての可能な選択肢 `choices` を示しています。チェスボードの状態 `state` は、各クイーンが配置されるにつれて継続的に変化します。
![4クイーン問題の解](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" }
<p align="center"> 図 13-15 &nbsp; 4クイーン問題の解 </p>
以下の図は、この問題の3つの制約を示しています:**複数のクイーンは同じ行、列、または対角線を占有できません**。対角線は主対角線 `\` と副対角線 `/` に分かれることに注意することが重要です。
![Nクイーン問題の制約](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" }
<p align="center"> 図 13-16 &nbsp; Nクイーン問題の制約 </p>
### 1. &nbsp; 行ごとの配置戦略
クイーンの数がチェスボードの行数と等しく、どちらも $n$ であるため、**チェスボードの各行には1つのクイーンのみが配置できることが**容易に結論付けられます。
これは、行ごとの配置戦略を採用できることを意味します:最初の行から開始して、最後の行に到達するまで行ごとに1つのクイーンを配置します。
以下の図は、4クイーン問題の行ごとの配置プロセスを示しています。スペースの制限により、図は最初の行の1つの検索分岐のみを展開し、列と対角線の制約を満たさない配置を剪定します。
![行ごとの配置戦略](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" }
<p align="center"> 図 13-17 &nbsp; 行ごとの配置戦略 </p>
本質的に、**行ごとの配置戦略は剪定関数として機能し**、同じ行に複数のクイーンを配置するすべての検索分岐を除去します。
### 2. &nbsp; 列と対角線の剪定
列の制約を満たすために、長さ $n$ のブール配列 `cols` を使用して、各列にクイーンが占有されているかどうかを追跡できます。各配置決定の前に、`cols` を使用してすでにクイーンがある列を剪定し、バックトラッキング中に動的に更新されます。
!!! tip
行列の原点は左上隅にあり、行インデックスは上から下に増加し、列インデックスは左から右に増加することに注意してください。
対角線の制約はどうでしょうか?チェスボード上の特定のセルの行と列のインデックスを $(row, col)$ とします。特定の主対角線を選択することで、その対角線上のすべてのセルで差 $row - col$ が同じであることに気付きます。**つまり、$row - col$ は主対角線上で定数値です**。
言い換えると、2つのセルが $row_1 - col_1 = row_2 - col_2$ を満たす場合、それらは確実に同じ主対角線上にあります。このパターンを使用して、以下の図に示す配列 `diags1` を利用して、クイーンが主対角線上にあるかどうかを追跡できます。
同様に、**$row + col$ の和は副対角線上のすべてのセルで定数値です**。配列 `diags2` を使用して副対角線の制約も処理できます。
![列と対角線の制約の処理](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" }
<p align="center"> 図 13-18 &nbsp; 列と対角線の制約の処理 </p>
### 3. &nbsp; コード実装
$n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で、$row + col$ の範囲は $[0, 2n - 2]$ であることに注意してください。したがって、主対角線と副対角線の数はどちらも $2n - 1$ で、配列 `diags1``diags2` の長さは $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"
[class]{n_queens}-[func]{Backtrack}
[class]{n_queens}-[func]{NQueens}
```
=== "Go"
```go title="n_queens.go"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Swift"
```swift title="n_queens.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "JS"
```javascript title="n_queens.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "TS"
```typescript title="n_queens.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Dart"
```dart title="n_queens.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Rust"
```rust title="n_queens.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{n_queens}
```
=== "C"
```c title="n_queens.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Kotlin"
```kotlin title="n_queens.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
=== "Ruby"
```ruby title="n_queens.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{n_queens}
```
=== "Zig"
```zig title="n_queens.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{nQueens}
```
$n$ 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、$n$、$n-1$、$\dots$、$2$、$1$ の選択肢があり、$O(n!)$ 時間を使用します。解を記録する際、行列 `state` をコピーして `res` に追加する必要があり、コピー操作は $O(n^2)$ 時間を使用します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$ です**。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。
配列 `state` は $O(n^2)$ 空間を使用し、配列 `cols`、`diags1`、`diags2` はそれぞれ $O(n)$ 空間を使用します。最大再帰深度は $n$ で、$O(n)$ のスタックフレーム空間を使用します。したがって、**空間計算量は $O(n^2)$ です**。
@@ -0,0 +1,493 @@
---
comments: true
---
# 13.2 &nbsp; 順列問題
順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。
以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。
<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` のすべての要素は一意である必要があります**。
以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の `state` を表します。ルートノードから開始して、3回の選択の後、葉ノードに到達します—それぞれが順列に対応します。
![順列の再帰木](permutations_problem.assets/permutations_i.png){ class="animation-figure" }
<p align="center"> 図 13-5 &nbsp; 順列の再帰木 </p>
### 1. &nbsp; 重複選択の剪定
各要素が一度だけ選択されることを保証するために、ブール配列 `selected` を導入します。ここで `selected[i]``choices[i]` が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します:
- `choice[i]` を選択した後、`selected[i]` を $\text{True}$ に設定して選択されたとマークします。
- `choices` を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。
以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。
![順列の剪定例](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" }
<p align="center"> 図 13-6 &nbsp; 順列の剪定例 </p>
図から、この剪定プロセスが検索空間を $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"
[class]{permutations_i}-[func]{Backtrack}
[class]{permutations_i}-[func]{PermutationsI}
```
=== "Go"
```go title="permutations_i.go"
[class]{}-[func]{backtrackI}
[class]{}-[func]{permutationsI}
```
=== "Swift"
```swift title="permutations_i.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "JS"
```javascript title="permutations_i.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "TS"
```typescript title="permutations_i.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Dart"
```dart title="permutations_i.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Rust"
```rust title="permutations_i.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_i}
```
=== "C"
```c title="permutations_i.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Kotlin"
```kotlin title="permutations_i.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
=== "Ruby"
```ruby title="permutations_i.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_i}
```
=== "Zig"
```zig title="permutations_i.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
## 13.2.2 &nbsp; 重複要素を考慮する場合
!!! question
**重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。
入力配列が $[1, 1, 2]$ だとします。2つの同一要素 $1$ を区別するために、2番目を $\hat{1}$ とラベル付けします。
以下の図に示すように、この方法で生成される順列の半分は重複です:
![重複順列](permutations_problem.assets/permutations_ii.png){ class="animation-figure" }
<p align="center"> 図 13-7 &nbsp; 重複順列 </p>
では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。**重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです**、これによりアルゴリズムの効率が向上します。
### 1. &nbsp; 等値要素の剪定
以下の図を見ると、最初のラウンドで $1$ または $\hat{1}$ を選択すると同じ順列につながるため、$\hat{1}$ を剪定します。
同様に、最初のラウンドで $2$ を選択した後、2番目のラウンドで $1$ または $\hat{1}$ を選択しても重複分岐につながるため、その時も $\hat{1}$ を剪定します。
本質的に、**私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。**
![重複順列の剪定](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" }
<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"
[class]{permutations_ii}-[func]{Backtrack}
[class]{permutations_ii}-[func]{PermutationsII}
```
=== "Go"
```go title="permutations_ii.go"
[class]{}-[func]{backtrackII}
[class]{}-[func]{permutationsII}
```
=== "Swift"
```swift title="permutations_ii.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "JS"
```javascript title="permutations_ii.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "TS"
```typescript title="permutations_ii.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Dart"
```dart title="permutations_ii.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Rust"
```rust title="permutations_ii.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_ii}
```
=== "C"
```c title="permutations_ii.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Kotlin"
```kotlin title="permutations_ii.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
=== "Ruby"
```ruby title="permutations_ii.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_ii}
```
=== "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; 2つの剪定方法の比較
`selected` と `duplicated` はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています:
- **重複選択の剪定**`selected` 経由):検索全体に単一の `selected` 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が `state` に複数回現れることを防ぎます。
- **等値要素の剪定**`duplicated` 経由):`backtrack` 関数の各呼び出しは独自の `duplicated` セットを使用し、その特定の反復(`for` ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。
以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ノードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。
![2つの剪定条件の範囲](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" }
<p align="center"> 図 13-9 &nbsp; 2つの剪定条件の範囲 </p>
@@ -0,0 +1,703 @@
---
comments: true
---
# 13.3 &nbsp; 部分集合和問題
## 13.3.1 &nbsp; 重複要素がない場合
!!! question
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素がなく、各要素は複数回選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
例えば、入力集合 $\{3, 4, 5\}$ とターゲット整数 $9$ の場合、解は $\{3, 3, 3\}, \{4, 5\}$ です。以下の2点に注意してください。
- 入力集合の要素は無制限に選択できます。
- 部分集合は要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。
### 1. &nbsp; 順列解法の参考
順列問題と同様に、部分集合の生成を一連の選択として想像でき、選択プロセス中に「要素和」をリアルタイムで更新できます。要素和が `target` に等しくなったとき、部分集合を結果リストに記録します。
順列問題とは異なり、**この問題では要素は無制限に選択できるため**、要素が選択されたかどうかを記録するための `selected` ブール配列を使用する必要がありません。順列コードに軽微な修正を加えて、最初に問題を解決できます:
=== "Python"
```python title="subset_sum_i_naive.py"
def backtrack(
state: list[int],
target: int,
total: int,
choices: list[int],
res: list[list[int]],
):
"""バックトラッキングアルゴリズム:部分集合の和 I"""
# 部分集合の和が target と等しいとき、解を記録
if total == target:
res.append(list(state))
return
# すべての選択肢を走査
for i in range(len(choices)):
# 枝刈り:部分集合の和が target を超える場合、その選択をスキップ
if total + choices[i] > target:
continue
# 試行:選択を行い、要素と total を更新
state.append(choices[i])
# 次の選択ラウンドに進む
backtrack(state, target, total + choices[i], choices, res)
# 撤回:選択を取り消し、前の状態に復元
state.pop()
def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:
"""部分集合の和 I を解く(重複する部分集合を含む)"""
state = [] # 状態(部分集合)
total = 0 # 部分集合の和
res = [] # 結果リスト(部分集合リスト)
backtrack(state, target, total, nums, res)
return res
```
=== "C++"
```cpp title="subset_sum_i_naive.cpp"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (total == target) {
res.push_back(state);
return;
}
// すべての選択肢を走査
for (int i = 0; i < choices.size(); i++) {
// 剪定:部分集合の和がtargetを超えた場合、その選択をスキップ
if (total + choices[i] > target) {
continue;
}
// 試行:選択を行い、要素とtotalを更新
state.push_back(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target, total + choices[i], choices, res);
// 回退:選択を取り消し、前の状態に復元
state.pop_back();
}
}
/* 部分集合和 I を解く(重複する部分集合を含む) */
vector<vector<int>> subsetSumINaive(vector<int> nums, int target) {
vector<int> state; // 状態(部分集合)
int total = 0; // 部分集合の和
vector<vector<int>> res; // 結果リスト(部分集合リスト)
backtrack(state, target, total, nums, res);
return res;
}
```
=== "Java"
```java title="subset_sum_i_naive.java"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (total == target) {
res.add(new ArrayList<>(state));
return;
}
// すべての選択肢を走査
for (int i = 0; i < choices.length; i++) {
// 剪定:部分集合の和がtargetを超えた場合、その選択をスキップ
if (total + choices[i] > target) {
continue;
}
// 試行:選択を行い、要素とtotalを更新
state.add(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target, total + choices[i], choices, res);
// 回退:選択を取り消し、前の状態に復元
state.remove(state.size() - 1);
}
}
/* 部分集合和 I を解く(重複する部分集合を含む) */
List<List<Integer>> subsetSumINaive(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状態(部分集合)
int total = 0; // 部分集合の和
List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)
backtrack(state, target, total, nums, res);
return res;
}
```
=== "C#"
```csharp title="subset_sum_i_naive.cs"
[class]{subset_sum_i_naive}-[func]{Backtrack}
[class]{subset_sum_i_naive}-[func]{SubsetSumINaive}
```
=== "Go"
```go title="subset_sum_i_naive.go"
[class]{}-[func]{backtrackSubsetSumINaive}
[class]{}-[func]{subsetSumINaive}
```
=== "Swift"
```swift title="subset_sum_i_naive.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "JS"
```javascript title="subset_sum_i_naive.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "TS"
```typescript title="subset_sum_i_naive.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Dart"
```dart title="subset_sum_i_naive.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Rust"
```rust title="subset_sum_i_naive.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i_naive}
```
=== "C"
```c title="subset_sum_i_naive.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Kotlin"
```kotlin title="subset_sum_i_naive.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
=== "Ruby"
```ruby title="subset_sum_i_naive.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i_naive}
```
=== "Zig"
```zig title="subset_sum_i_naive.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumINaive}
```
配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力すると、結果 $[3, 3, 3], [4, 5], [5, 4]$ が得られます。**和が $9$ のすべての部分集合を正常に見つけましたが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。
これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。
![部分集合の検索と境界外の剪定](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" }
<p align="center"> 図 13-10 &nbsp; 部分集合の検索と境界外の剪定 </p>
重複する部分集合を除去するために、**直接的なアイデアは結果リストを重複除去することです**。しかし、この方法は2つの理由で非常に非効率的です。
- 配列要素が多い場合、特に `target` が大きい場合、検索プロセスで大量の重複する部分集合が生成されます。
- 部分集合(配列)の差異を比較することは非常に時間がかかり、まず配列をソートし、次に配列の各要素の差異を比較する必要があります。
### 2. &nbsp; 重複部分集合の剪定
**剪定を通じて検索プロセス中に重複除去を検討します**。以下の図を観察すると、異なる順序で配列要素を選択するときに重複する部分集合が生成されます。例えば、以下の状況です。
1. 最初のラウンドで $3$ を選択し、2番目のラウンドで $4$ を選択すると、これら2つの要素を含むすべての部分集合が生成され、$[3, 4, \dots]$ と表記されます。
2. 後で、最初のラウンドで $4$ が選択されたとき、**2番目のラウンドは $3$ をスキップすべきです**。この選択によって生成される部分集合 $[4, 3, \dots]$ はステップ `1.` の部分集合と完全に重複するからです。
検索プロセスでは、各層の選択が左から右に一つずつ試行されるため、右側の分岐ほどより多く剪定されます。
1. 最初の2ラウンドで $3$ と $5$ を選択し、部分集合 $[3, 5, \dots]$ を生成します。
2. 最初の2ラウンドで $4$ と $5$ を選択し、部分集合 $[4, 5, \dots]$ を生成します。
3. 最初のラウンドで $5$ が選択された場合、**2番目のラウンドは $3$ と $4$ をスキップすべきです**。部分集合 $[5, 3, \dots]$ と $[5, 4, \dots]$ はステップ `1.` と `2.` で記述された部分集合と完全に重複するからです。
![異なる選択順序による重複部分集合](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" }
<p align="center"> 図 13-11 &nbsp; 異なる選択順序による重複部分集合 </p>
要約すると、入力配列 $[x_1, x_2, \dots, x_n]$ が与えられた場合、検索プロセスでの選択シーケンスは $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ であるべきで、$i_1 \leq i_2 \leq \dots \leq i_m$ を満たす必要があります。**この条件を満たさない選択シーケンスは重複を引き起こし、剪定されるべきです**。
### 3. &nbsp; コード実装
この剪定を実装するために、変数 `start` を初期化し、これは走査の開始点を示します。**選択 $x_{i}$ を行った後、次のラウンドをインデックス $i$ から開始するように設定します**。これにより、選択シーケンスが $i_1 \leq i_2 \leq \dots \leq i_m$ を満たすことが保証され、部分集合の一意性が保証されます。
さらに、コードに以下の2つの最適化を行いました。
- 検索を開始する前に、配列 `nums` をソートします。すべての選択の走査で、**部分集合和が `target` を超えたときにループを直接終了します**。後続の要素はより大きく、それらの部分集合和は確実に `target` を超えるからです。
- 要素和変数 `total` を除去し、**`target` に対して減算を実行して要素和をカウントします**。`target` が $0$ に等しくなったとき、解を記録します。
=== "Python"
```python title="subset_sum_i.py"
def backtrack(
state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
):
"""バックトラッキングアルゴリズム:部分集合の和 I"""
# 部分集合の和が target と等しいとき、解を記録
if target == 0:
res.append(list(state))
return
# すべての選択肢を走査
# 枝刈り二:start から走査を開始して重複する部分集合の生成を避ける
for i in range(start, len(choices)):
# 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了
# これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため
if target - choices[i] < 0:
break
# 試行:選択を行い、target、start を更新
state.append(choices[i])
# 次の選択ラウンドに進む
backtrack(state, target - choices[i], choices, i, res)
# 撤回:選択を取り消し、前の状態に復元
state.pop()
def subset_sum_i(nums: list[int], target: int) -> list[list[int]]:
"""部分集合の和 I を解く"""
state = [] # 状態(部分集合)
nums.sort() # nums をソート
start = 0 # 走査の開始点
res = [] # 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res)
return res
```
=== "C++"
```cpp title="subset_sum_i.cpp"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.push_back(state);
return;
}
// すべての選択肢を走査
// 剪定二:startから走査を開始し、重複する部分集合の生成を回避
for (int i = start; i < choices.size(); i++) {
// 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 試行:選択を行い、target、startを更新
state.push_back(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i, res);
// 回退:選択を取り消し、前の状態に復元
state.pop_back();
}
}
/* 部分集合和 I を解く */
vector<vector<int>> subsetSumI(vector<int> nums, int target) {
vector<int> state; // 状態(部分集合)
sort(nums.begin(), nums.end()); // nums をソート
int start = 0; // 走査の開始点
vector<vector<int>> res; // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "Java"
```java title="subset_sum_i.java"
/* バックトラッキングアルゴリズム:部分集合和 I */
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// すべての選択肢を走査
// 剪定二:startから走査を開始し、重複する部分集合の生成を回避
for (int i = start; i < choices.length; i++) {
// 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 試行:選択を行い、target、startを更新
state.add(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i, res);
// 回退:選択を取り消し、前の状態に復元
state.remove(state.size() - 1);
}
}
/* 部分集合和 I を解く */
List<List<Integer>> subsetSumI(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状態(部分集合)
Arrays.sort(nums); // nums をソート
int start = 0; // 走査の開始点
List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "C#"
```csharp title="subset_sum_i.cs"
[class]{subset_sum_i}-[func]{Backtrack}
[class]{subset_sum_i}-[func]{SubsetSumI}
```
=== "Go"
```go title="subset_sum_i.go"
[class]{}-[func]{backtrackSubsetSumI}
[class]{}-[func]{subsetSumI}
```
=== "Swift"
```swift title="subset_sum_i.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "JS"
```javascript title="subset_sum_i.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "TS"
```typescript title="subset_sum_i.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Dart"
```dart title="subset_sum_i.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Rust"
```rust title="subset_sum_i.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i}
```
=== "C"
```c title="subset_sum_i.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Kotlin"
```kotlin title="subset_sum_i.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
=== "Ruby"
```ruby title="subset_sum_i.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_i}
```
=== "Zig"
```zig title="subset_sum_i.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumI}
```
以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。
![部分集合和 I のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" }
<p align="center"> 図 13-12 &nbsp; 部分集合和 I のバックトラッキングプロセス </p>
## 13.3.2 &nbsp; 重複要素がある場合を考慮
!!! question
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は一度だけ選択できます**。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
前の問題と比較して、**この問題の入力配列には重複要素が含まれる可能性があり**、新しい問題が導入されます。例えば、配列 $[4, \hat{4}, 5]$ とターゲット要素 $9$ が与えられた場合、既存のコードの出力結果は $[4, 5], [\hat{4}, 5]$ となり、重複する部分集合が生成されます。
**この重複の理由は、特定のラウンドで等しい要素が複数回選択されることです**。以下の図では、最初のラウンドに3つの選択肢があり、そのうち2つが $4$ であり、2つの重複する検索分岐を生成し、重複する部分集合を出力します。同様に、2番目のラウンドの2つの $4$ も重複する部分集合を生成します。
![等しい要素による重複部分集合](subset_sum_problem.assets/subset_sum_ii_repeat.png){ class="animation-figure" }
<p align="center"> 図 13-13 &nbsp; 等しい要素による重複部分集合 </p>
### 1. &nbsp; 等値要素の剪定
この問題を解決するために、**等しい要素がラウンドごとに一度だけ選択されるように制限する必要があります**。実装は非常に巧妙です:配列がソートされているため、等しい要素は隣接しています。これは、特定のラウンドの選択で、現在の要素がその左側の要素と等しい場合、それはすでに選択されていることを意味するため、現在の要素を直接スキップします。
同時に、**この問題では各配列要素は一度だけ選択できると規定されています**。幸い、変数 `start` を使用してこの制約も満たすことができます:選択 $x_{i}$ を行った後、次のラウンドをインデックス $i + 1$ から前方に開始するように設定します。これにより、重複する部分集合が除去されるだけでなく、要素の重複選択も回避されます。
### 2. &nbsp; コード実装
=== "Python"
```python title="subset_sum_ii.py"
def backtrack(
state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
):
"""バックトラッキングアルゴリズム:部分集合の和 II"""
# 部分集合の和が target と等しいとき、解を記録
if target == 0:
res.append(list(state))
return
# すべての選択肢を走査
# 枝刈り二:start から走査を開始して重複する部分集合の生成を避ける
# 枝刈り三:start から走査を開始して同じ要素の重複選択を避ける
for i in range(start, len(choices)):
# 枝刈り一:部分集合の和が target を超える場合、直ちにループを終了
# これは配列がソートされており、後の要素がより大きいため、部分集合の和は必ず target を超えるため
if target - choices[i] < 0:
break
# 枝刈り四:要素が左の要素と等しい場合、検索分岐が重複していることを示すため、スキップ
if i > start and choices[i] == choices[i - 1]:
continue
# 試行:選択を行い、target、start を更新
state.append(choices[i])
# 次の選択ラウンドに進む
backtrack(state, target - choices[i], choices, i + 1, res)
# 撤回:選択を取り消し、前の状態に復元
state.pop()
def subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:
"""部分集合の和 II を解く"""
state = [] # 状態(部分集合)
nums.sort() # nums をソート
start = 0 # 走査の開始点
res = [] # 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res)
return res
```
=== "C++"
```cpp title="subset_sum_ii.cpp"
/* バックトラッキングアルゴリズム:部分集合和 II */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.push_back(state);
return;
}
// すべての選択肢を走査
// 剪定二:startから走査を開始し、重複する部分集合の生成を回避
// 剪定三:startから走査を開始し、同じ要素の繰り返し選択を回避
for (int i = start; i < choices.size(); i++) {
// 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 試行:選択を行い、target、startを更新
state.push_back(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:選択を取り消し、前の状態に復元
state.pop_back();
}
}
/* 部分集合和 II を解く */
vector<vector<int>> subsetSumII(vector<int> nums, int target) {
vector<int> state; // 状態(部分集合)
sort(nums.begin(), nums.end()); // nums をソート
int start = 0; // 走査の開始点
vector<vector<int>> res; // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "Java"
```java title="subset_sum_ii.java"
/* バックトラッキングアルゴリズム:部分集合和 II */
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 部分集合の和がtargetと等しいとき、解を記録
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// すべての選択肢を走査
// 剪定二:startから走査を開始し、重複する部分集合の生成を回避
// 剪定三:startから走査を開始し、同じ要素の繰り返し選択を回避
for (int i = start; i < choices.length; i++) {
// 剪定一:部分集合の和がtargetを超えた場合、即座にループを終了
// 配列がソートされているため、後の要素はさらに大きく、部分集合の和は必ずtargetを超える
if (target - choices[i] < 0) {
break;
}
// 剪定四:要素が左の要素と等しい場合、検索ブランチの重複を示すのでスキップ
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 試行:選択を行い、target、startを更新
state.add(choices[i]);
// 次のラウンドの選択に進む
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:選択を取り消し、前の状態に復元
state.remove(state.size() - 1);
}
}
/* 部分集合和 II を解く */
List<List<Integer>> subsetSumII(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状態(部分集合)
Arrays.sort(nums); // nums をソート
int start = 0; // 走査の開始点
List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合リスト)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "C#"
```csharp title="subset_sum_ii.cs"
[class]{subset_sum_ii}-[func]{Backtrack}
[class]{subset_sum_ii}-[func]{SubsetSumII}
```
=== "Go"
```go title="subset_sum_ii.go"
[class]{}-[func]{backtrackSubsetSumII}
[class]{}-[func]{subsetSumII}
```
=== "Swift"
```swift title="subset_sum_ii.swift"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "JS"
```javascript title="subset_sum_ii.js"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "TS"
```typescript title="subset_sum_ii.ts"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Dart"
```dart title="subset_sum_ii.dart"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Rust"
```rust title="subset_sum_ii.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_ii}
```
=== "C"
```c title="subset_sum_ii.c"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Kotlin"
```kotlin title="subset_sum_ii.kt"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
=== "Ruby"
```ruby title="subset_sum_ii.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{subset_sum_ii}
```
=== "Zig"
```zig title="subset_sum_ii.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{subsetSumII}
```
以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。
![部分集合和 II のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" }
<p align="center"> 図 13-14 &nbsp; 部分集合和 II のバックトラッキングプロセス </p>
+27
View File
@@ -0,0 +1,27 @@
---
comments: true
---
# 13.5 &nbsp; まとめ
### 1. &nbsp; 重要な復習
- バックトラッキングアルゴリズムの本質は全数探索です。解空間の深さ優先走査を実行することで条件を満たす解を求めます。検索中に満足のいく解が見つかった場合、それを記録し、すべての解が見つかるか走査が完了するまで続けます。
- バックトラッキングアルゴリズムの検索プロセスには試行と後退が含まれます。深さ優先探索を使用して様々な選択を探索し、選択が制約を満たさない場合、前の選択を取り消します。そして前の状態に戻って他のオプションを試し続けます。試行と後退は反対方向の操作です。
- バックトラッキング問題には通常複数の制約が含まれます。これらの制約は剪定操作を実行するために使用できます。剪定は不要な検索分岐を事前に終了し、検索効率を大幅に向上させることができます。
- バックトラッキングアルゴリズムは主に検索問題と制約満足問題を解決するために使用されます。組み合わせ最適化問題はバックトラッキングを使用して解決できますが、多くの場合、より効率的または効果的な解決方法が利用可能です。
- 順列問題は、与えられた集合の要素のすべての可能な順列を検索することを目的とします。各要素が選択されたかどうかを記録するために配列を使用し、同じ要素の重複選択を避けます。これにより、各要素が一度だけ選択されることが保証されます。
- 順列問題では、集合に重複要素が含まれている場合、最終結果に重複順列が含まれます。同一要素が各ラウンドで一度だけ選択できるように制限する必要があり、これは通常ハッシュセットを使用して実装されます。
- 部分集合和問題は、与えられた集合でターゲット値に合計する全ての部分集合を見つけることを目的とします。集合は要素の順序を区別しませんが、検索プロセスでは重複する部分集合が生成される可能性があります。これは、アルゴリズムが異なる要素順序を独特のパスとして探索するために発生します。バックトラッキングの前に、データをソートし、各ラウンドの走査の開始点を示す変数を設定します。これにより、重複する部分集合を生成する検索分岐を剪定できます。
- 部分集合和問題では、配列内の等しい要素は重複集合を生成する可能性があります。配列がすでにソートされているという前提条件を使用して、隣接する要素が等しいかどうかを判定することで剪定を行います。これにより、等しい要素がラウンドごとに一度だけ選択されることが保証されます。
- $n$ クイーン問題は、2つのクイーンが互いに攻撃できないように $n \times n$ のチェスボードに $n$ 個のクイーンを配置する方案を見つけることを目的とします。問題の制約には行制約、列制約、および主対角線と副対角線の制約が含まれます。行制約を満たすために、行ごとに1つのクイーンを配置する戦略を採用し、各行に1つのクイーンが配置されることを保証します。
- 列制約と対角線制約の処理は似ています。列制約については、各列にクイーンがあるかどうかを記録する配列を使用し、選択されたセルが合法かどうかを示します。対角線制約については、2つの配列を使用して主対角線と副対角線にそれぞれクイーンの存在を記録します。課題は、同じ主対角線または副対角線上のセルの行と列のインデックス間の関係を決定することです。
### 2. &nbsp; Q & A
**Q**: バックトラッキングと再帰の関係をどのように理解すればよいですか?
全体的に、バックトラッキングは「アルゴリズム戦略」であり、再帰はより「ツール」です。
- バックトラッキングアルゴリズムは通常再帰に基づいています。しかし、バックトラッキングは再帰の応用シナリオの一つであり、特に検索問題においてです。
- 再帰の構造は「部分問題分解」の問題解決パラダイムを反映します。分割統治、バックトラッキング、動的プログラミング(メモ化再帰)を含む問題の解決でよく使用されます。