マガジン

ハノイの塔の手数は、なぜ2倍ずつ増えるのか

遊びを見つける

執筆にClaudeを使用しています。

道をつなぐ線、丸い駒、幾何学のタイルからなるパズルの模型。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

円盤が1枚なら1手、2枚なら3手、3枚なら7手。ハノイの塔の最少手数を並べると、1、3、7、と増えていきます。見た目は「だいたい2倍」ですが、ぴったり2倍ではありません。3の2倍は6で、実際は7です。この「ぴったりではない」ところに、再帰の構造がそのまま現れています。 この記事の問いは、「円盤が1枚増えると、なぜ手数がほぼ倍になるのか」です。答えは、円盤の動かし方を図にすると見えてきます。n枚の塔を動かす手順の中に、(n−1)枚の塔を動かす手順が、そっくり2回入っています。残りの1手は、いちばん大きい円盤を動かす手です。 数式の暗記は必要ありません。棒を3本と円盤の手順表を用意して、自分で手を数え、表に記録します。そのうえで、数えた結果が「2倍して1足す」の形になっているかを確かめます。最後に、同じ手順を再帰で書いた短いコードも用意しました。

この記事の目次
  1. 問いを正確にする:2倍ずつ増えるのは何か
  2. 1枚・2枚・3枚の手順を図で追う
  3. 再帰の構造:同じ形が一回り小さく入っている
  4. 自分で数える:記録表と読み方
  5. 起こりやすい誤り
  6. 次の行動

問いを正確にする:2倍ずつ増えるのは何か

まずルールを確認します。棒はA、B、Cの3本で、最初はAに円盤がすべて、大きい順に下から積まれています。目標は、全部をCへ移すことです。守るルールは次の2つです。

  • 1回に動かせる円盤は1枚だけです。動かすのは、棒の一番上にある円盤です。
  • 小さい円盤の上に、大きい円盤を置くことはできません。

この記事では、円盤1枚を1本の棒から別の棒へ動かすことを「1手」と数えます。数え方を先に決めておくのは大切なことです。「戻す」操作を手数に含めるかどうかなど、定義が違えば数字も変わります。

円盤n枚の最少手数を T(n) と書きます。ここでの問いは「T(n) は T(n−1) の何倍か」ですが、ぴったり2倍にはなりません。正しくは次の関係です。

text
T(n) = 2 × T(n−1) + 1

「+1」を移項すると、見通しがよくなります。両辺に1を足すと次の形になります。

text
T(n) + 1 = 2 × ( T(n−1) + 1 )

つまり、手数に1を足した値は、枚数が1枚増えるたびにぴったり2倍になります。題名の「2倍ずつ」は、手数そのものではなく、この「手数+1」について正確に言えることです。

1枚・2枚・3枚の手順を図で追う

小さい場合から順に手順を追います。円盤は、小さい方から1、2、3と呼びます。

1枚の場合です。1をAからCへ動かして終わりです。

text
手1: 円盤1  A→C

2枚の場合です。1をいったんBに逃がし、2をCへ動かし、1をCへ戻します。

text
手1: 円盤1  A→B
手2: 円盤2  A→C
手3: 円盤1  B→C

3枚の場合です。各手のあとの棒の状態を、下から上の順に書きます。

text
開始      A: 3 2 1    B: -       C: -
手1: 1 A→C  A: 3 2      B: -       C: 1
手2: 2 A→B  A: 3        B: 2       C: 1
手3: 1 C→B  A: 3        B: 2 1     C: -
手4: 3 A→C  A: -        B: 2 1     C: 3
手5: 1 B→A  A: 1        B: 2       C: 3
手6: 2 B→C  A: 1        B: -       C: 3 2
手7: 1 A→C  A: -        B: -       C: 3 2 1

ここで手4に注目します。いちばん大きい円盤3が動くのは、この1回だけです。手4の直前には、小さな2枚(2と1)がBに積まれていて、AとCがそれぞれ「3だけ」「空」になっています。大きい円盤を動かすには、この状態が必要です。

手1〜3は、「2枚の塔をAからBへ動かす」手順になっています。2枚の手順で、行き先をCからBに読み替えたものです。手5〜7は、「2枚の塔をBからCへ動かす」手順です。3枚の手順は、2枚の手順2回と、大きい円盤の1手でできています。

再帰の構造:同じ形が一回り小さく入っている

3枚で見えた形は、枚数を問わず成り立ちます。n枚の塔を、出発の棒から目的の棒へ動かす手順は、次の3段階です。

  1. 上の(n−1)枚を、出発の棒から「残りの1本」へ動かします。
  2. いちばん大きい円盤を、出発の棒から目的の棒へ動かします。
  3. 先ほど動かした(n−1)枚を、残りの棒から目的の棒へ動かします。

この1と3は、「n枚」の問題を「(n−1)枚」にした同じ形の問題です。自分自身の手順を、一回り小さい規模で呼び出しています。これが再帰です。(n−1)枚の動かし方は、さらに(n−2)枚の動かし方を2回使います。これを繰り返すと、最後は1枚の動かし方、つまり1手に行き着きます。

手数を数えると、1と3でそれぞれ T(n−1) 手、2で1手なので、合計は 2×T(n−1)+1 手です。これが先ほどの関係の正体です。

