🙆‍♀️

【Go】ハノイの塔を再帰的に行うサンプルコードを書いてみた

に公開

概要

  • Goでハノイの塔を再帰的に行うサンプルコード
  • ハノイの塔は3本の棒と複数の円盤を使う数学パズル。全ての円盤を別の棒に移動させる問題で、再帰アルゴリズムの代表例。

特徴

  • 分割統治法: 問題を小さな部分問題に分割
  • 再帰の美しさ: 複雑な問題をシンプルなコードで解決
  • 指数的移動回数: 2^n - 1回の最小移動回数
  • 数学的証明可能: 最適解が数学的に証明済み
  • 視覚的理解: 動きをステップごとに追跡可能

動作原理

基本的な流れ:

  1. n枚のディスクを元の棒から目的の棒へ移動
  2. 補助の棒を使って一時的にディスクを置く
  3. 大きいディスクの上に小さいディスクしか置けない
  4. 一度に動かせるのは1枚のみ

再帰的解法

n枚のディスクを棒Aから棒Cへ移動する手順:

  1. 上のn-1枚を棒Aから棒Bへ(棒C経由)
  2. 最大のディスクを棒Aから棒Cへ
  3. n-1枚を棒Bから棒Cへ(棒A経由)

具体例

3枚のディスクの移動過程:

初期状態:        移動1後:         移動2後:
  1
  2               2              3
  3               3
-----           -----           -----
A B C           A B C           A B C
                    1            2 1

移動3後:         移動4後:         移動5後:
                                 1
  3               2              2
                  3              3
-----           -----           -----
A B C           A B C           A B C
  1 2               1

移動6後:         移動7後(完了):
                    1
                    2
1                   3
-----           -----
A B C           A B C
    2 3

総移動回数: 7回 = 2^3 - 1

サンプルコード

基本実装

package recursion

import "fmt"

// HanoiMove は1回の移動を表す構造体
type HanoiMove struct {
    From string
    To   string
    Disk int
}

// TowerOfHanoi はハノイの塔の移動手順を返す
func TowerOfHanoi(n int, from, to, aux string) []HanoiMove {
    moves := []HanoiMove{}
    hanoiHelper(n, from, to, aux, &moves)
    return moves
}

func hanoiHelper(n int, from, to, aux string, moves *[]HanoiMove) {
    // ベースケース: 1枚の場合は直接移動
    if n == 1 {
        *moves = append(*moves, HanoiMove{From: from, To: to, Disk: n})
        return
    }

    // ステップ1: n-1枚を補助の棒へ
    hanoiHelper(n-1, from, aux, to, moves)

    // ステップ2: 最大のディスクを目的地へ
    *moves = append(*moves, HanoiMove{From: from, To: to, Disk: n})

    // ステップ3: n-1枚を目的地へ
    hanoiHelper(n-1, aux, to, from, moves)
}


ステップ表示版

// TowerOfHanoiWithSteps は再帰呼び出しの過程を表示
func TowerOfHanoiWithSteps(n int, from, to, aux string, depth int) {
    indent := ""
    for i := 0; i < depth; i++ {
        indent += "  "
    }

    fmt.Printf("%sHanoi(%d, %s, %s, %s) を呼び出し\\\\n", indent, n, from, to, aux)

    if n == 1 {
        fmt.Printf("%s  ディスク1を%sから%sへ移動\\\\n", indent, from, to)
        return
    }

    fmt.Printf("%s  ステップ1: %d枚のディスクを%sから%sへ(%s経由)\\\\n",
               indent, n-1, from, aux, to)
    TowerOfHanoiWithSteps(n-1, from, aux, to, depth+1)

    fmt.Printf("%s  ステップ2: ディスク%dを%sから%sへ移動\\\\n",
               indent, n, from, to)

    fmt.Printf("%s  ステップ3: %d枚のディスクを%sから%sへ(%s経由)\\\\n",
               indent, n-1, aux, to, from)
    TowerOfHanoiWithSteps(n-1, aux, to, from, depth+1)
}


シミュレーション版

// SimulateTowerOfHanoi は移動過程を視覚的に表示
func SimulateTowerOfHanoi(n int) {
    // 3本の棒を初期化
    towers := map[string][]int{
        "A": make([]int, 0),
        "B": make([]int, 0),
        "C": make([]int, 0),
    }

    // 初期状態: 全てのディスクを棒Aに配置
    for i := n; i >= 1; i-- {
        towers["A"] = append(towers["A"], i)
    }

    fmt.Printf("初期状態(%d枚のディスク):\\\\n", n)
    PrintHanoiState(towers)

    // 移動手順を取得
    moves := TowerOfHanoi(n, "A", "C", "B")

    // 各移動を実行
    for i, move := range moves {
        fmt.Printf("移動 %d: ディスク%dを%sから%sへ\\\\n",
                   i+1, move.Disk, move.From, move.To)

        // ディスクを移動
        fromTower := towers[move.From]
        disk := fromTower[len(fromTower)-1]
        towers[move.From] = fromTower[:len(fromTower)-1]
        towers[move.To] = append(towers[move.To], disk)

        PrintHanoiState(towers)
    }

    fmt.Printf("\\\\n完了! 総移動回数: %d\\\\n", len(moves))
}

// PrintHanoiState は塔の状態を表示
func PrintHanoiState(towers map[string][]int) {
    maxHeight := 0
    for _, tower := range towers {
        if len(tower) > maxHeight {
            maxHeight = len(tower)
        }
    }

    // 上から順に表示
    for level := maxHeight - 1; level >= 0; level-- {
        for _, peg := range []string{"A", "B", "C"} {
            if level < len(towers[peg]) {
                fmt.Printf("  %d  ", towers[peg][level])
            } else {
                fmt.Print("  |  ")
            }
        }
        fmt.Println()
    }
    fmt.Println("-----+-----+-----")
    fmt.Println("  A     B     C  ")
    fmt.Println()
}


移動回数の計算

// CountHanoiMoves はn枚のディスクの最小移動回数を計算
func CountHanoiMoves(n int) int {
    return (1 << n) - 1  // 2^n - 1
}

// 使用例
func main() {
    for n := 1; n <= 10; n++ {
        moves := CountHanoiMoves(n)
        fmt.Printf("%2d枚: %d回\\\\n", n, moves)
    }
    // 出力:
    //  1枚: 1回
    //  2枚: 3回
    //  3枚: 7回
    //  4枚: 15回
    //  5枚: 31回
}


反復版(比較用)

// TowerOfHanoiIterative は反復による実装
func TowerOfHanoiIterative(n int) []HanoiMove {
    moves := []HanoiMove{}

    // 偶数と奇数で移動パターンが異なる
    if n%2 == 0 {
        moves = iterativeHanoi(n, "A", "B", "C")
    } else {
        moves = iterativeHanoi(n, "A", "C", "B")
    }

    return moves
}

func iterativeHanoi(n int, source, auxiliary, destination string) []HanoiMove {
    moves := []HanoiMove{}
    totalMoves := (1 << n) - 1

    // 各棒の状態を管理
    towers := map[string][]int{
        source:      make([]int, 0),
        auxiliary:   make([]int, 0),
        destination: make([]int, 0),
    }

    // 初期配置
    for i := n; i >= 1; i-- {
        towers[source] = append(towers[source], i)
    }

    pegs := []string{source, auxiliary, destination}

    // ビット演算を使った移動パターン
    for i := 1; i <= totalMoves; i++ {
        from := pegs[(i&(i-1))%3]
        to := pegs[((i|(i-1))+1)%3]

        // 小さいディスクを大きいディスクの上に移動
        if canMove(towers, from, to) {
            moveDisc(towers, from, to, &moves)
        } else {
            moveDisc(towers, to, from, &moves)
        }
    }

    return moves
}


計算量

時間計算量

  • 移動回数: O(2^n) - 正確には2^n - 1回
  • 再帰呼び出し: O(2^n) - 各ノードで2つの再帰呼び出し

空間計算量

  • 再帰版: O(n) - 再帰スタックの深さ
  • 反復版: O(n) - ディスクの状態を保持

ディスク枚数と移動回数

ディスク枚数 移動回数 計算式
1 1 2^1 - 1
2 3 2^2 - 1
3 7 2^3 - 1
4 15 2^4 - 1
5 31 2^5 - 1
10 1,023 2^10 - 1
20 1,048,575 2^20 - 1
64 約1,844京 2^64 - 1

使いどころ

向いてる場面

  • 再帰アルゴリズムの教育
  • 分割統治法の理解
  • 問題解決思考の訓練
  • アルゴリズム設計の練習

実例:パズルゲーム実装

// HanoiGame はハノイの塔ゲームの実装
type HanoiGame struct {
    Towers    map[string][]int
    NumDisks  int
    MoveCount int
    History   []HanoiMove
}

func NewHanoiGame(n int) *HanoiGame {
    game := &HanoiGame{
        Towers:    make(map[string][]int),
        NumDisks:  n,
        MoveCount: 0,
        History:   []HanoiMove{},
    }

    // 初期配置
    game.Towers["A"] = make([]int, 0)
    game.Towers["B"] = make([]int, 0)
    game.Towers["C"] = make([]int, 0)

    for i := n; i >= 1; i-- {
        game.Towers["A"] = append(game.Towers["A"], i)
    }

    return game
}

// Move はディスクを移動
func (g *HanoiGame) Move(from, to string) error {
    fromTower := g.Towers[from]
    toTower := g.Towers[to]

    // 移動可能チェック
    if len(fromTower) == 0 {
        return fmt.Errorf("棒%sにディスクがありません", from)
    }

    disk := fromTower[len(fromTower)-1]
    if len(toTower) > 0 && toTower[len(toTower)-1] < disk {
        return fmt.Errorf("大きいディスクを小さいディスクの上に置けません")
    }

    // 移動実行
    g.Towers[from] = fromTower[:len(fromTower)-1]
    g.Towers[to] = append(toTower, disk)
    g.MoveCount++
    g.History = append(g.History, HanoiMove{From: from, To: to, Disk: disk})

    return nil
}

// IsComplete はパズルが完成したか判定
func (g *HanoiGame) IsComplete() bool {
    return len(g.Towers["C"]) == g.NumDisks
}


他の実装との比較

実装方法 時間計算量 空間計算量 可読性 実装難易度
再帰 O(2^n) O(n)
反復(ビット演算) O(2^n) O(n)
反復(スタック) O(2^n) O(n)
並列処理 O(2^n) O(n)

最適化アイデア

パフォーマンス測定

import "time"

func BenchmarkHanoi(n int) {
    fmt.Printf("n = %d での比較:\\\\n", n)

    // 再帰版
    start := time.Now()
    recursiveMoves := TowerOfHanoi(n, "A", "C", "B")
    recursiveTime := time.Since(start)

    // 反復版
    start = time.Now()
    iterativeMoves := TowerOfHanoiIterative(n)
    iterativeTime := time.Since(start)

    fmt.Printf("再帰版: %v (%d moves)\\\\n", recursiveTime, len(recursiveMoves))
    fmt.Printf("反復版: %v (%d moves)\\\\n", iterativeTime, len(iterativeMoves))
}


並列処理版

// ParallelHanoi は並列処理を使った実装(概念的)
func ParallelHanoi(n int) []HanoiMove {
    // 左右の部分問題を並列実行
    ch1 := make(chan []HanoiMove)
    ch2 := make(chan []HanoiMove)

    go func() {
        // n-1枚を補助棒へ
        ch1 <- TowerOfHanoi(n-1, "A", "B", "C")
    }()

    go func() {
        // n-1枚を目的棒へ
        ch2 <- TowerOfHanoi(n-1, "B", "C", "A")
    }()

    // 結果を結合
    moves1 := <-ch1
    middleMove := HanoiMove{From: "A", To: "C", Disk: n}
    moves2 := <-ch2

    result := append(moves1, middleMove)
    result = append(result, moves2...)

    return result
}


メモリ効率版

// HanoiGenerator は必要に応じて移動を生成
func HanoiGenerator(n int) <-chan HanoiMove {
    ch := make(chan HanoiMove)

    go func() {
        defer close(ch)
        generateMoves(n, "A", "C", "B", ch)
    }()

    return ch
}

func generateMoves(n int, from, to, aux string, ch chan<- HanoiMove) {
    if n == 1 {
        ch <- HanoiMove{From: from, To: to, Disk: n}
        return
    }

    generateMoves(n-1, from, aux, to, ch)
    ch <- HanoiMove{From: from, To: to, Disk: n}
    generateMoves(n-1, aux, to, from, ch)
}

// 使用例
func main() {
    for move := range HanoiGenerator(3) {
        fmt.Printf("移動: ディスク%dを%sから%sへ\\\\n",
                   move.Disk, move.From, move.To)
    }
}


まとめ

メリット

  • エレガントな再帰解法
  • 数学的に最適解が保証
  • 分割統治法の良い例
  • 実装がシンプル

デメリット

  • 指数的な実行時間
  • 大きなnでは実用不可
  • 再帰スタックの制限
  • 並列化が困難

使うべき時

  • アルゴリズム教育
  • 再帰の理解
  • パズルゲーム実装
  • 問題解決思考の訓練

学習ポイント

  • 再帰的思考: 大きな問題を小さな問題に分割
  • 数学的帰納法: ベースケースと再帰ケース
  • 計算量の理解: 指数的増加の実感
  • 最適性の証明: 数学的に最小手数が証明可能

ハノイの塔は再帰アルゴリズムの美しさと威力を示す最良の例。シンプルなルールから複雑な動作が生まれ、数学的な美しさとプログラミングの実用性を兼ね備えた問題。

GitHubで編集を提案

Discussion