mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-11 15:06:07 +00:00
Re-translate the Japanese version (#1871)
* Retranslate Japanese docs with GPT-5.4 * Retranslate Japanese code with GPT-5.4
This commit is contained in:
@@ -1,45 +1,45 @@
|
||||
# バックトラッキングアルゴリズム
|
||||
|
||||
<u>バックトラッキングアルゴリズム</u>は全数探索によって問題を解決する方法です。その核心概念は、初期状態から開始してすべての可能な解を総当たりで探索することです。アルゴリズムは正しいものを記録し、解が見つかるか、すべての可能な解が試されたが解が見つからないまで続けます。
|
||||
<u>バックトラッキングアルゴリズム(backtracking algorithm)</u>は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。
|
||||
|
||||
バックトラッキングは通常「深さ優先探索」を使用して解空間を走査します。「二分木」の章で、前順、中順、後順走査はすべて深さ優先探索であることを述べました。次に、前順走査を使用してバックトラッキング問題を解決し、アルゴリズムの動作を段階的に理解していきます。
|
||||
バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。
|
||||
|
||||
!!! question "例1"
|
||||
!!! question "例題1"
|
||||
|
||||
二分木が与えられた場合、値が $7$ のすべてのノードを検索して記録し、リストで返してください。
|
||||
1 本の二分木が与えられたとき、値が $7$ のノードをすべて探索して記録し、そのノードのリストを返してください。
|
||||
|
||||
この問題を解決するために、この木を前順で走査し、現在のノードの値が $7$ かどうかを確認します。そうであれば、ノードの値を結果リスト `res` に追加します。プロセスは以下の図に示されています:
|
||||
この問題では、この木を前順走査し、現在のノードの値が $7$ かどうかを判定します。該当する場合は、そのノードの値を結果リスト `res` に追加します。関連する処理は下図と次のコードのとおりです。
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## 試行と後退
|
||||
## 試行と戻る
|
||||
|
||||
**解空間を探索する際に「試行」と「後退」戦略を使用するため、バックトラッキングアルゴリズムと呼ばれます**。探索中、満足のいく解を得るためにもはや進めない状態に遭遇するたびに、前の選択を取り消して前の状態に戻り、次の試行のために他の可能な選択を選択できるようにします。
|
||||
**バックトラッキングアルゴリズムと呼ばれるのは、解空間を探索する際に「試行」と「戻る」という戦略を取るためです**。探索中に、ある状態から先へ進めない、または条件を満たす解を得られないと分かった場合、アルゴリズムは直前の選択を取り消して前の状態へ戻り、別の選択肢を試します。
|
||||
|
||||
例1では、各ノードの訪問が「試行」を開始します。そして葉ノードを通過するか、`return` 文で親ノードに戻ることが「後退」を示唆します。
|
||||
例題1では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る `return` は「戻る」を表します。
|
||||
|
||||
**後退は単に関数の戻り値ではないことに注意してください**。例1の問題を少し拡張して、それが何を意味するかを説明します。
|
||||
ここで強調しておきたいのは、**戻るとは関数の return だけを指すわけではない**という点です。これを説明するために、例題1を少し拡張します。
|
||||
|
||||
!!! question "例2"
|
||||
!!! question "例題2"
|
||||
|
||||
二分木で、値が $7$ のすべてのノードを検索し、すべてのマッチングノードについて、**ルートノードからそのノードまでのパスを返してください**。
|
||||
二分木の中で値が $7$ のノードをすべて探索し、**根ノードからそれらのノードまでの経路を返してください**。
|
||||
|
||||
例1のコードに基づいて、訪問したノードパスを記録するために `path` というリストを使用する必要があります。値が $7$ のノードに到達すると、`path` をコピーして結果リスト `res` に追加します。走査後、`res` にはすべての解が保持されます。コードは以下の通りです:
|
||||
例題1のコードを土台に、訪問済みノードの経路を記録するためのリスト `path` を導入します。値が $7$ のノードに到達したら、`path` をコピーして結果リスト `res` に追加します。走査が完了すると、`res` にはすべての解が保存されています。コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
各「試行」で、現在のノードを `path` に追加することでパスを記録します。「後退」が必要なときはいつでも、`path` からノードをポップして**この失敗した試行前の状態を復元します**。
|
||||
各「試行」で現在のノードを `path` に追加して経路を記録し、「戻る」前にはそのノードを `path` から取り除き、**今回の試行前の状態を復元する**必要があります。
|
||||
|
||||
以下の図に示すプロセスを観察することで、**試行は「前進」のようで、後退は「元に戻す」のようです**。後者のペアは、対応するものに対する逆操作と見なすことができます。
|
||||
次の図に示す過程を見ると、**試行と戻るは「前進」と「取り消し」として理解できます**。この 2 つの操作は互いに逆向きです。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -71,72 +71,72 @@
|
||||
=== "<11>"
|
||||

|
||||
|
||||
## 剪定
|
||||
## 枝刈り
|
||||
|
||||
複雑なバックトラッキング問題は通常1つ以上の制約を含み、**これらは「剪定」によく使用されます**。
|
||||
複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。**制約条件は多くの場合「枝刈り」に利用できます**。
|
||||
|
||||
!!! question "例3"
|
||||
!!! question "例題3"
|
||||
|
||||
二分木で、値が $7$ のすべてのノードを検索し、ルートからこれらのノードまでのパスを返してください。**ただし、パスには値が $3$ のノードを含まないという制限があります**。
|
||||
二分木の中で値が $7$ のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。**ただし、経路には値が $3$ のノードを含めてはいけません**。
|
||||
|
||||
上記の制約を満たすために、**剪定操作を追加する必要があります**:検索プロセス中に、値が $3$ のノードに遭遇した場合、そのパスを通じてさらに検索することを即座に中止します。コードは以下の通りです:
|
||||
上の制約条件を満たすために、**枝刈り操作を追加する必要があります**。探索中に値が $3$ のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
「剪定」は非常に生き生きとした名詞です。以下の図に示すように、検索プロセスで、**制約を満たさない検索分岐を「切り取り」ます**。さらなる不要な試行を避け、検索効率を向上させます。
|
||||
「枝刈り」は非常にイメージしやすい名称です。次の図のように、探索中に**制約条件を満たさない探索分岐を切り落とす**ことで、多くの無意味な試行を避け、探索効率を高められます。
|
||||
|
||||

|
||||

|
||||
|
||||
## フレームワークコード
|
||||
|
||||
今度は、バックトラッキングから「試行、後退、剪定」の主要なフレームワークを抽出して、コードの汎用性を向上させてみましょう。
|
||||
次に、バックトラッキングにおける「試行・戻る・枝刈り」の本体部分を抽出し、汎用性の高いコードフレームワークへまとめてみます。
|
||||
|
||||
以下のフレームワークコードでは、`state` は問題の現在の状態を表し、`choices` は現在の状態で利用可能な選択肢を表します:
|
||||
以下のフレームワークコードでは、`state` は問題の現在状態、`choices` はその状態で取り得る選択肢を表します。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
def backtrack(state: State, choices: list[choice], res: list[state]):
|
||||
"""バックトラッキングアルゴリズムフレームワーク"""
|
||||
# 解かどうかを確認
|
||||
"""バックトラッキングアルゴリズムのフレームワーク"""
|
||||
# 解かどうかを判定
|
||||
if is_solution(state):
|
||||
# 解を記録
|
||||
record_solution(state, res)
|
||||
# 検索を停止
|
||||
# これ以上探索しない
|
||||
return
|
||||
# すべての選択肢を反復
|
||||
# すべての選択肢を走査
|
||||
for choice in choices:
|
||||
# 剪定:選択肢が有効かどうかを確認
|
||||
# 枝刈り: 選択が妥当かを判定
|
||||
if is_valid(state, choice):
|
||||
# 試行:選択を行い、状態を更新
|
||||
# 試行: 選択を行い、状態を更新
|
||||
make_choice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
# 後退:選択を取り消し、前の状態に戻す
|
||||
# 戻る: 選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (Choice choice : choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -146,23 +146,23 @@
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
void backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (Choice choice : choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -172,23 +172,23 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
void Backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (IsSolution(state)) {
|
||||
// 解を記録
|
||||
RecordSolution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
foreach (Choice choice in choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (IsValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
MakeChoice(state, choice);
|
||||
Backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
UndoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -198,23 +198,23 @@
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
func backtrack(state *State, choices []Choice, res *[]State) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if isSolution(state) {
|
||||
// 解を記録
|
||||
recordSolution(state, res)
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for _, choice := range choices {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if isValid(state, choice) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
@@ -224,23 +224,23 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if isSolution(state: state) {
|
||||
// 解を記録
|
||||
recordSolution(state: state, res: &res)
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for choice in choices {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if isValid(state: state, choice: choice) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state: &state, choice: choice)
|
||||
backtrack(state: &state, choices: choices, res: &res)
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state: &state, choice: choice)
|
||||
}
|
||||
}
|
||||
@@ -250,23 +250,23 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
function backtrack(state, choices, res) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (let choice of choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -276,23 +276,23 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
function backtrack(state: State, choices: Choice[], res: State[]): void {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (let choice of choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -302,23 +302,23 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
void backtrack(State state, List<Choice>, List<State> res) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (Choice choice in choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -328,23 +328,23 @@
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
fn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if is_solution(state) {
|
||||
// 解を記録
|
||||
record_solution(state, res);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for choice in choices {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if is_valid(state, choice) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
make_choice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -354,23 +354,23 @@
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res, numRes);
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (int i = 0; i < numChoices; i++) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, &choices[i])) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, &choices[i]);
|
||||
backtrack(state, choices, numChoices, res, numRes);
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, &choices[i]);
|
||||
}
|
||||
}
|
||||
@@ -380,23 +380,23 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* バックトラッキングアルゴリズムフレームワーク */
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
fun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {
|
||||
// 解かどうかを確認
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
// 解を記録
|
||||
recordSolution(state, res)
|
||||
// 検索を停止
|
||||
// これ以上探索しない
|
||||
return
|
||||
}
|
||||
// すべての選択肢を反復
|
||||
// すべての選択肢を走査
|
||||
for (choice in choices) {
|
||||
// 剪定:選択肢が有効かどうかを確認
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行:選択を行い、状態を更新
|
||||
// 試行: 選択を行い、状態を更新
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// 後退:選択を取り消し、前の状態に戻す
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
@@ -406,98 +406,98 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
### バックトラッキングアルゴリズムフレームワーク ###
|
||||
### バックトラッキングアルゴリズムのフレームワーク ###
|
||||
def backtrack(state, choices, res)
|
||||
# 解かどうかを確認
|
||||
# 解かどうかを判定
|
||||
if is_solution?(state)
|
||||
# 解を記録
|
||||
record_solution(state, res)
|
||||
return
|
||||
end
|
||||
|
||||
# すべての選択肢を反復
|
||||
# すべての選択肢を走査
|
||||
for choice in choices
|
||||
# 剪定:選択肢が有効かどうかを確認
|
||||
# 枝刈り: 選択が妥当かを判定
|
||||
if is_valid?(state, choice)
|
||||
# 試行:選択を行い、状態を更新
|
||||
# 試行: 選択を行い、状態を更新
|
||||
make_choice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
# 後退:選択を取り消し、前の状態に戻す
|
||||
# 戻る: 選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
次に、フレームワークコードに基づいて例題 3 を解きます。状態 `state` はノードの走査経路を表し、選択肢 `choices` は現在ノードの左子ノードと右子ノード、結果 `res` は経路リストです:
|
||||
次に、このフレームワークコードを用いて例題3を解きます。状態 `state` はノードの走査経路、選択肢 `choices` は現在のノードの左子ノードと右子ノード、結果 `res` は経路のリストです。
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_template}-[class]{}-[func]{backtrack}
|
||||
```
|
||||
|
||||
問題文の意味に従い、値が $7$ のノードを見つけた後も探索を続ける必要があります。**したがって、解を記録した後の `return` 文を削除する必要があります**。次の図は、`return` 文を保持する場合と削除する場合の探索過程の比較です。
|
||||
問題の条件より、値が $7$ のノードを見つけた後も探索を続ける必要があります。**そのため、解を記録した後の `return` 文は削除しなければなりません**。次の図は、`return` 文を残す場合と削除する場合の探索過程を比較したものです。
|
||||
|
||||

|
||||

|
||||
|
||||
前順走査に基づくコード実装と比べると、バックトラッキングアルゴリズムのフレームワークに基づく実装はやや冗長に見えますが、汎用性はより高いです。実際、**多くのバックトラッキング問題はこのフレームワークの下で解くことができます**。具体的な問題に応じて `state` と `choices` を定義し、フレームワーク内の各メソッドを実装すればよいのです。
|
||||
前順走査にもとづく実装と比べると、バックトラッキングアルゴリズムのフレームワークにもとづく実装はやや冗長に見えますが、汎用性に優れています。実際、**多くのバックトラッキング問題はこのフレームワークで解けます**。具体的な問題に応じて `state` と `choices` を定義し、各メソッドを実装すれば十分です。
|
||||
|
||||
## よく使われる用語
|
||||
|
||||
アルゴリズム問題をより明確に分析するために、バックトラッキングアルゴリズムでよく使われる用語の意味をまとめ、例題 3 の対応例を以下の表に示します。
|
||||
アルゴリズム問題をより明確に分析するために、バックトラッキングでよく使われる用語の意味を整理し、例題3に対応する例を次の表にまとめます。
|
||||
|
||||
<p align="center"> 表 <id> バックトラッキングアルゴリズムでよく使われる用語 </p>
|
||||
<p align="center"> 表 <id> よく使われるバックトラッキング用語 </p>
|
||||
|
||||
| 名称 | 定義 | 例題 3 |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
||||
| 解(solution) | 解は問題の特定条件を満たす答えであり、1 つまたは複数存在する可能性がある | 根ノードからノード $7$ までの制約条件を満たすすべての経路 |
|
||||
| 制約条件(constraint) | 制約条件は、解の実現可能性を制限する条件であり、通常は枝刈りに使用される | 経路にノード $3$ を含まない |
|
||||
| 状態(state) | 状態は、ある時点での問題の状況を表し、これまでに行った選択を含む | 現在訪問したノード経路、すなわち `path` ノードリスト |
|
||||
| 試行(attempt) | 試行は、利用可能な選択肢に基づいて解空間を探索する過程であり、選択を行い、状態を更新し、解かどうかを確認する | 左(右)子ノードを再帰的に訪問し、ノードを `path` に追加し、ノードの値が $7$ かを確認する |
|
||||
| バックトラック(backtracking) | 制約条件を満たさない状態に遭遇した場合、以前の選択を取り消して前の状態に戻ること | 葉ノードを越えたとき、探索終了、値が $3$ のノードに遭遇したとき探索を終了し、関数が戻る |
|
||||
| 枝刈り(pruning) | 問題の特性や制約条件に基づき、無意味な探索経路を避ける方法であり、探索効率を向上させる | 値が $3$ のノードに遭遇した場合、それ以上探索しない |
|
||||
| 用語 | 定義 | 例題3 |
|
||||
| ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| 解(solution) | 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る | 根ノードからノード $7$ までの、制約条件を満たすすべての経路 |
|
||||
| 制約条件(constraint) | 解の実現可能性を制限する条件であり、通常は枝刈りに用いられる | 経路にノード $3$ を含まないこと |
|
||||
| 状態(state) | ある時点における問題の状況を表し、すでに行った選択を含む | 現在までに訪問したノードの経路、すなわち `path` ノードリスト |
|
||||
| 試行(attempt) | 利用可能な選択肢にもとづいて解空間を探索する過程であり、選択、状態更新、解判定を含む | 左右の子ノードを再帰的に訪問し、ノードを `path` に追加し、値が $7$ か判定する |
|
||||
| 戻る(backtracking) | 制約条件を満たさない状態に出会ったとき、それまでの選択を取り消して前の状態へ戻ること | 葉ノードを越えたとき、ノード訪問を終えたとき、値が $3$ のノードに出会ったときに探索を終了し、関数から戻る |
|
||||
| 枝刈り(pruning) | 問題の性質や制約条件にもとづき、無意味な探索経路を避ける方法であり、探索効率を高める | 値が $3$ のノードに出会ったら、それ以上探索しない |
|
||||
|
||||
!!! tip
|
||||
|
||||
問題、解、状態などの概念は一般的なものであり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも関係します。
|
||||
問題、解、状態などの概念は汎用的であり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも共通して現れます。
|
||||
|
||||
## 長所と限界
|
||||
## 利点と限界
|
||||
|
||||
バックトラッキングアルゴリズムは本質的に深さ優先探索(DFS)アルゴリズムの一種であり、条件を満たす解を見つけるまであらゆる可能な解を試みます。この方法の利点は、すべての可能な解を見つけられる点であり、適切な枝刈りを行えば効率が高いことです。
|
||||
バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。
|
||||
|
||||
しかし、大規模または複雑な問題を扱う場合、**バックトラッキングアルゴリズムの実行効率は許容できないほど低下する可能性があります**。
|
||||
しかし、大規模または複雑な問題を扱う場合、**バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります**。
|
||||
|
||||
- **時間**:バックトラッキングアルゴリズムは通常、状態空間のすべての可能性を探索する必要があり、時間計算量は指数オーダーまたは階乗オーダーに達する可能性があります。
|
||||
- **空間**:再帰呼び出し中に現在の状態(例:経路、枝刈り用の補助変数など)を保存する必要があり、深さが大きい場合、空間の使用量が増加します。
|
||||
- **時間**:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。
|
||||
- **空間**:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。
|
||||
|
||||
それでもなお、**バックトラッキングアルゴリズムは特定の探索問題や制約満足問題の最良の解法であることが多いです**。これらの問題では、どの選択が有効な解を生成するかを予測できないため、すべての可能な選択を試す必要があります。このような場合、**効率の最適化が鍵**となります。一般的な最適化手法は次の 2 つです。
|
||||
それでもなお、**バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です**。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。
|
||||
|
||||
- **枝刈り**:解を生成しないことが確実な経路を避けることで、時間と空間を節約します。
|
||||
- **ヒューリスティック探索**:探索中に戦略や評価値を導入し、有効な解を生成する可能性が高い経路を優先的に探索します。
|
||||
- **枝刈り**:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。
|
||||
- **ヒューリスティック探索**:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。
|
||||
|
||||
## バックトラッキングの典型的な例題
|
||||
## バックトラッキングの典型例題
|
||||
|
||||
バックトラッキングアルゴリズムは、多くの探索問題、制約満足問題、組合せ最適化問題を解くのに使用できます。
|
||||
バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。
|
||||
|
||||
**探索問題**:この種の問題の目標は、特定の条件を満たす解を見つけることです。
|
||||
|
||||
- 全順列問題:与えられた集合のすべての可能な順列を求める。
|
||||
- 部分和問題:与えられた集合と目標和に対して、和が目標値になるすべての部分集合を求める。
|
||||
- ハノイの塔:3 本の柱と異なるサイズの円盤があり、すべての円盤を 1 本の柱から別の柱に移す。1 回に 1 枚しか動かせず、大きな円盤を小さい円盤の上に置くことはできない。
|
||||
- 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。
|
||||
- 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。
|
||||
- ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。
|
||||
|
||||
**制約満足問題**:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。
|
||||
**制約充足問題**:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。
|
||||
|
||||
- $n$ クイーン問題:$n imes n$ のチェス盤に $n$ 個のクイーンを配置し、互いに攻撃しないようにする。
|
||||
- 数独:$9 imes 9$ のグリッドに数字 $1$ \~ $9$ を入力し、各行、列、$3 imes 3$ のサブグリッドに重複がないようにする。
|
||||
- グラフ彩色問題:与えられた無向グラフに対し、隣接頂点が異なる色になるように最小限の色で彩色する。
|
||||
- $n$ クイーン問題:$n \times n$ の盤面に $n$ 個のクイーンを配置し、互いに攻撃し合わないようにする。
|
||||
- 数独:$9 \times 9$ のグリッドに数字 $1$ ~ $9$ を入れ、各行・各列・各 $3 \times 3$ の小区画で数字が重複しないようにする。
|
||||
- グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。
|
||||
|
||||
**組合せ最適化問題**:この種の問題の目標は、組合せ空間内で特定の条件を満たす最適解を見つけることです。
|
||||
**組合せ最適化問題**:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。
|
||||
|
||||
- 0-1 ナップサック問題:与えられた物品群とバックパックがあり、各物品には価値と重さが設定されている。バックパックの容量制限内で、総価値を最大化する物品の選択を求める。
|
||||
- 旅行セールスマン問題:グラフ上で、1 つの点から出発し、すべての他の点を 1 回ずつ訪問して出発点に戻る最短経路を求める。
|
||||
- 最大クリーク問題:与えられた無向グラフの中で、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。
|
||||
- 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。
|
||||
- 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。
|
||||
- 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。
|
||||
|
||||
注意すべきは、多くの組合せ最適化問題に対して、バックトラッキングが最適解法ではないということです。
|
||||
多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。
|
||||
|
||||
- 0-1 ナップサック問題は、時間効率を高めるために動的計画法がよく使用されます。
|
||||
- 旅行セールスマン問題は有名な NP-Hard 問題であり、遺伝的アルゴリズムやアントコロニーアルゴリズムなどの手法がよく使われます。
|
||||
- 最大クリーク問題はグラフ理論の古典的な問題であり、貪欲法などのヒューリスティックアルゴリズムで解くことができます。
|
||||
- 0-1 ナップサック問題は通常、より高い時間効率を得るために動的計画法で解く。
|
||||
- 巡回セールスマン問題は著名な NP-Hard 問題であり、よく用いられる解法には遺伝的アルゴリズムや蟻コロニー最適化などがある。
|
||||
- 最大クリーク問題はグラフ理論における古典的問題であり、貪欲法などのヒューリスティックで解ける。
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
!!! abstract
|
||||
|
||||
迷路の探検家のように、私たちは前進する道で障害に遭遇することがあります。
|
||||
|
||||
バックトラッキングの力は、私たちに新しく始めること、試し続けること、そして最終的に光への出口を見つけることを可能にします。
|
||||
私たちは迷宮の探検者のように、前へ進む道で困難に出会うことがあります。
|
||||
|
||||
バックトラッキングの力によってやり直しができ、試行を重ね、最後には光へ通じる出口を見つけられます。
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
# Nクイーン問題
|
||||
# n クイーン問題
|
||||
|
||||
!!! question
|
||||
|
||||
チェスのルールによると、クイーンは同じ行、列、または対角線上の駒を攻撃できます。$n$ 個のクイーンと $n \times n$ のチェスボードが与えられた場合、2つのクイーンが互いに攻撃できない配置を見つけてください。
|
||||
チェスのルールによれば、クイーンは同じ行、同じ列、または同じ斜線上にある駒を攻撃できます。$n$ 個のクイーンと $n \times n$ サイズの盤面が与えられたとき、すべてのクイーンが互いに攻撃し合わない配置を求めます。
|
||||
|
||||
以下の図に示すように、$n = 4$ の場合、2つの解があります。バックトラッキングアルゴリズムの観点から、$n \times n$ のチェスボードには $n^2$ 個のマスがあり、すべての可能な選択肢 `choices` を示しています。チェスボードの状態 `state` は、各クイーンが配置されるにつれて継続的に変化します。
|
||||
下図に示すように、$n = 4$ のとき、2 つの解を見つけることができます。バックトラッキングの観点から見ると、$n \times n$ サイズの盤面には合計 $n^2$ 個のマスがあり、これがすべての選択肢 `choices` を与えます。クイーンを 1 つずつ配置していく過程で、盤面の状態は絶えず変化し、その各時点の盤面が状態 `state` です。
|
||||
|
||||

|
||||

|
||||
|
||||
以下の図は、この問題の3つの制約を示しています:**複数のクイーンは同じ行、列、または対角線を占有できません**。対角線は主対角線 `\` と副対角線 `/` に分かれることに注意することが重要です。
|
||||
下図は本問題の 3 つの制約条件を示しています。**複数のクイーンは同じ行、同じ列、同じ対角線上に置けません**。なお、対角線には主対角線 `\` と副対角線 `/` の 2 種類があります。
|
||||
|
||||

|
||||

|
||||
|
||||
### 行ごとの配置戦略
|
||||
|
||||
クイーンの数がチェスボードの行数と等しく、どちらも $n$ であるため、**チェスボードの各行には1つのクイーンのみが配置できることが**容易に結論付けられます。
|
||||
クイーンの数と盤面の行数はいずれも $n$ なので、次の推論を容易に得られます:**盤面の各行にはクイーンを 1 つだけ配置できます**。
|
||||
|
||||
これは、行ごとの配置戦略を採用できることを意味します:最初の行から開始して、最後の行に到達するまで行ごとに1つのクイーンを配置します。
|
||||
つまり、行ごとの配置戦略を採用できます:最初の行から始めて、各行に 1 つのクイーンを配置し、最後の行まで進みます。
|
||||
|
||||
以下の図は、4クイーン問題の行ごとの配置プロセスを示しています。スペースの制限により、図は最初の行の1つの検索分岐のみを展開し、列と対角線の制約を満たさない配置を剪定します。
|
||||
下図は 4 クイーン問題における行ごとの配置過程を示しています。図の大きさの都合上、下図では 1 行目における検索分岐の 1 つだけを展開し、列制約と対角線制約を満たさない案はすべて枝刈りしています。
|
||||
|
||||

|
||||
|
||||
本質的に、**行ごとの配置戦略は剪定関数として機能し**、同じ行に複数のクイーンを配置するすべての検索分岐を除去します。
|
||||
本質的には、**行ごとの配置戦略は枝刈りとして機能します**。これにより、同じ行に複数のクイーンが現れるすべての探索分岐を回避できます。
|
||||
|
||||
### 列と対角線の剪定
|
||||
### 列と対角線の枝刈り
|
||||
|
||||
列の制約を満たすために、長さ $n$ のブール配列 `cols` を使用して、各列にクイーンが占有されているかどうかを追跡できます。各配置決定の前に、`cols` を使用してすでにクイーンがある列を剪定し、バックトラッキング中に動的に更新されます。
|
||||
列制約を満たすために、長さ $n$ のブール配列 `cols` を用いて、各列にクイーンがあるかどうかを記録できます。配置を決めるたびに、`cols` を使って既存のクイーンがある列を枝刈りし、バックトラッキングの中で `cols` の状態を動的に更新します。
|
||||
|
||||
!!! tip
|
||||
|
||||
行列の原点は左上隅にあり、行インデックスは上から下に増加し、列インデックスは左から右に増加することに注意してください。
|
||||
注意として、行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増加します。
|
||||
|
||||
対角線の制約はどうでしょうか?チェスボード上の特定のセルの行と列のインデックスを $(row, col)$ とします。特定の主対角線を選択することで、その対角線上のすべてのセルで差 $row - col$ が同じであることに気付きます。**つまり、$row - col$ は主対角線上で定数値です**。
|
||||
では、対角線制約はどのように扱えばよいのでしょうか。盤面上のあるマスの行列インデックスを $(row, col)$ とし、行列内のある主対角線を選ぶと、その対角線上のすべてのマスで行インデックスから列インデックスを引いた値が等しいことが分かります。**つまり、主対角線上のすべてのマスでは $row - col$ が一定値になります**。
|
||||
|
||||
言い換えると、2つのセルが $row_1 - col_1 = row_2 - col_2$ を満たす場合、それらは確実に同じ主対角線上にあります。このパターンを使用して、以下の図に示す配列 `diags1` を利用して、クイーンが主対角線上にあるかどうかを追跡できます。
|
||||
つまり、2 つのマスが $row_1 - col_1 = row_2 - col_2$ を満たすなら、それらは必ず同じ主対角線上にあります。この性質を利用して、下図の配列 `diags1` により、各主対角線にクイーンがあるかどうかを記録できます。
|
||||
|
||||
同様に、**$row + col$ の和は副対角線上のすべてのセルで定数値です**。配列 `diags2` を使用して副対角線の制約も処理できます。
|
||||
同様に、**副対角線上のすべてのマスでは $row + col$ が一定値です**。副対角線制約も配列 `diags2` を使って処理できます。
|
||||
|
||||

|
||||

|
||||
|
||||
### コード実装
|
||||
|
||||
$n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で、$row + col$ の範囲は $[0, 2n - 2]$ であることに注意してください。したがって、主対角線と副対角線の数はどちらも $2n - 1$ で、配列 `diags1` と `diags2` の長さは $2n - 1$ です。
|
||||
注意として、$n$ 次正方行列では $row - col$ の範囲は $[-n + 1, n - 1]$ 、$row + col$ の範囲は $[0, 2n - 2]$ です。したがって、主対角線と副対角線の本数はいずれも $2n - 1$ であり、配列 `diags1` と `diags2` の長さもともに $2n - 1$ です。
|
||||
|
||||
```src
|
||||
[file]{n_queens}-[class]{}-[func]{n_queens}
|
||||
```
|
||||
|
||||
$n$ 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、$n$、$n-1$、$\dots$、$2$、$1$ の選択肢があり、$O(n!)$ 時間を使用します。解を記録する際、行列 `state` をコピーして `res` に追加する必要があり、コピー操作は $O(n^2)$ 時間を使用します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$ です**。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。
|
||||
行ごとに $n$ 回配置し、列制約を考慮すると、1 行目から最終行までの選択肢はそれぞれ $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)$ です**。
|
||||
配列 `state` は $O(n^2)$ の空間を使用し、配列 `cols`、`diags1`、`diags2` はいずれも $O(n)$ の空間を使用します。最大再帰深さは $n$ で、スタックフレーム空間として $O(n)$ を使用します。したがって、**空間計算量は $O(n^2)$** です。
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
# 順列問題
|
||||
# 全順列問題
|
||||
|
||||
順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。
|
||||
全順列問題はバックトラッキングアルゴリズムの典型的な応用例です。これは、ある集合(配列や文字列など)が与えられたとき、その要素のあり得るすべての順列を求める問題です。
|
||||
|
||||
以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。
|
||||
下表に、入力配列とそれに対応するすべての順列から成る例をいくつか示します。
|
||||
|
||||
<p align="center"> 表 <id> 順列の例 </p>
|
||||
<p align="center"> 表 <id> 全順列の例 </p>
|
||||
|
||||
| 入力配列 | 順列 |
|
||||
| :----------- | :----------------------------------------------------------------- |
|
||||
| $[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]$ |
|
||||
| 入力配列 | すべての順列 |
|
||||
| :---------- | :----------------------------------------------------------------- |
|
||||
| $[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]$ |
|
||||
|
||||
## 重複要素がない場合
|
||||
## 等しい要素がない場合
|
||||
|
||||
!!! question
|
||||
|
||||
重複要素のない整数配列が与えられた場合、すべての可能な順列を返してください。
|
||||
重複要素を含まない整数配列を入力として受け取り、あり得るすべての順列を返します。
|
||||
|
||||
バックトラッキングの観点から、**順列を生成するプロセスを一連の選択として見ることができます。** 入力配列が $[1, 2, 3]$ だとします。最初に $1$ を選択し、次に $3$、最後に $2$ を選択すると、順列 $[1, 3, 2]$ が得られます。「バックトラッキング」は前の選択を取り消して、代替オプションを探索することを意味します。
|
||||
バックトラッキングアルゴリズムの観点から見ると、**順列生成の過程は一連の選択の結果として捉えられます**。入力配列が $[1, 2, 3]$ だとすると、最初に $1$ を選び、次に $3$ を選び、最後に $2$ を選べば、順列 $[1, 3, 2]$ が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。
|
||||
|
||||
コーディングの観点から、候補集合 `choices` は入力配列のすべての要素で構成され、`state` はこれまでに選択された要素を保持します。各要素は一度だけ選択できるため、**`state` のすべての要素は一意である必要があります**。
|
||||
バックトラッキングコードの観点では、候補集合 `choices` は入力配列中のすべての要素であり、状態 `state` は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。**したがって `state` 内の要素はすべて一意でなければなりません**。
|
||||
|
||||
以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の `state` を表します。ルートノードから開始して、3回の選択の後、葉ノードに到達します—それぞれが順列に対応します。
|
||||
下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 `state` を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。
|
||||
|
||||

|
||||

|
||||
|
||||
### 重複選択の剪定
|
||||
### 重複選択の枝刈り
|
||||
|
||||
各要素が一度だけ選択されることを保証するために、ブール配列 `selected` を導入します。ここで `selected[i]` は `choices[i]` が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します:
|
||||
各要素が 1 回しか選ばれないようにするため、ブール配列 `selected` の導入を考えます。ここで `selected[i]` は `choices[i]` がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。
|
||||
|
||||
- `choice[i]` を選択した後、`selected[i]` を $\text{True}$ に設定して選択されたとマークします。
|
||||
- `choices` を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。
|
||||
- 選択 `choice[i]` を行った後、`selected[i]` を $\text{True}$ に設定し、その要素が選択済みであることを表します。
|
||||
- 選択肢リスト `choices` を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。
|
||||
|
||||
以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。
|
||||
下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 1 と要素 3 の分岐を刈り取る必要があります。
|
||||
|
||||

|
||||

|
||||
|
||||
図から、この剪定プロセスが検索空間を $O(n^n)$ から $O(n!)$ に削減することがわかります。
|
||||
上図から、この枝刈りにより探索空間の大きさは $O(n^n)$ から $O(n!)$ へ削減されることがわかります。
|
||||
|
||||
### コード実装
|
||||
|
||||
この理解により、フレームワークコードの「空欄を埋める」ことができます。全体のコードを簡潔に保つため、フレームワークの各部分を個別に実装せず、代わりに `backtrack()` 関数ですべてを展開します:
|
||||
以上を整理できれば、フレームワークコードの「穴埋め」を行えます。全体のコードを短くするため、フレームワークコード中の各関数を個別には実装せず、これらを `backtrack()` 関数内に展開します。
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
## 重複要素を考慮する場合
|
||||
## 等しい要素を考慮する場合
|
||||
|
||||
!!! question
|
||||
|
||||
**重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。
|
||||
整数配列を入力として受け取り、**配列には重複要素が含まれる場合があります**。重複しない順列をすべて返します。
|
||||
|
||||
入力配列が $[1, 1, 2]$ だとします。2つの同一要素 $1$ を区別するために、2番目を $\hat{1}$ とラベル付けします。
|
||||
入力配列が $[1, 1, 2]$ だと仮定します。2 つの重複する要素 $1$ を区別しやすくするため、2 つ目の $1$ を $\hat{1}$ と記します。
|
||||
|
||||
以下の図に示すように、この方法で生成される順列の半分は重複です:
|
||||
下図のように、上述の方法で生成される順列の半分は重複しています。
|
||||
|
||||

|
||||

|
||||
|
||||
では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。**重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです**、これによりアルゴリズムの効率が向上します。
|
||||
では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。**なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです**。そうすることで、アルゴリズム効率をさらに高められます。
|
||||
|
||||
### 等値要素の剪定
|
||||
### 等しい要素の枝刈り
|
||||
|
||||
以下の図を見ると、最初のラウンドで $1$ または $\hat{1}$ を選択すると同じ順列につながるため、$\hat{1}$ を剪定します。
|
||||
下図を見ると、1 回目のラウンドでは $1$ を選ぶことと $\hat{1}$ を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって $\hat{1}$ を枝刈りすべきです。
|
||||
|
||||
同様に、最初のラウンドで $2$ を選択した後、2番目のラウンドで $1$ または $\hat{1}$ を選択しても重複分岐につながるため、その時も $\hat{1}$ を剪定します。
|
||||
同様に、1 回目で $2$ を選んだ後では、2 回目のラウンドにおける $1$ と $\hat{1}$ も重複分岐を生むため、2 回目の $\hat{1}$ も枝刈りすべきです。
|
||||
|
||||
本質的に、**私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。**
|
||||
本質的には、**各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です**。
|
||||
|
||||

|
||||

|
||||
|
||||
### コード実装
|
||||
|
||||
前の問題のコードに基づいて、各ラウンドでハッシュセット `duplicated` を導入します。このセットは、すでに試行した要素を追跡し、重複を剪定できるようにします:
|
||||
前問のコードを土台として、各ラウンドの選択でハッシュ集合 `duplicated` を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。
|
||||
|
||||
```src
|
||||
[file]{permutations_ii}-[class]{}-[func]{permutations_ii}
|
||||
```
|
||||
|
||||
すべての要素が異なると仮定すると、$n$ 個の要素の順列は $n!$ (階乗)個あります。各結果を記録するには長さ $n$ のリストをコピーする必要があり、これには $O(n)$ 時間がかかります。**したがって、総時間計算量は $O(n!n)$ です。**
|
||||
要素どうしがすべて互いに異なると仮定すると、$n$ 個の要素には全部で $n!$ 通りの順列(階乗)があります。結果を記録する際には、長さ $n$ のリストをコピーする必要があり、これに $O(n)$ 時間を要します。**したがって時間計算量は $O(n!n)$** です。
|
||||
|
||||
最大再帰深度は $n$ で、$O(n)$ のスタック空間を使用します。`selected` 配列も $O(n)$ 空間が必要です。一度に最大 $n$ 個の個別の `duplicated` セットが存在する可能性があるため、それらは集合的に $O(n^2)$ 空間を占有します。**したがって、空間計算量は $O(n^2)$ です。**
|
||||
再帰の最大深さは $n$ であり、$O(n)$ のスタックフレーム空間を使います。`selected` は $O(n)$ 空間を使用します。同時刻に存在する `duplicated` は最大で $n$ 個であり、$O(n^2)$ 空間を要します。**したがって空間計算量は $O(n^2)$** です。
|
||||
|
||||
### 2つの剪定方法の比較
|
||||
### 2 種類の枝刈りの比較
|
||||
|
||||
`selected` と `duplicated` はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています:
|
||||
`selected` と `duplicated` はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。
|
||||
|
||||
- **重複選択の剪定**(`selected` 経由):検索全体に単一の `selected` 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が `state` に複数回現れることを防ぎます。
|
||||
- **等値要素の剪定**(`duplicated` 経由):`backtrack` 関数の各呼び出しは独自の `duplicated` セットを使用し、その特定の反復(`for` ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。
|
||||
- **重複選択の枝刈り**:探索全体を通して `selected` は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が `state` に重複して現れるのを防ぎます。
|
||||
- **等しい要素の枝刈り**:各ラウンドの選択、すなわち各回の `backtrack` 呼び出しには `duplicated` が含まれます。これはそのラウンドの走査(`for` ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。
|
||||
|
||||
以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ノードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。
|
||||
下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
# 部分集合和問題
|
||||
# 部分和問題
|
||||
|
||||
## 重複要素がない場合
|
||||
## 重複しない要素の場合
|
||||
|
||||
!!! question
|
||||
|
||||
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。与えられた配列には重複要素がなく、各要素は複数回選択できます。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
|
||||
正整数配列 `nums` と目標の正整数 `target` が与えられたとき、要素の和が `target` に等しくなるすべての組合せを見つけてください。配列に重複要素はなく、各要素は複数回選択できます。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。
|
||||
|
||||
例えば、入力集合 $\{3, 4, 5\}$ とターゲット整数 $9$ の場合、解は $\{3, 3, 3\}, \{4, 5\}$ です。以下の2点に注意してください。
|
||||
例えば、入力集合 $\{3, 4, 5\}$ と目標整数 $9$ に対する解は $\{3, 3, 3\}, \{4, 5\}$ です。次の 2 点に注意してください。
|
||||
|
||||
- 入力集合の要素は無制限に選択できます。
|
||||
- 部分集合は要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。
|
||||
- 入力集合内の要素は何度でも繰り返し選択できます。
|
||||
- 部分集合では要素の順序を区別しません。例えば $\{4, 5\}$ と $\{5, 4\}$ は同じ部分集合です。
|
||||
|
||||
### 順列解法の参考
|
||||
### 全順列の解法を参考にする
|
||||
|
||||
順列問題と同様に、部分集合の生成を一連の選択として想像でき、選択プロセス中に「要素和」をリアルタイムで更新できます。要素和が `target` に等しくなったとき、部分集合を結果リストに記録します。
|
||||
全順列問題と同様に、部分集合の生成過程を一連の選択結果として捉え、選択の過程で「要素の和」を逐次更新できます。そして要素の和が `target` に等しくなった時点で、その部分集合を結果リストに記録します。
|
||||
|
||||
順列問題とは異なり、**この問題では要素は無制限に選択できるため**、要素が選択されたかどうかを記録するための `selected` ブール配列を使用する必要がありません。順列コードに軽微な修正を加えて、最初に問題を解決できます:
|
||||
ただし全順列問題と異なるのは、**この問題では集合内の要素を無制限に選択できる**点です。そのため、要素がすでに選択されたかどうかを記録する `selected` ブール配列は不要です。全順列のコードに少し修正を加えると、まず次の解法コードが得られます。
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i_naive}-[class]{}-[func]{subset_sum_i_naive}
|
||||
```
|
||||
|
||||
配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力すると、結果 $[3, 3, 3], [4, 5], [5, 4]$ が得られます。**和が $9$ のすべての部分集合を正常に見つけましたが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。
|
||||
上のコードに配列 $[3, 4, 5]$ と目標値 $9$ を入力すると、出力は $[3, 3, 3], [4, 5], [5, 4]$ となります。**和が $9$ となる部分集合はすべて見つかっていますが、重複する部分集合 $[4, 5]$ と $[5, 4]$ が含まれています**。
|
||||
|
||||
これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。
|
||||
これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に $4$ を選んでから $5$ を選ぶ場合と、先に $5$ を選んでから $4$ を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。
|
||||
|
||||

|
||||

|
||||
|
||||
重複する部分集合を除去するために、**直接的なアイデアは結果リストを重複除去することです**。しかし、この方法は2つの理由で非常に非効率的です。
|
||||
重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。
|
||||
|
||||
- 配列要素が多い場合、特に `target` が大きい場合、検索プロセスで大量の重複する部分集合が生成されます。
|
||||
- 部分集合(配列)の差異を比較することは非常に時間がかかり、まず配列をソートし、次に配列の各要素の差異を比較する必要があります。
|
||||
- 配列要素が多い場合、特に `target` が大きい場合には、探索過程で大量の重複部分集合が生成されます。
|
||||
- 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。
|
||||
|
||||
### 重複部分集合の剪定
|
||||
### 重複部分集合の枝刈り
|
||||
|
||||
**剪定を通じて検索プロセス中に重複除去を検討します**。以下の図を観察すると、異なる順序で配列要素を選択するときに重複する部分集合が生成されます。例えば、以下の状況です。
|
||||
**探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。
|
||||
|
||||
1. 最初のラウンドで $3$ を選択し、2番目のラウンドで $4$ を選択すると、これら2つの要素を含むすべての部分集合が生成され、$[3, 4, \dots]$ と表記されます。
|
||||
2. 後で、最初のラウンドで $4$ が選択されたとき、**2番目のラウンドは $3$ をスキップすべきです**。この選択によって生成される部分集合 $[4, 3, \dots]$ はステップ `1.` の部分集合と完全に重複するからです。
|
||||
1. 1 回目と 2 回目でそれぞれ $3$ と $4$ を選ぶと、これら 2 要素を含むすべての部分集合、すなわち $[3, 4, \dots]$ が生成されます。
|
||||
2. その後、1 回目で $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.` で記述された部分集合と完全に重複するからです。
|
||||
1. 最初の 2 回で $3$ と $5$ を選ぶと、部分集合 $[3, 5, \dots]$ が生成されます。
|
||||
2. 最初の 2 回で $4$ と $5$ を選ぶと、部分集合 $[4, 5, \dots]$ が生成されます。
|
||||
3. もし 1 回目で $5$ を選ぶなら、**2 回目では $3$ と $4$ をスキップすべき**です。なぜなら、部分集合 $[5, 3, \dots]$ と $[5, 4, \dots]$ は、手順 `1.` と手順 `2.` で述べた部分集合と完全に重複するからです。
|
||||
|
||||

|
||||

|
||||
|
||||
要約すると、入力配列 $[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$ を満たす必要があります。**この条件を満たさない選択シーケンスは重複を引き起こし、剪定されるべきです**。
|
||||
まとめると、入力配列 $[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$ を満たす必要があります。**この条件を満たさない選択列は重複を生むため、枝刈りすべきです**。
|
||||
|
||||
### コード実装
|
||||
|
||||
この剪定を実装するために、変数 `start` を初期化し、これは走査の開始点を示します。**選択 $x_{i}$ を行った後、次のラウンドをインデックス $i$ から開始するように設定します**。これにより、選択シーケンスが $i_1 \leq i_2 \leq \dots \leq i_m$ を満たすことが保証され、部分集合の一意性が保証されます。
|
||||
この枝刈りを実現するために、走査の開始位置を示す変数 `start` を初期化します。**選択 $x_{i}$ を行った後、次のラウンドはインデックス $i$ から走査を開始する**ように設定します。これにより、選択列が $i_1 \leq i_2 \leq \dots \leq i_m$ を満たし、部分集合の一意性が保証されます。
|
||||
|
||||
さらに、コードに以下の2つの最適化を行いました。
|
||||
これに加えて、コードには次の 2 つの最適化も施しています。
|
||||
|
||||
- 検索を開始する前に、配列 `nums` をソートします。すべての選択の走査で、**部分集合和が `target` を超えたときにループを直接終了します**。後続の要素はより大きく、それらの部分集合和は確実に `target` を超えるからです。
|
||||
- 要素和変数 `total` を除去し、**`target` に対して減算を実行して要素和をカウントします**。`target` が $0$ に等しくなったとき、解を記録します。
|
||||
- 探索を始める前に、まず配列 `nums` をソートします。すべての選択肢を走査するとき、**部分集合の和が `target` を超えたら直ちにループを終了**します。後続の要素はさらに大きいため、その和も必ず `target` を超えるからです。
|
||||
- 要素和を保持する変数 `total` は省略し、**`target` から減算することで要素和を管理**します。`target` が $0$ になったときに解を記録します。
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i}
|
||||
```
|
||||
|
||||
以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。
|
||||
次の図は、配列 $[3, 4, 5]$ と目標値 $9$ を上のコードに入力したときの、全体のバックトラッキング過程を示しています。
|
||||
|
||||

|
||||

|
||||
|
||||
## 重複要素がある場合を考慮
|
||||
## 重複要素を考慮する場合
|
||||
|
||||
!!! question
|
||||
|
||||
正の整数の配列 `nums` とターゲット正整数 `target` が与えられた場合、組み合わせ内の要素の和が `target` に等しくなるようなすべての可能な組み合わせを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は一度だけ選択できます**。これらの組み合わせを重複する組み合わせを含まないリストとして返してください。
|
||||
正整数配列 `nums` と目標の正整数 `target` が与えられたとき、要素の和が `target` に等しくなるすべての組合せを見つけてください。**与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません**。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。
|
||||
|
||||
前の問題と比較して、**この問題の入力配列には重複要素が含まれる可能性があり**、新しい問題が導入されます。例えば、配列 $[4, \hat{4}, 5]$ とターゲット要素 $9$ が与えられた場合、既存のコードの出力結果は $[4, 5], [\hat{4}, 5]$ となり、重複する部分集合が生成されます。
|
||||
前問と比べると、**この問題の入力配列には重複要素が含まれる可能性があります**。そのため、新たな問題が生じます。例えば、配列 $[4, \hat{4}, 5]$ と目標値 $9$ が与えられると、既存コードの出力は $[4, 5], [\hat{4}, 5]$ となり、重複部分集合が現れます。
|
||||
|
||||
**この重複の理由は、特定のラウンドで等しい要素が複数回選択されることです**。以下の図では、最初のラウンドに3つの選択肢があり、そのうち2つが $4$ であり、2つの重複する検索分岐を生成し、重複する部分集合を出力します。同様に、2番目のラウンドの2つの $4$ も重複する部分集合を生成します。
|
||||
**この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります**。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも $4$ です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの $4$ も重複部分集合を生みます。
|
||||
|
||||

|
||||

|
||||
|
||||
### 等値要素の剪定
|
||||
### 等しい要素の枝刈り
|
||||
|
||||
この問題を解決するために、**等しい要素がラウンドごとに一度だけ選択されるように制限する必要があります**。実装は非常に巧妙です:配列がソートされているため、等しい要素は隣接しています。これは、特定のラウンドの選択で、現在の要素がその左側の要素と等しい場合、それはすでに選択されていることを意味するため、現在の要素を直接スキップします。
|
||||
この問題を解決するには、**各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります**。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。
|
||||
|
||||
同時に、**この問題では各配列要素は一度だけ選択できると規定されています**。幸い、変数 `start` を使用してこの制約も満たすことができます:選択 $x_{i}$ を行った後、次のラウンドをインデックス $i + 1$ から前方に開始するように設定します。これにより、重複する部分集合が除去されるだけでなく、要素の重複選択も回避されます。
|
||||
同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 `start` を使って満たせます。すなわち、選択 $x_{i}$ を行った後、次のラウンドはインデックス $i + 1$ から後ろへ走査するよう設定します。これにより、重複部分集合を除去できるだけでなく、同じ要素を繰り返し選ぶことも防げます。
|
||||
|
||||
### コード実装
|
||||
|
||||
@@ -90,6 +90,6 @@
|
||||
[file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii}
|
||||
```
|
||||
|
||||
以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。
|
||||
次の図は、配列 $[4, 4, 5]$ と目標値 $9$ に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# まとめ
|
||||
|
||||
### 重要な復習
|
||||
### 重要なポイントの振り返り
|
||||
|
||||
- バックトラッキングアルゴリズムの本質は全数探索です。解空間の深さ優先走査を実行することで条件を満たす解を求めます。検索中に満足のいく解が見つかった場合、それを記録し、すべての解が見つかるか走査が完了するまで続けます。
|
||||
- バックトラッキングアルゴリズムの検索プロセスには試行と後退が含まれます。深さ優先探索を使用して様々な選択を探索し、選択が制約を満たさない場合、前の選択を取り消します。そして前の状態に戻って他のオプションを試し続けます。試行と後退は反対方向の操作です。
|
||||
- バックトラッキング問題には通常複数の制約が含まれます。これらの制約は剪定操作を実行するために使用できます。剪定は不要な検索分岐を事前に終了し、検索効率を大幅に向上させることができます。
|
||||
- バックトラッキングアルゴリズムは主に検索問題と制約満足問題を解決するために使用されます。組み合わせ最適化問題はバックトラッキングを使用して解決できますが、多くの場合、より効率的または効果的な解決方法が利用可能です。
|
||||
- 順列問題は、与えられた集合の要素のすべての可能な順列を検索することを目的とします。各要素が選択されたかどうかを記録するために配列を使用し、同じ要素の重複選択を避けます。これにより、各要素が一度だけ選択されることが保証されます。
|
||||
- 順列問題では、集合に重複要素が含まれている場合、最終結果に重複順列が含まれます。同一要素が各ラウンドで一度だけ選択できるように制限する必要があり、これは通常ハッシュセットを使用して実装されます。
|
||||
- 部分集合和問題は、与えられた集合でターゲット値に合計する全ての部分集合を見つけることを目的とします。集合は要素の順序を区別しませんが、検索プロセスでは重複する部分集合が生成される可能性があります。これは、アルゴリズムが異なる要素順序を独特のパスとして探索するために発生します。バックトラッキングの前に、データをソートし、各ラウンドの走査の開始点を示す変数を設定します。これにより、重複する部分集合を生成する検索分岐を剪定できます。
|
||||
- 部分集合和問題では、配列内の等しい要素は重複集合を生成する可能性があります。配列がすでにソートされているという前提条件を使用して、隣接する要素が等しいかどうかを判定することで剪定を行います。これにより、等しい要素がラウンドごとに一度だけ選択されることが保証されます。
|
||||
- $n$ クイーン問題は、2つのクイーンが互いに攻撃できないように $n \times n$ のチェスボードに $n$ 個のクイーンを配置する方案を見つけることを目的とします。問題の制約には行制約、列制約、および主対角線と副対角線の制約が含まれます。行制約を満たすために、行ごとに1つのクイーンを配置する戦略を採用し、各行に1つのクイーンが配置されることを保証します。
|
||||
- 列制約と対角線制約の処理は似ています。列制約については、各列にクイーンがあるかどうかを記録する配列を使用し、選択されたセルが合法かどうかを示します。対角線制約については、2つの配列を使用して主対角線と副対角線にそれぞれクイーンの存在を記録します。課題は、同じ主対角線または副対角線上のセルの行と列のインデックス間の関係を決定することです。
|
||||
- バックトラッキングアルゴリズムの本質は全探索法であり、解空間を深さ優先で走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解を見つけるか探索が完了するまで続けます。
|
||||
- バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。
|
||||
- バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な探索分岐を早期に打ち切り、探索効率を大幅に高められます。
|
||||
- バックトラッキングアルゴリズムは主に探索問題と制約充足問題の解決に用いられます。組合せ最適化問題もバックトラッキングで解けますが、より高効率またはより適した解法が存在することが少なくありません。
|
||||
- 全順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素を重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。
|
||||
- 全順列問題では、集合内に重複要素があると最終結果にも重複した順列が現れます。各ラウンドで等しい要素は 1 回しか選べないように制約する必要があり、通常はハッシュ集合を用いて実現します。
|
||||
- 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることです。集合では要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する探索分岐を枝刈りします。
|
||||
- 部分和問題では、配列中の等しい要素が重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素が各ラウンドで 1 回しか選ばれないようにします。
|
||||
- $n$ クイーン問題の目的は、$n \times n$ の盤面に $n$ 個のクイーンを配置する方法を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。
|
||||
- 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれにクイーンが存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: バックトラッキングと再帰の関係をどのように理解すればよいですか?
|
||||
**Q**:バックトラッキングと再帰の関係はどのように理解すればよいですか?
|
||||
|
||||
全体的に、バックトラッキングは「アルゴリズム戦略」であり、再帰はより「ツール」です。
|
||||
全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。
|
||||
|
||||
- バックトラッキングアルゴリズムは通常再帰に基づいています。しかし、バックトラッキングは再帰の応用シナリオの一つであり、特に検索問題においてです。
|
||||
- 再帰の構造は「部分問題分解」の問題解決パラダイムを反映します。分割統治、バックトラッキング、動的プログラミング(メモ化再帰)を含む問題の解決でよく使用されます。
|
||||
- バックトラッキングアルゴリズムは通常、再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、探索問題における再帰の応用です。
|
||||
- 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。
|
||||
|
||||
Reference in New Issue
Block a user