この手順が最少である理由も、図から読み取れます。いちばん大きい円盤を動かす瞬間、上に円盤が乗っていてはいけません。また、行き先の棒にも円盤があってはいけません。大きい円盤の上にも、行き先の棒にも、小さい円盤は置けない、というルールのためです。したがって、残りの(n−1)枚は第三の棒に積まれている必要があります。大きい円盤が動く前に、(n−1)枚を第三の棒へ移す手が必ず要ります。動かしたあとも、(n−1)枚を大きい円盤の上に乗せ直す手が要ります。この2回分は省けません。

自分で数える:記録表と読み方

円盤は硬貨や紙片でも代用できます。大きさの違うものを重ね、ここでは「紙を3枚の円盤、紙コップを棒」のような見立てで十分です。1枚から4枚まで、次の手順で記録します。

  • 1手動かすたびに、手数を1つ数えます。
  • 各枚数で、無駄な手を入れずに動かしきったときの手数を記録します。
  • 動かし方に迷ったら、前の章の3段階に従います。

記録表は次のようにします。右側の列は、数えた結果から計算して埋めます。

  • 1、数えた手数 T(n):、T(n)+1:、T(n)+1 が前の行の何倍か:(基準)
  • 2、数えた手数 T(n):、T(n)+1:、T(n)+1 が前の行の何倍か:
  • 3、数えた手数 T(n):、T(n)+1:、T(n)+1 が前の行の何倍か:
  • 4、数えた手数 T(n):、T(n)+1:、T(n)+1 が前の行の何倍か:

読み方の基準は次のとおりです。

  • 「T(n)+1」の列が、行ごとにぴったり2倍になっていれば、手順が最少で、構造もそのとおりです。
  • 3枚で7手より多くなったら、途中で同じ円盤を意味なく往復させていないか、手順表で確認します。
  • 4枚の手数は、表を見る前に「3枚の手数×2+1」で予想し、実際に数えた値と照らし合わせます。予想と合えば、構造を理解できています。

枚数と手数の関係は、一般に T(n) = 2ⁿ − 1 と書けます。表の「T(n)+1」の列が2、4、8、…と並ぶのは、このためです。数えた結果が2の累乗に1足りない形になっているかどうかが、確認の目印になります。

コードで同じ手順を列挙する

手で数える代わりに、再帰の定義をそのままコードにして確かめる方法もあります。次のファイルは外部ライブラリを使わず、単独で動きます。ブラウザで開くと、手順と合計手数が表示されます。

html
<pre id="out"></pre>
<script>
let count = 0;
const lines = [];
function hanoi(n, from, to, via) {
  if (n === 0) return;
  hanoi(n - 1, from, via, to);
  count++;
  lines.push(count + ": 円盤" + n + " " + from + "→" + to);
  hanoi(n - 1, via, to, from);
}
const n = 3;
hanoi(n, "A", "C", "B");
lines.push("合計 " + count + " 手");
document.getElementById("out").textContent = lines.join("\n");
</script>

関数 hanoi の中身は、前の章の3段階そのものです。1行目の hanoi(n - 1, from, via, to) が「(n−1)枚を残りの棒へ」、真ん中が「大きい円盤を目的の棒へ」、最後の行が「(n−1)枚を目的の棒へ」にあたります。n = 3 の出力は、図で追った7手と同じ順になります。n の値を4、5と変えて、合計の手数を表に書き込んでください。

起こりやすい誤り

数えたり実装したりするとき、つまずきやすい点を挙げます。

  • 手数の定義がぶれる:「1手」を円盤1枚の移動と決めたのに、複数枚を一度に動かす数え方が混ざることがあります。ルールで許されるのは1枚ずつです。
  • 「手数が2倍」と思い込む:3手の次は6手ではなく7手です。「+1」を忘れると、表の比が合いません。「手数+1」で見ると、ぴったり2倍になります。
  • 棒の役割の取り違え:再帰の1段階目では、行き先が「目的の棒」ではなく「残りの棒」です。コードでは hanoi(n - 1, from, via, to) の引数の順番を間違えやすい箇所です。
  • 0枚の扱い忘れ:コードの if (n === 0) return; を外すと、再帰が止まらなくなります。止まる条件を最初に決めることが再帰の基本です。
  • 「最少」の検証不足:動かしきれたことと最少であることは別です。少なくとも3枚では、7手より少ない手順がないかを、大きい円盤が動く前に必要な状態から考えてみます。

「手数がどう数えられるか」の定義が結果を左右する点は、ほかのパズルでも同じです。たとえば「かたちのあいだ」には、次の一手のヒントが最短とは限らない仕組みと、一手戻しても手数は減らない仕組みがあります。自分の記録を読むときは、どこまでを1手と数えているのかを、ゲーム側の説明で確かめておくと誤解を避けられます。

次の行動

手元の紙片か上のコードを使って、1〜4枚の手数を表に書き込み、「T(n)+1」の列が2倍ずつになるかを確かめてみてください。4枚を数える前に、3枚の手数から予想を立てておくと、再帰の理解度がはっきり分かります。

さらに進めたい場合は、上のコードに円盤の絵と「クリックで動かす」操作を足し、index.html 1つの作品にして投稿する方法があります。アソバースは単独のHTMLファイルをアップロードして公開できます。手順の列挙だけでも、「枚数を選ぶと手順が表示される」小さな教材として成り立ちます。