mirror of
https://github.com/krahets/hello-algo.git
synced 2026-11-04 04:02:48 +08:00
Retranslate the complete Japanese online edition from Chinese (#2008)
* Retranslate the complete Japanese online edition from Chinese * Fit Japanese homepage labels and localize heading permalinks * Point Japanese contribution instructions to the Japanese preview * Replace locale conditionals with multilingual UI tables
This commit is contained in:
@@ -1,126 +1,126 @@
|
||||
# バックトラッキングアルゴリズム
|
||||
|
||||
<u>バックトラッキングアルゴリズム(backtracking algorithm)</u>は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。
|
||||
<u>バックトラッキングアルゴリズム(backtracking algorithm)</u>は、総当たりで問題を解く方法です。初期状態から出発し、可能な解をすべて探索して、条件に合う解が見つかれば記録します。解が見つかるか、すべての選択肢を試しても解がないとわかるまで続けることが、その基本的な考え方です。
|
||||
|
||||
バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。
|
||||
通常は「深さ優先探索」で解の空間を走査します。「二分木」の章では、行きがけ順、通りがけ順、帰りがけ順の走査が、いずれも深さ優先探索に属すると説明しました。ここでは行きがけ順走査を使ってバックトラッキングの問題を作り、仕組みを段階的に理解していきます。
|
||||
|
||||
!!! question "例題1"
|
||||
!!! question "例題 1"
|
||||
|
||||
1 本の二分木が与えられたとき、値が $7$ のノードをすべて探索して記録し、そのノードのリストを返してください。
|
||||
二分木が与えられます。値が $7$ のノードをすべて探して記録し、ノードのリストを返してください。
|
||||
|
||||
この問題では、この木を前順走査し、現在のノードの値が $7$ かどうかを判定します。該当する場合は、そのノードの値を結果リスト `res` に追加します。関連する処理は下図と次のコードのとおりです。
|
||||
この問題では、木を行きがけ順に走査し、現在のノードの値が $7$ かを調べます。そうなら、そのノードの値を結果リスト `res` に追加します。処理の流れと実装を、下図と次のコードに示します。
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## 試行と戻る
|
||||
## 試行と後戻り
|
||||
|
||||
**バックトラッキングアルゴリズムと呼ばれるのは、解空間を探索する際に「試行」と「戻る」という戦略を取るためです**。探索中に、ある状態から先へ進めない、または条件を満たす解を得られないと分かった場合、アルゴリズムは直前の選択を取り消して前の状態へ戻り、別の選択肢を試します。
|
||||
**バックトラッキングと呼ぶのは、解の空間を探す際に「試行」と「後戻り」を行うためです**。探索中に、それ以上進めない状態や、条件を満たす解が得られない状態に出会うと、直前の選択を取り消して元の状態に戻り、ほかの可能な選択肢を試します。
|
||||
|
||||
例題1では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る `return` は「戻る」を表します。
|
||||
例題 1 では、各ノードへの訪問が一つの「試行」に対応し、葉ノードの先へ進んだときや、親ノードへ戻るときの `return` が「後戻り」に対応します。
|
||||
|
||||
ここで強調しておきたいのは、**戻るとは関数の return だけを指すわけではない**という点です。これを説明するために、例題1を少し拡張します。
|
||||
ただし、**後戻りは単に関数から戻ることだけではありません**。これを説明するため、例題 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>"
|
||||

|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||

|
||||
|
||||
## 枝刈り
|
||||
|
||||
複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。**制約条件は多くの場合「枝刈り」に利用できます**。
|
||||
複雑なバックトラッキングの問題には、一つ以上の制約条件があります。**制約条件は通常、「枝刈り」に使えます**。
|
||||
|
||||
!!! question "例題3"
|
||||
!!! question "例題 3"
|
||||
|
||||
二分木の中で値が $7$ のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。**ただし、経路には値が $3$ のノードを含めてはいけません**。
|
||||
二分木で値が $7$ のノードをすべて探し、根ノードからそれらまでの経路を返してください。ただし、**経路に値が $3$ のノードを含めてはいけません**。
|
||||
|
||||
上の制約条件を満たすために、**枝刈り操作を追加する必要があります**。探索中に値が $3$ のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。
|
||||
この制約を満たすため、**枝刈りの操作を加えます**。探索中に値が $3$ のノードに出会ったら、そこで戻り、それ以上探索しません。コードは次のとおりです。
|
||||
|
||||
```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)) {
|
||||
@@ -129,14 +129,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for (Choice choice : choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
void backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
@@ -155,14 +155,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for (Choice choice : choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
void Backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// 解かどうかを判定
|
||||
if (IsSolution(state)) {
|
||||
@@ -181,14 +181,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
foreach (Choice choice in choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (IsValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
MakeChoice(state, choice);
|
||||
Backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
UndoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
func backtrack(state *State, choices []Choice, res *[]State) {
|
||||
// 解かどうかを判定
|
||||
if isSolution(state) {
|
||||
@@ -207,14 +207,14 @@
|
||||
// これ以上探索しない
|
||||
return
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for _, choice := range choices {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if isValid(state, choice) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
|
||||
// 解かどうかを判定
|
||||
if isSolution(state: state) {
|
||||
@@ -233,14 +233,14 @@
|
||||
// これ以上探索しない
|
||||
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,7 +250,7 @@
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
function backtrack(state, choices, res) {
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
@@ -259,14 +259,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for (let choice of choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
function backtrack(state: State, choices: Choice[], res: State[]): void {
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
@@ -285,14 +285,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for (let choice of choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -302,7 +302,7 @@
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
void backtrack(State state, List<Choice>, List<State> res) {
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
@@ -311,14 +311,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for (Choice choice in choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
fn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {
|
||||
// 解かどうかを判定
|
||||
if is_solution(state) {
|
||||
@@ -337,14 +337,14 @@
|
||||
// これ以上探索しない
|
||||
return;
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for choice in choices {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if is_valid(state, choice) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
make_choice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undo_choice(state, choice);
|
||||
}
|
||||
}
|
||||
@@ -354,7 +354,7 @@
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
@@ -363,14 +363,14 @@
|
||||
// これ以上探索しない
|
||||
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,7 +380,7 @@
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* バックトラッキングアルゴリズムのフレームワーク */
|
||||
/* バックトラッキングの基本形 */
|
||||
fun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {
|
||||
// 解かどうかを判定
|
||||
if (isSolution(state)) {
|
||||
@@ -389,14 +389,14 @@
|
||||
// これ以上探索しない
|
||||
return
|
||||
}
|
||||
// すべての選択肢を走査
|
||||
// すべての選択肢を調べる
|
||||
for (choice in choices) {
|
||||
// 枝刈り: 選択が妥当かを判定
|
||||
// 枝刈り:選択が有効かを判定
|
||||
if (isValid(state, choice)) {
|
||||
// 試行: 選択を行い、状態を更新
|
||||
// 試行:選択して状態を更新
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// 戻る: 選択を取り消し、前の状態に戻す
|
||||
// 後戻り:選択を取り消し、前の状態に戻す
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
@@ -406,7 +406,7 @@
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
### バックトラッキングアルゴリズムのフレームワーク ###
|
||||
### バックトラッキングの基本形 ###
|
||||
def backtrack(state, choices, res)
|
||||
# 解かどうかを判定
|
||||
if is_solution?(state)
|
||||
@@ -415,89 +415,89 @@
|
||||
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 |
|
||||
| 用語 | 定義 | 例題 3 |
|
||||
| ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| 解(solution) | 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る | 根ノードからノード $7$ までの、制約条件を満たすすべての経路 |
|
||||
| 制約条件(constraint) | 解の実現可能性を制限する条件であり、通常は枝刈りに用いられる | 経路にノード $3$ を含まないこと |
|
||||
| 状態(state) | ある時点における問題の状況を表し、すでに行った選択を含む | 現在までに訪問したノードの経路、すなわち `path` ノードリスト |
|
||||
| 試行(attempt) | 利用可能な選択肢にもとづいて解空間を探索する過程であり、選択、状態更新、解判定を含む | 左右の子ノードを再帰的に訪問し、ノードを `path` に追加し、値が $7$ か判定する |
|
||||
| 戻る(backtracking) | 制約条件を満たさない状態に出会ったとき、それまでの選択を取り消して前の状態へ戻ること | 葉ノードを越えたとき、ノード訪問を終えたとき、値が $3$ のノードに出会ったときに探索を終了し、関数から戻る |
|
||||
| 枝刈り(pruning) | 問題の性質や制約条件にもとづき、無意味な探索経路を避ける方法であり、探索効率を高める | 値が $3$ のノードに出会ったら、それ以上探索しない |
|
||||
| 解(solution) | 問題の特定の条件を満たす答え。一つの場合も複数の場合もある | 根ノードからノード $7$ までの、制約を満たすすべての経路 |
|
||||
| 制約条件(constraint) | 解が実行可能であるための条件。通常は枝刈りに使う | 経路にノード $3$ を含めない |
|
||||
| 状態(state) | それまでの選択を含む、ある時点での問題の状況 | 現在までに訪問した経路、すなわちノードのリスト `path` |
|
||||
| 試行(attempt) | 利用可能な選択肢で解の空間を探索する過程。選択、状態更新、解の判定を含む | 左(右)の子ノードを再帰的に訪問し、`path` に追加して、値が $7$ かを調べる |
|
||||
| 後戻り(backtracking) | 制約を満たさない状態に出会ったとき、前の選択を取り消して一つ前の状態へ戻ること | 葉ノードの先へ進んだとき、訪問を終えたとき、値が $3$ のノードに出会ったときに探索を終えて関数から戻る |
|
||||
| 枝刈り(pruning) | 問題の性質や制約を使って無意味な探索経路を避け、効率を高める方法 | 値が $3$ のノードに出会ったら、それ以上探索しない |
|
||||
|
||||
!!! tip
|
||||
|
||||
問題、解、状態などの概念は汎用的であり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも共通して現れます。
|
||||
問題、解、状態などの概念は共通のもので、分割統治、バックトラッキング、動的計画法、貪欲法などにも登場します。
|
||||
|
||||
## 利点と限界
|
||||
## 利点と制約
|
||||
|
||||
バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。
|
||||
バックトラッキングは本質的に深さ優先探索であり、条件を満たす解が見つかるまで、可能な候補をすべて試します。すべての解を見つけられることが利点であり、適切な枝刈りをすれば高い効率を得られます。
|
||||
|
||||
しかし、大規模または複雑な問題を扱う場合、**バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります**。
|
||||
ただし、大規模または複雑な問題では、**実行時間が許容できないほど長くなる可能性があります**。
|
||||
|
||||
- **時間**:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。
|
||||
- **空間**:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。
|
||||
- **時間**:状態空間のすべての可能性を調べる必要があることが多く、時間計算量は指数や階乗のオーダーになる場合があります。
|
||||
- **空間**:再帰呼び出しでは現在の状態(経路や枝刈り用の補助変数など)を保持するため、深さが大きいと必要な空間も大きくなる場合があります。
|
||||
|
||||
それでもなお、**バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です**。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。
|
||||
それでも、**一部の探索問題や制約充足問題には、バックトラッキングが最も適した解法です**。どの選択が有効な解につながるか予測できないため、すべての選択肢を調べる必要があるからです。この場合、**重要なのは効率をどう高めるか**であり、代表的な方法には二つあります。
|
||||
|
||||
- **枝刈り**:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。
|
||||
- **ヒューリスティック探索**:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。
|
||||
- **枝刈り**:解が得られないと確実にわかる経路を探索せず、時間と空間を節約します。
|
||||
- **ヒューリスティック探索**:戦略や推定値を取り入れ、有効な解が得られそうな経路を優先して探索します。
|
||||
|
||||
## バックトラッキングの典型例題
|
||||
## バックトラッキングの代表的な問題
|
||||
|
||||
バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。
|
||||
バックトラッキングは、多くの探索問題、制約充足問題、組合せ最適化問題に使えます。
|
||||
|
||||
**探索問題**:この種の問題の目標は、特定の条件を満たす解を見つけることです。
|
||||
**探索問題**:特定の条件を満たす解を見つけることが目標です。
|
||||
|
||||
- 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。
|
||||
- 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。
|
||||
- ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。
|
||||
- 順列列挙問題:集合が与えられ、その要素の可能な並べ方をすべて求めます。
|
||||
- 部分集合和問題:集合と目標の和が与えられ、和が目標に一致するすべての部分集合を求めます。
|
||||
- ハノイの塔の問題:3 本の柱と大きさの異なる円盤があり、すべての円盤を一つの柱から別の柱へ移します。一度に動かせるのは 1 枚だけで、小さな円盤の上に大きな円盤を置いてはいけません。
|
||||
|
||||
**制約充足問題**:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。
|
||||
**制約充足問題**:すべての制約条件を満たす解を見つけることが目標です。
|
||||
|
||||
- $n$ クイーン問題:$n \times n$ の盤面に $n$ 個のクイーンを配置し、互いに攻撃し合わないようにする。
|
||||
- 数独:$9 \times 9$ のグリッドに数字 $1$ ~ $9$ を入れ、各行・各列・各 $3 \times 3$ の小区画で数字が重複しないようにする。
|
||||
- グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。
|
||||
- $n$ クイーン:$n \times n$ の盤面に $n$ 個のクイーンを、互いに攻撃できないように配置します。
|
||||
- 数独:$9 \times 9$ のマスに $1$ ~ $9$ を入れ、各行、各列、各 $3 \times 3$ の領域で数字が重複しないようにします。
|
||||
- グラフ彩色問題:無向グラフの各頂点を、隣接する頂点が同じ色にならないよう、最少の色数で塗ります。
|
||||
|
||||
**組合せ最適化問題**:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。
|
||||
**組合せ最適化問題**:組合せの空間から、条件を満たす最適解を見つけることが目標です。
|
||||
|
||||
- 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。
|
||||
- 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。
|
||||
- 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。
|
||||
- 0-1 ナップサック問題:価値と重さを持つ品物の集合とナップサックが与えられ、容量の制限内で総価値が最大になるよう品物を選びます。
|
||||
- 巡回セールスマン問題:グラフのある頂点から出発し、ほかのすべての頂点をちょうど 1 回ずつ訪問して出発点に戻る、最短の経路を求めます。
|
||||
- 最大クリーク問題:無向グラフから、任意の 2 頂点の間に辺がある完全部分グラフのうち、最大のものを求めます。
|
||||
|
||||
多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。
|
||||
ただし、多くの組合せ最適化問題では、バックトラッキングが最良の解法とは限りません。
|
||||
|
||||
- 0-1 ナップサック問題は通常、より高い時間効率を得るために動的計画法で解く。
|
||||
- 巡回セールスマン問題は著名な NP-Hard 問題であり、よく用いられる解法には遺伝的アルゴリズムや蟻コロニー最適化などがある。
|
||||
- 最大クリーク問題はグラフ理論における古典的問題であり、貪欲法などのヒューリスティックで解ける。
|
||||
- 0-1 ナップサック問題は通常、時間効率の高い動的計画法で解きます。
|
||||
- 巡回セールスマン問題は有名な NP-Hard 問題で、遺伝的アルゴリズムや蟻コロニー最適化などがよく使われます。
|
||||
- 最大クリーク問題はグラフ理論の古典的な問題で、貪欲法などのヒューリスティックアルゴリズムで解けます。
|
||||
|
||||
Reference in New Issue
Block a user