12 KiB
comments
| comments |
|---|
| true |
12.4 ハノイの塔問題
マージソートと二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハノイの塔では、異なる分解戦略を採用します。
!!! question
3つの柱があり、それぞれ `A`、`B`、`C` と表記されます。最初、柱 `A` には $n$ 枚の円盤があり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの $n$ 枚の円盤を柱 `C` に移動し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます:
1. 円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。
2. 一度に移動できるのは1枚の円盤のみです。
3. 小さい円盤は常に大きい円盤の上にある必要があります。
図 12-10 ハノイの塔の例
サイズ i のハノイの塔問題を f(i) と表記します。例えば、f(3) は3枚の円盤を柱 A から柱 C に移動することを表します。
1. 基本ケースを考える
以下の図に示すように、問題 $f(1)$(円盤が1枚のみ)については、A から C に直接移動できます。
=== "<1>"
{ class="animation-figure" }
=== "<2>"
{ class="animation-figure" }
図 12-11 サイズ1の問題の解
$f(2)$(円盤が2枚)については、柱 B の助けを借りて小さい円盤を大きい円盤の上に保つ必要があります。以下の図に示すように:
- まず、小さい円盤を
AからBに移動します。 - 次に、大きい円盤を
AからCに移動します。 - 最後に、小さい円盤を
BからCに移動します。
=== "<1>"
{ class="animation-figure" }
=== "<2>"
{ class="animation-figure" }
=== "<3>"
{ class="animation-figure" }
=== "<4>"
{ class="animation-figure" }
図 12-12 サイズ2の問題の解
f(2) を解決する過程は次のように要約できます:B の助けを借りて2枚の円盤を A から C に移動する。ここで、C をターゲット柱、B をバッファ柱と呼びます。
2. 部分問題の分解
問題 $f(3)$(つまり、円盤が3枚の場合)については、状況がやや複雑になります。
すでに f(1) と f(2) の解が分かっているので、分割統治の観点を採用し、A の上の2枚の円盤を1つの単位として扱い、以下の図に示すステップを実行できます。これにより、3枚の円盤を A から C に正常に移動できます。
Bをターゲット柱、Cをバッファ柱として、2枚の円盤をAからBに移動します。- 残りの円盤を
Aから直接Cに移動します。 Cをターゲット柱、Aをバッファ柱として、2枚の円盤をBからCに移動します。
=== "<1>"
{ class="animation-figure" }
=== "<2>"
{ class="animation-figure" }
=== "<3>"
{ class="animation-figure" }
=== "<4>"
{ class="animation-figure" }
図 12-13 サイズ3の問題の解
本質的に、f(3) を2つの f(2) 部分問題と1つの f(1) 部分問題に分解します。これら3つの部分問題を順次解決することで、元の問題が解決され、部分問題が独立しており、それらの解をマージできることを示しています。
ここから、以下の図に示すハノイの塔の分割統治戦略を要約できます。元の問題 f(n) を2つの部分問題 f(n-1) と1つの部分問題 f(1) に分割し、以下の順序でこれら3つの部分問題を解決します:
Cをバッファとして使用し、n-1枚の円盤をAからBに移動します。- 残りの円盤を
Aから直接Cに移動します。 Aをバッファとして使用し、n-1枚の円盤をBからCに移動します。
各 f(n-1) 部分問題について、同じ再帰分割を適用でき、最小の部分問題 f(1) に到達するまで続けます。f(1) は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。
図 12-14 ハノイの塔を解決するための分割統治戦略
3. コード実装
コードでは、再帰関数 dfs(i, src, buf, tar) を定義します。これは柱 src から上の i 枚の円盤を柱 tar に移動し、柱 buf をバッファとして使用します:
=== "Python"
```python title="hanota.py"
def move(src: list[int], tar: list[int]):
"""円盤を移動"""
# src の上から円盤を取り出す
pan = src.pop()
# 円盤を tar の上に置く
tar.append(pan)
def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
"""ハノイの塔問題 f(i) を解く"""
# src に円盤が 1 つだけ残っている場合、それを tar に移動
if i == 1:
move(src, tar)
return
# 部分問題 f(i-1):tar の助けを借りて src の上の i-1 個の円盤を buf に移動
dfs(i - 1, src, tar, buf)
# 部分問題 f(1):残りの 1 個の円盤を src から tar に移動
move(src, tar)
# 部分問題 f(i-1):src の助けを借りて buf の上の i-1 個の円盤を tar に移動
dfs(i - 1, buf, src, tar)
def solve_hanota(A: list[int], B: list[int], C: list[int]):
"""ハノイの塔問題を解く"""
n = len(A)
# B の助けを借りて A の上の n 個の円盤を C に移動
dfs(n, A, B, C)
```
=== "C++"
```cpp title="hanota.cpp"
/* 円盤を移動 */
void move(vector<int> &src, vector<int> &tar) {
// src の最上部から円盤を取り出す
int pan = src.back();
src.pop_back();
// 円盤を tar の最上部に配置
tar.push_back(pan);
}
/* ハノイの塔問題 f(i) を解く */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
// src に円盤が1つだけ残っている場合、それを tar に移動
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1):tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動
dfs(i - 1, src, tar, buf);
// 部分問題 f(1):残りの1つの円盤を src から tar に移動
move(src, tar);
// 部分問題 f(i-1):src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動
dfs(i - 1, buf, src, tar);
}
/* ハノイの塔問題を解く */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
int n = A.size();
// B の助けを借りて、上位 n 個の円盤を A から C に移動
dfs(n, A, B, C);
}
```
=== "Java"
```java title="hanota.java"
/* 円盤を移動 */
void move(List<Integer> src, List<Integer> tar) {
// src の最上部から円盤を取り出す
Integer pan = src.remove(src.size() - 1);
// 円盤を tar の最上部に配置
tar.add(pan);
}
/* ハノイの塔問題 f(i) を解く */
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
// src に円盤が1つだけ残っている場合、それを tar に移動
if (i == 1) {
move(src, tar);
return;
}
// 部分問題 f(i-1):tar の助けを借りて、上位 i-1 個の円盤を src から buf に移動
dfs(i - 1, src, tar, buf);
// 部分問題 f(1):残りの1つの円盤を src から tar に移動
move(src, tar);
// 部分問題 f(i-1):src の助けを借りて、上位 i-1 個の円盤を buf から tar に移動
dfs(i - 1, buf, src, tar);
}
/* ハノイの塔問題を解く */
void solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {
int n = A.size();
// B の助けを借りて、上位 n 個の円盤を A から C に移動
dfs(n, A, B, C);
}
```
=== "C#"
```csharp title="hanota.cs"
[class]{hanota}-[func]{Move}
[class]{hanota}-[func]{DFS}
[class]{hanota}-[func]{SolveHanota}
```
=== "Go"
```go title="hanota.go"
[class]{}-[func]{move}
[class]{}-[func]{dfsHanota}
[class]{}-[func]{solveHanota}
```
=== "Swift"
```swift title="hanota.swift"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "JS"
```javascript title="hanota.js"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "TS"
```typescript title="hanota.ts"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Dart"
```dart title="hanota.dart"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Rust"
```rust title="hanota.rs"
[class]{}-[func]{move_pan}
[class]{}-[func]{dfs}
[class]{}-[func]{solve_hanota}
```
=== "C"
```c title="hanota.c"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Kotlin"
```kotlin title="hanota.kt"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solveHanota}
```
=== "Ruby"
```ruby title="hanota.rb"
[class]{}-[func]{move}
[class]{}-[func]{dfs}
[class]{}-[func]{solve_hanota}
```
以下の図に示すように、ハノイの塔問題は高さ n の再帰木として視覚化できます。各ノードは部分問題を表し、dfs() の呼び出しに対応します。したがって、時間計算量は $O(2^n)$、空間計算量は O(n) です。
図 12-15 ハノイの塔の再帰木
!!! quote
ハノイの塔は古代の伝説に由来します。古代インドの寺院で、僧侶たちは3本の高いダイヤモンドの柱と、異なるサイズの $64$ 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。
しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 $2^{64} \approx 1.84×10^{19}$ —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう。


