ハノイの塔はアルゴリズムの定番問題です。3本の杭と、中央に穴の開いた大きさの異なる複数の円盤から構成され、最初はすべての円盤が左端の杭に小さいものが上になるように順に積み重ねられています。すべての円盤を右端の杭に移動させられれば完成ですが、一度に移動することができる円盤は一枚だけです。また小さな円盤の上に大きな円盤を乗せることはできません。

C# コンソールアプリ

まずC#でこれをやってみます。

塔の初期化

Initメソッドはハノイの塔の初期化をおこないます。配列に空のリストをセットして、一番左側の杭にあたるpiles[0]に{n, n-1, ……, 1}を格納します。

ハノイの塔では小さな円盤の上に大きな円盤を乗せることはできないというルールがあります。1~nの整数が円盤の大きさになっていて、先に格納されている値が下に存在する円盤にあたります。そのため値は大きい順に格納されます。

塔の状態を表示する

ShowPilesメソッドは各杭にある円盤の番号を表示します。

円盤を移動させる処理

円盤を移動させる処理を示します。piles[from]の一番最後の要素を取り除き、piles[to]の一番最後に追加しているだけです。

再帰で複数の円盤を移動させる

移動させるためにはまず一番下にあるもの以外を動かしたい杭ではない杭に移動させます。そして一番下にある円盤を移動させたい杭の位置に移動させて、事前に移動してあった他の円盤をその上に移動させます。では図の場合、1~3はどうやって移動させればよいでしょうか? 1と2を空いている別の杭に移動させてから3を移動させればよいのです。1と2を移動させるためには1を別の杭に移動させることで2を移動できるようになります。

このように考えると再帰処理をすればよいことがわかります。Hanoiメソッドのなかでnを1減らして再帰呼び出しをして、そのあとMoveOneメソッドで一番下にある円盤を移動させたあと、いったん待避させておいた円盤をその上に移動させます。そのため移動の処理の回数はnが1増えるにしたがって前回の2倍+1回ということになります。

漸化式 A(1) = 1, A(n+1) = 2A(n) + 1 を解くとA(n) = 2^n – 1となることがわかります。

実行結果

実行結果は以下のようになります。A(n) = 2^n – 1なので塔が3段の場合は7回の動作で移動させることができます。

特定のステップの状態を表示する

次に塔の高さと回数が与えられたら現在の状態を表示できるようにします。A(n) = 2^n – 1なのでnが少し大きな数になると処理が爆発的に増えてしまいます。例えば塔が64段で638億4000万回(西暦元年に1秒に1枚移動する処理を開始したとするとだいたいこの回数になる)移動したときどのような状態になっているか調べようとする場合、プログラムを改良しないと時間がいくらあっても終わりません。

変更部分だけ示します。まずフィールド変数としてtargetとdoneを定義します。

doneがtrueの場合は結果が取得できたあとなので以降はMoveOneメソッドとHanoiメソッドのなかではなにもしません。ShowPilesメソッドを実行するのはmoveCount == targetのときだけです。

n枚を移動させるためにはMath.Pow(2, n) – 1回の処理が必要ですが、HanoiメソッドのなかではmoveCount + Math.Pow(2, n) – 1 <= targetである場合、再帰処理はしないで直接移動処理をしています。またその場合はmoveCountにMath.Pow(2, n) - 1を加算しています。

実行結果はこうなります。

西暦元年から移動処理を開始していたとしても半分どころか41番~64番までまったく手つかずであることがわかります。

インドのガンジス河の畔のヴァラナシに、世界の中心を表すという巨大な寺院がある。そこには青銅の板の上に、長さ1キュビット、太さが蜂の体ほどの3本のダイヤモンドの針が立てられている。そのうちの1本には、天地創造のときに神が64枚の純金の円盤を大きい円盤から順に重ねて置いた。これが「ブラフマーの塔」である。司祭たちはそこで、昼夜を通して円盤を別の柱に移し替えている。そして、全ての円盤の移し替えが終わったときに、世界は崩壊し終焉を迎える。

とのことですが、64段のハノイの塔を実際に動かす場合は、2^64-1回(約1844京6744兆回)となり、これは1回を1秒で動かすとして約5845億年もかかります。世界が滅亡するのはまだまだ先のことなので心配はなさそうです。

JavaScriptでもやってみる

JavaScriptでもやってみることにします。これは塔が10段の場合、どのような移動がおこなわれるかシミュレーションできます。また表示速度も変更できるようにします。

HTML部分

グローバル変数と定数

index.js

ページが読み込まれたときの処理

ページが読み込まれたときの処理を示します。

杭の状態を表示する関数を示します。ここでは文字情報だけでなく視覚的にもわかるようにそれぞれの円盤をcanvasに描画しています。

円盤を描画する関数を示します。

円盤を移動させる

円盤を移動させる関数を示します。配列の最後の要素を取り出して移動先の配列の最後に追加しています。そして自作関数のsleepを呼び出してintervalだけ待機しています。

待機処理をする関数を示します。

hanoi関数は再帰処理で引数分の個数の円盤を移動させる関数です。

シミュレーションを開始する

[実行]ボタンをクリックしたらシミュレーションを開始します。その処理を示します。

実行中は[実行]ボタンを非表示にしてキャンセル用のボタンを表示させます。1回1回の移動の間隔をどれだけ開けるかはテキストボックスから取得します。取得された値がminとmaxのあいだであればその値をintervalに格納し、不適切な値の場合はminかmaxの値を採用します。

シミュレーションをキャンセルしようとしたときの処理を示します。cancelingフラグをセットすることでhanoi関数とmoveOne関数がなにもしないで処理を返すのでシミュレーションの処理も終了します。