--- comments: true --- # 12.4   ハノイの塔問題 マージソートと二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハノイの塔では、異なる分解戦略を採用します。 !!! question 3つの柱があり、それぞれ `A`、`B`、`C` と表記されます。最初、柱 `A` には $n$ 枚の円盤があり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの $n$ 枚の円盤を柱 `C` に移動し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます: 1. 円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。 2. 一度に移動できるのは1枚の円盤のみです。 3. 小さい円盤は常に大きい円盤の上にある必要があります。 ![ハノイの塔の例](hanota_problem.assets/hanota_example.png){ class="animation-figure" }

図 12-10   ハノイの塔の例

**サイズ $i$ のハノイの塔問題を $f(i)$ と表記します**。例えば、$f(3)$ は3枚の円盤を柱 `A` から柱 `C` に移動することを表します。 ### 1.   基本ケースを考える 以下の図に示すように、問題 $f(1)$(円盤が1枚のみ)については、`A` から `C` に直接移動できます。 === "<1>" ![サイズ1の問題の解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" } === "<2>" ![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png){ class="animation-figure" }

図 12-11   サイズ1の問題の解

$f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さい円盤を大きい円盤の上に保つ**必要があります。以下の図に示すように: 1. まず、小さい円盤を `A` から `B` に移動します。 2. 次に、大きい円盤を `A` から `C` に移動します。 3. 最後に、小さい円盤を `B` から `C` に移動します。 === "<1>" ![サイズ2の問題の解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" } === "<2>" ![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png){ class="animation-figure" } === "<3>" ![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png){ class="animation-figure" } === "<4>" ![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ 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` に正常に移動できます。 1. `B` をターゲット柱、`C` をバッファ柱として、2枚の円盤を `A` から `B` に移動します。 2. 残りの円盤を `A` から直接 `C` に移動します。 3. `C` をターゲット柱、`A` をバッファ柱として、2枚の円盤を `B` から `C` に移動します。 === "<1>" ![サイズ3の問題の解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" } === "<2>" ![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png){ class="animation-figure" } === "<3>" ![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png){ class="animation-figure" } === "<4>" ![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ 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つの部分問題を解決します: 1. `C` をバッファとして使用し、$n-1$ 枚の円盤を `A` から `B` に移動します。 2. 残りの円盤を `A` から直接 `C` に移動します。 3. `A` をバッファとして使用し、$n-1$ 枚の円盤を `B` から `C` に移動します。 各 $f(n-1)$ 部分問題について、**同じ再帰分割を適用でき**、最小の部分問題 $f(1)$ に到達するまで続けます。$f(1)$ は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。 ![ハノイの塔を解決するための分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" }

図 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 &src, vector &tar) { // src の最上部から円盤を取り出す int pan = src.back(); src.pop_back(); // 円盤を tar の最上部に配置 tar.push_back(pan); } /* ハノイの塔問題 f(i) を解く */ void dfs(int i, vector &src, vector &buf, vector &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 &A, vector &B, vector &C) { int n = A.size(); // B の助けを借りて、上位 n 個の円盤を A から C に移動 dfs(n, A, B, C); } ``` === "Java" ```java title="hanota.java" /* 円盤を移動 */ void move(List src, List tar) { // src の最上部から円盤を取り出す Integer pan = src.remove(src.size() - 1); // 円盤を tar の最上部に配置 tar.add(pan); } /* ハノイの塔問題 f(i) を解く */ void dfs(int i, List src, List buf, List 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 A, List B, List 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)$ です。** ![ハノイの塔の再帰木](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" }

図 12-15   ハノイの塔の再帰木

!!! quote ハノイの塔は古代の伝説に由来します。古代インドの寺院で、僧侶たちは3本の高いダイヤモンドの柱と、異なるサイズの $64$ 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。 しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 $2^{64} \approx 1.84×10^{19}$ —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう。