M.Hiroi's Home Page
http://www.geocities.jp/m_hiroi/

Functional Programming

お気楽 Standard ML of New Jersey 入門

[ PrevPage | SML/NJ | NextPage ]

パズルに挑戦!

今回は 4 つのパズルを出題します。SML/NJ で解法プログラムを作成してください。

●問題1「騎士の周遊」

騎士(ナイト)はチェスの駒のひとつで、将棋の桂馬の動きを前後左右にとることができます。次の図を見てください。

    ┌─┬─┬─┬─┬─┐        ┌─┬─┐ 
    │  │●│  │●│  │        │K│  │ 
    ├─┼─┼─┼─┼─┤    ┌─┼─┼─┼─┐ 
    │●│  │  │  │●│    │  │  │  │  │ 
    ├─┼─┼─┼─┼─┤    ├─┼─┼─┼─┤ 
    │  │  │K│  │  │    │  │×│×│  │ 
    ├─┼─┼─┼─┼─┤    ├─┼─┼─┼─┤ 
    │●│  │  │  │●│    │  │  │  │  │ 
    ├─┼─┼─┼─┼─┤    └─┼─┼─┼─┘ 
    │  │●│  │●│  │        │  │  │ 
    └─┴─┴─┴─┴─┘        └─┴─┘ 

 ●:ナイト (K) が動ける位置        問題A

                図 : 騎士の周遊

このナイトを動かして、どのマスにもちょうど一回ずつ訪れて出発点に戻る周遊経路を求めるのが問題です。ちなみに、4 行 4 列の盤面には解がありませんが、6 行 6 列、8 行 8 列の盤面には解が存在します。大きな盤面を解くのは大変なので、問題 A の盤面でナイトの周遊経路を求めてください。なお、ナイトは×印のマスに移動することはできません。

解答

●問題2「変形魔方陣」

次は三角形の魔方陣です。下図を見てください。

              A
            /  \           A + B + D + F = 20  
          B      C
        /          \       A + C + E + I = 20
      D              E
    /                  \   F + G + H + I = 20
  F───G───H───I

                図 : 変形魔方陣

上図の三角形の A から I の場所に 1 から 9 までの数字をひとつずつ配置します。直線上にある 4 つの数字の和が、3 本の直線で 20 になる配置を求めてください。

なお、このパズルは拙作のページ Memorandum 2004 年 7 月 5 日 で出題したものです。このときは 4 つの数字の和が 17 になる配置を求めました。このほかにも 19, 21, 23 になる配置があります。興味のある方は全ての配置を求めてみてください。

解答

●問題3「チャイニーズ・チェッカー」

チャイニーズ・チェッカーは「ペグ・ソリテア」と呼ばれるパズルのひとつです。ペグ・ソリテアは、盤上に配置されたペグ (駒) を、最後にはひとつ残るように取り除いていく古典的なパズルです。ペグは次のルールに従って移動し、除去することができます。

盤は今までに多数考案されていますが、33 穴英国盤、37 穴フランス盤、41 穴盤が有名でしょう。33 穴英国盤とチャイニーズ・チェッカーを図に示します。

          ●─●─●
          │  │  │
          ●─●─●
          │  │  │                             ●
  ●─●─●─●─●─●─●                   /  \
  │  │  │  │  │  │  │                 ●───●
  ●─●─●─○─●─●─●               /  \  /  \
  │  │  │  │  │  │  │             ●───●───●
  ●─●─●─●─●─●─●           /  \  /  \  /  \
          │  │  │                 ●───●───●───●
          ●─●─●               /  \  /  \  /  \  /  \
          │  │  │             ●───●───○───●───●  
          ●─●─●            

      (1) 33 穴英国盤               (2) チャイニーズ・チェッカー

                        図 : ペグ・ソリテア

それぞれのマスにペグがありますが、そこからひとつペグを取り除いてゲームを始めます。図では黒丸でペグを表し、白丸で空き場所を表しています。ルールに従ってペグを移動し、最後にひとつだけ残ればクリアとなります。

それでは問題です。図 (2) に示したように、下辺の中央のペグを取り除きます。この状態から始めて、最後のペグが最初に取り除いた位置に残る跳び方の最小手数を求めてください。

解答

●問題4「7パズル」

最後の問題はスライドパズルです。1 から 7 までの数字を並べる7パズルを考えます。次の図を見てください。

  ┌─┬─┬─┬─┐    ┌─┬─┬─┬─┐  
  │1│2│3│4│    │?│?│?│?│
  ├─┼─┼─┼─┤ => ├─┼─┼─┼─┤
  │5│6│7│  │    │?│?│?│?│
  └─┴─┴─┴─┘    └─┴─┴─┴─┘
        完成形            最長手数の局面

                図 : 7パズル

7パズルの最長手数の局面を求めてください。

解答


●問題1「騎士の周遊」の解答

それではプログラムを作りましょう。この問題は盤面が小さいので、単純な深さ優先探索で簡単に解くことができます。下図に示すように、盤面のマスに番号をつけます。

    ┌─┬─┐           ┌─┬─┐     
    │K│  │           │0│1│     
┌─┼─┼─┼─┐   ┌─┼─┼─┼─┐ 
│  │  │  │  │   │2│3│4│5│ 
├─┼─┼─┼─┤   ├─┼─┼─┼─┤ 
│  │×│×│  │   │6│×│×│7│ 
├─┼─┼─┼─┤   ├─┼─┼─┼─┤ 
│  │  │  │  │   │8│9│10│11│ 
└─┼─┼─┼─┘   └─┼─┼─┼─┘ 
    │  │  │           │12│13│     
    └─┴─┘           └─┴─┘     

       盤面                 番号

        図 : 盤面と番号の関係

あとは隣接リストを定義して、深さ優先探索で周遊経路を探索するだけです。プログラムは次のようになります。

リスト : 「騎士の周遊」解法プログラム

(* 隣接リスト *)
val adjacent = #[
    [5, 6],         (* 0 *)
    [2, 7],         (* 1 *)
    [1, 9],         (* 2 *)
    [7, 8, 10],     (* 3 *)
    [6, 9, 11],     (* 4 *)
    [0, 10],        (* 5 *)
    [0, 4, 10, 12], (* 6 *)
    [1, 3, 9, 13],  (* 7 *)
    [3, 13],        (* 8 *)
    [2, 4, 7],      (* 9 *)
    [3, 5, 6],      (* 10 *)
    [4, 12],        (* 11 *)
    [6, 11],        (* 12 *)
    [7, 8]]         (* 13 *)

(* int list の表示 *)
fun print_intlist( nil ) = print( "\n" )
|   print_intlist( x::xs ) =
    ( print( Int.toString(x) ^ " " ); print_intlist( xs ) )

(* 深さ優先探索 *)
fun knight_tour( n, goal, path ) =
    let
      val al = Vector.sub( adjacent, hd( path ) )
    in
      if n = 14
      then if List.exists (fn(x) => x = goal) al
           then print_intlist( goal::path )
           else ()
      else app (fn(x) => if List.exists (fn(y) => x = y) path
                         then ()
                         else knight_tour( n + 1, goal, x :: path ))  
               al
     end

fun solve() = knight_tour( 1, 0, [0] )

隣接リストはベクタ adjacent に定義します。要素はリストであることに注意してください。関数 knight_tour は深さ優先探索で騎士の周遊経路を求めます。引数 n は訪れたマスの個数、goal はゴール地点(出発点)、path は経路(リスト)を表します。周遊経路を求めるので出発点はどこでもいいのですが、今回は 0 を出発点としてます。変数 al は現在地点(path の先頭要素)の隣接リストを表します。

全部のマスを 1 回ずつ訪れると n の値は 14 になります。最後のマスから出発点 (goal) に戻ることができれば周遊経路になります。これは最後のマスの隣接リストに goal が含まれているかチェックすればいいですね。この処理を List.exists で行っています。そうであれば周遊経路になるので、関数 print_intlist で path を表示します。

n が 14 より小さい場合は、深さ優先で騎士を進めていきます。この処理は経路の探索と同じなので、詳しく説明する必要はないでしょう。これでプログラムは完成です。

それでは、実行してみましょう。

- solve();
0 6 12 11 4 9 2 1 7 13 8 3 10 5 0
0 5 10 3 8 13 7 1 2 9 4 11 12 6 0
val it = () : unit

2 通りの周遊経路が表示されましたが、逆回りの経路があるので、実際の経路は次の 1 通りしかありません。

      ┌─┬─┐      
      │0│7│      
  ┌─┼─┼─┼─┐  
  │6│11│4│13│  
  ├─┼─┼─┼─┤  
  │1│×│×│8│  
  ├─┼─┼─┼─┤  
  │10│5│12│3│  
  └─┼─┼─┼─┘  
      │2│9│      
      └─┴─┘      

    図 : 周遊経路

「騎士の周遊」は、拙作のページ Puzzle DE Programming「騎士の巡歴 (Knight's Tour)」 でも取り上げています。興味のある方は参考にしてください。


●問題2「変形魔方陣」の解答

それではプログラムを作りましょう。今回は重複解をチェックすることにします。次の図を見てください。

              0              
            /  \            
          1      8          
        /          \        
      2              7      
    /                  \    
  3───4───5───6  

    図 : 変形魔方陣の盤面

変形魔方陣の場合、回転解が 3 種類あって、鏡像解が 2 種類あります。3 つの頂点の大小関係をチェックすることで、これらの対称解を排除することができます。盤面を配列 board で表すことにすると、具体的には次の条件を満たす解を探します。

Array.sub( board, 0 ) < Array.sub( board, 3 ) < Array.sub( board, 6 )

このほかに、頂点の間にある 2 つの数字を入れ替えただけの解もあります。これらを重複解と考えて排除することにしましょう。具体的には、次の条件を追加します。

Array.sub( board, 1 ) < Array.sub( board, 2 )
Array.sub( board, 4 ) < Array.sub( board, 5 )
Array.sub( board, 7 ) < Array.sub( board, 8 )

このように、数字の大小関係をチェックすることで、重複解を排除することができます。あとは順列を生成する途中で、条件を満たしているかチェックするだけです。プログラムは次のようになります。

リスト : 変形魔方陣の解法

(* 盤面 *)
val board = Array.array( 9, 0 )

(* 要素の削除 *)
fun remove( _, nil ) = nil
|   remove( n, x::xs ) =
    if x = n then xs else x::remove( n, xs )

(* 盤面の表示 *)
fun print_board() = (
    Array.app (fn(x) => print( Int.toString(x) ^ " " )) board;
    print("\n"))

(* 数字の加算 *)
fun add_number( nil, sum ) = sum
|   add_number( x::xs, sum ) = add_number( xs, sum + Array.sub( board, x ) )  

(* 条件チェック *)
val check_func_list = [
    fn(x) => true,                               (* 0 *)
    fn(x) => true,                               (* 1 *)
    fn(x) => Array.sub( board, 1 ) < x,          (* 2 *)
    fn(x) => Array.sub( board, 0 ) < x andalso
             add_number( [0,1,2], 0 ) + x = 20,  (* 3 *)
    fn(x) => true,                               (* 4 *)
    fn(x) => Array.sub( board, 4 ) < x,          (* 5 *)
    fn(x) => Array.sub( board, 3 ) < x andalso
             add_number( [3,4,5], 0 ) + x = 20,  (* 6 *)
    fn(x) => true,                               (* 7 *)
    fn(x) => Array.sub( board, 7 ) < x andalso
             add_number( [0,6,7], 0 ) + x = 20 ] (* 8 *)

(* 盤面の生成 *)
fun make_board( _, nil, _ ) = print_board()
|   make_board( n, f::fs, nums ) =
    app (fn(x) => if f(x)
                  then (Array.update( board, n, x );
                        make_board( n + 1, fs, remove( x, nums )))
                  else ())
        nums

fun solve() = make_board( 0, check_func_list, [1,2,3,4,5,6,7,8,9] )

関数 make_board で順列を生成します。条件はリスト check_func_list に格納されている関数を使ってチェックします。第 1 引数 n が盤面の位置、第 2 引数がチェック関数のリスト、第 3 引数が数字のリストです。第 2 引数が nil になると条件を全て満たしているので、関数 print_board で盤面を表示します。

数字を一つ選ぶ場合は、第 2 引数の関数 f を呼び出して条件を満たしているかチェックします。条件を満たしている場合は配列 board の n 番目に x を書き込み、make_board を再帰呼び出しして次の数字を選びます。

チェック関数は匿名関数を使って定義します。引数 x が選んだ数字です。0, 1, 4, 7 番目の数字を選ぶ場合は無条件で true を返します。2 番目の数字を選ぶ場合は、1 番目の数字よりも大きいかチェックします。3 番目の数字を選ぶ場合は、0 番目の数字よりも大きいことと、0, 1, 2, 3 番目の数字の合計値が 20 になるかチェックします。board にセットされた数字の足し算は関数 add_number で行います。あとは同様に、5, 6, 8 番目の数字を選ぶときに条件を満たしているかチェックします。

それでは、実行結果を示します。

- solve();
1 6 8 5 2 4 9 3 7
2 4 9 5 1 6 8 3 7
2 6 7 5 3 4 8 1 9
3 4 8 5 2 6 7 1 9
4 2 9 5 1 8 6 3 7
4 3 8 5 2 7 6 1 9
val it = () : unit

解は全部で 6 通りになりました。

ところで、直線の値は 20 のほかにもいくつかあります。check_func_list を次のように修正するだけで簡単に求めることができます。

リスト:変形魔方陣の解法 (2)

val check_func_list = [
    fn(x) => true,                               (* 0 *)
    fn(x) => true,                               (* 1 *)
    fn(x) => Array.sub( board, 1 ) < x,          (* 2 *)
    fn(x) => Array.sub( board, 0 ) < x,          (* 3 *)
    fn(x) => true,                               (* 4 *)
    fn(x) => Array.sub( board, 4 ) < x,          (* 5 *)
    fn(x) => Array.sub( board, 3 ) < x andalso
             add_number( [3,4,5], 0 ) + x = add_number( [0,1,2,3], 0 ),  (* 6 *)
    fn(x) => true,                               (* 7 *)
    fn(x) => Array.sub( board, 7 ) < x andalso
             add_number( [0,6,7], 0 ) + x = add_number( [0,1,2,3], 0 ) ] (* 8 *)  

それでは、実行結果を示します。

- solve();
17:1 5 9 2 4 8 3 6 7
19:1 5 9 4 2 6 7 3 8
17:1 6 8 2 5 7 3 4 9
19:1 6 8 4 3 5 7 2 9
20:1 6 8 5 2 4 9 3 7
20:2 4 9 5 1 6 8 3 7
19:2 5 9 3 1 8 7 4 6
20:2 6 7 5 3 4 8 1 9
19:2 6 8 3 4 5 7 1 9
21:3 2 9 7 1 5 8 4 6
20:3 4 8 5 2 6 7 1 9
21:3 4 8 6 1 5 9 2 7
21:3 5 6 7 2 4 8 1 9
21:3 5 7 6 2 4 9 1 8
20:4 2 9 5 1 8 6 3 7
20:4 3 8 5 2 7 6 1 9
23:7 2 6 8 1 5 9 3 4
23:7 3 5 8 2 4 9 1 6
val it = () : unit

直線の値は 17, 19, 20, 21, 23 の 5 通りで、解は全部で 18 通りになりました。頂点に配置される数字の組み合わせは次のようになります。

17: (1, 2, 3)
19: (1, 4, 7), (2, 3, 7)
20: (1, 5, 9), (2, 5, 8), (3, 5, 7), (4, 5, 6)
21: (3, 6, 9), (3, 7, 8)
23: (7, 8, 9)

●問題3「チャイニーズ・チェッカー」の解答

それでは、プログラムを作りましょう。最小手数を求めるアルゴリズムといえば「幅優先探索」ですが、チャイニーズ・チェッカーは単純な「反復深化」でも解くことができます。

プログラムのポイントは、ペグを跳び越すときに手数も同時に数えていくことです。直前に動かしたペグと違うペグを動かすときは手数をカウントし、同じペグを動かすときは手数をカウントしません。これで連続跳び越しを 1 手と数えることができます。そして、この手数を使って反復深化を実行するわけです。

今回は、チャイニーズ・チェッカーの盤面をリストではなく、整数値のビットを使って表すことにします。つまり、ペグがある状態をビットオンで、ペグがない状態をビットオフで表します。位置とビットの対応は、下図の座標を参照してください。

                 ●                                  0                 
               /  \                              /  \               
             ●───●                          1───2             
           /  \  /  \                      /  \  /  \           
         ●───●───●                  3───4───5         
       /  \  /  \  /  \              /  \  /  \  /  \       
     ●───●───●───●          6───7───8───9     
   /  \  /  \  /  \  /  \      /  \  /  \  /  \  /  \   
 ●───●───○───●───●  10───11───12───13───14 

             (1) 盤面                            (2) 座標

                    図 : チャイニーズ・チェッカー

ペグの移動は跳び先表を用意すると簡単です。次のリストを見てください。

リスト : 跳び先表

(* 跳び先表 *)
val jump_table = #[
  [(1, 3), (2, 5)],                     (*  0 *)  
  [(3, 6), (4, 8)],                     (*  1 *)
  [(4, 7), (5, 9)],                     (*  2 *)
  [(1, 0), (4, 5), (6, 10), (7, 12)],   (*  3 *)
  [(7, 11), (8, 13)],                   (*  4 *)
  [(2, 0), (4, 3), (8, 12), (9, 14)],   (*  5 *)
  [(3, 1), (7, 8)],                     (*  6 *)
  [(4, 2), (8, 9)],                     (*  7 *)
  [(4, 1), (7, 6)],                     (*  8 *)
  [(5, 2), (8, 7)],                     (*  9 *)
  [(6, 3), (11, 12)],                   (* 10 *)
  [(7, 4), (12, 13)],                   (* 11 *)
  [(7, 3), (8, 5), (11, 10), (13, 14)], (* 12 *)
  [(8, 4), (12, 11)],                   (* 13 *)
  [(9, 5), (13, 12)]]                   (* 14 *)

ペグの跳び先表はベクタ jump_table で定義します。ベクタの要素はリストであることに注意してください。リストの要素は、跳び越されるペグの位置と跳び先の位置を格納した組 (int * int) です。たとえば、0 番の位置にあるペグは、1 番を跳び越して 3 番へ移動する場合と、2 番を跳び越して 5 番へ移動する場合の 2 通りがあります。これを組 (1, 3) と (2, 5) で表しています。

次はペグを操作する関数を作ります。

リスト : ペグの操作関数

(* ビットをセットする *)
fun set_bit( n ) = Word.<<( 0w1, Word.fromInt( n ) )

(* ペグの有無を返す *)
fun check_peg( board, n ) = Word.andb( board, set_bit( n ) ) <> 0w0

(* ペグを動かす *)
fun move_peg( board, from, del, to ) =
    Word.xorb( board, set_bit( from ) + set_bit( del ) + set_bit( to ) )  

(* 動かすペグを探す *)
fun search_peg( _, _, 0w0 ) = ()
|   search_peg( f, n, board ) = (
    if Word.andb( board, 0w1 ) <> 0w0 then f( n ) else ();
    search_peg( f, n + 1, Word.>>( board, 0w1 ) ))

今回のプログラムはペグをビットで表すので、盤面を表すデータ型は Word を使います。関数 set_bit は引数 n の位置のビットを 1 にセットした Word 型データを返します。関数 check_peg は盤面 board の n 番目にペグがあるかチェックします。関数 move_peg は盤面 board のペグを動かして、新しい盤面を返します。from は跳ぶペグの位置、del は跳び越されるペグの位置、to は跳び先の位置です。この処理は from, del, to の位置のビットを 1 にセットし、board との排他的論理和 (xor) を計算するだけです。

関数 search_peg は盤面 board にあるペグを探す関数です。第 1 引数がペグを見つけたときに実行する関数です。この関数には引数として見つけたペグの位置を渡します。第 2 引数がペグの位置、第 3 引数が盤面を表すデータです。search_peg は board を 1 ビット右シフトしながらペグを探していることに注意してください。第 3 引数が 0w0 の場合、board にペグは存在しないので再帰呼び出しを終了します。

次は反復深化で解を探す関数 solve_id を作ります。

リスト : 反復深化

fun solve_id( board, n, jc, limit, history ) =
  if n = max_jump
  then if check_peg( board, 12 )
       then (print_move( ~1, rev( history ) ); raise Exit)
       else ()
  else
    search_peg(fn(from) =>
                app (fn(del,to) =>
                      if check_peg( board, del ) andalso not( check_peg( board, to ) )
                      then
                        let
                          val new_board = move_peg( board, from, del, to )
                          val new_jc = jump_count( jc, from, history )
                        in
                          if new_jc <= limit 
                          then solve_id(new_board, n + 1, new_jc, limit, (from,to)::history)  
                          else ()
                        end
                      else ())
                    (Vector.sub( jump_table, from )),
               0, board)

引数 board が盤面、n がペグを動かした回数、jc が手数(跳んだ回数)、limit が反復深化の上限値、history がペグの移動手順(履歴)を表します。移動手順は (跳ぶペグの位置, 跳び先の位置) を組にしてリストに格納して表します。

チャイニーズ・チェッカーの場合、ペグの総数は 14 個なので、13 回 (max_jump) ペグを移動するとペグの個数は 1 つになります。そして、そのペグが 12 番目にあるならば、解を見つけることができました。print_move で手順を表示し、raise で例外 Exit を送出して処理を終了します。

そうでなければペグを移動します。search_peg でペグを探し、匿名関数 fn(from) => app (fn(del,to) => ... ) ... でペグを移動します。最初の匿名関数は search_peg から呼び出され、引数 from には動かすペグの位置が与えられます。次に関数 app で、跳び先表 jump_table から跳び越すペグの位置 del と跳び先の位置 to を求め、2 番目の匿名関数 fn(del, to) => ... に渡します。

ここで、del の位置にペグがあり to の位置が空であればペグを移動することができます。関数 move_peg でペグを移動し、関数 jump_count で連続跳び越しの回数を求めます。history の先頭要素 (PrevFrom, PrevTo) を求め、PrevTo が from と等しい場合は連続跳び越しと判断することができます。そして、new_jc <= limit であれば solve_id を再帰呼び出しします。ペグ・ソリテアを反復深化で解く場合、上限値 limit に達していても連続跳びによりペグを移動できることに注意してください。

最後に、手順を表示する関数 print_move と solve_id を呼び出す関数 solve を作ります。

リスト : チャイニーズ・チェッカーの解法

(* 手順を表示 *)
fun print_move( x, nil ) = print( ")\n" )
|   print_move( x, (from, to)::z ) =
    if x = ~1
    then (print( "(" ^ Int.toString(from) ^ "," ^ Int.toString(to) );
          print_move( to, z ))
    else if x = from
    then (print( "," ^ Int.toString(to)); print_move( to, z ))
    else (print( ")(" ^ Int.toString(from) ^ "," ^ Int.toString(to)); print_move( to, z ))  

(* チャイニーズ・チェッカーの解法 *)
fun solve() =
    let
      val limit = ref 1
      val board = Word.xorb( 0wx7fff, set_bit( 12 ) )
    in
      (while !limit <= max_jump do (
        print( Int.toString( !limit ) ^ "moves\n" );
        solve_id( board, 0, 0, !limit, nil );
        limit := !limit + 1)) handle Exit => ()
    end

移動手順は 1 手を (from, to) で表し、連続跳び越しの場合は (from, to1, to2, ..., to3) とします。print_move の第 1 引数 x が 1 手前の跳び先の位置を表します。~1 の場合は最初の移動です。"(from, to" を表示して print_move を再帰呼び出しします。このとき、第 1 引数の値に to を渡します。

x と from が等しい場合は連続跳び越しです。", to" を表示して print_move を再帰呼び出しします。それ以外の場合は連続跳び越しではないので、")(form,to" を表示して print_move を再帰呼び出しします。最後に第 2 引数が nil になったら ")\n" を表示して終了します。

関数 solve の ref 変数 limit が上限値、変数 board が盤面を表します。0wx7fff は全ての位置にペグがある状態で、set_bit( 12 ) と排他的論理和 (xor) を計算することで 12 番目のビットを 0 にしています。あとは while ループで上限値 limit を一つずつ増やしながら、チャイニーズ・チェッカーの解を探索します。

あとは特に難しいところはないと思います。詳細は プログラムリスト をお読みくださいませ。

●実行結果

これでプログラムは完成です。実行結果は次のようになりました。

- solve();
1moves
2moves
3moves
4moves
5moves
6moves
7moves
8moves
9moves
(10,12)(13,11)(3,10,12,3)(1,6)(5,3)(6,1)(14,5)(2,9,7)(0,3,12)

最短手数は 9 手になりました。実行時間は M.Hiroi のオンボロマシン (Windows 95, Pentium 166 MHz) で約 35 秒でした。「下限値枝刈り法」を使うと、もう少し速くなると思います。興味のある方は挑戦してみてください。


●プログラムリスト

(*
 * peg15.sml : チャイニーズ・チェッカーの解法
 *
 *             Copyright (C) 2005 Makoto Hiroi
 *)

(* 例外 *)
exception Exit

(* 定数 *)
val max_jump = 13

(* 跳び先表 (del, to) *)
val jump_table = #[
  [(1, 3), (2, 5)],                     (*  0 *)
  [(3, 6), (4, 8)],                     (*  1 *)
  [(4, 7), (5, 9)],                     (*  2 *)
  [(1, 0), (4, 5), (6, 10), (7, 12)],   (*  3 *)
  [(7, 11), (8, 13)],                   (*  4 *)
  [(2, 0), (4, 3), (8, 12), (9, 14)],   (*  5 *)
  [(3, 1), (7, 8)],                     (*  6 *)
  [(4, 2), (8, 9)],                     (*  7 *)
  [(4, 1), (7, 6)],                     (*  8 *)
  [(5, 2), (8, 7)],                     (*  9 *)
  [(6, 3), (11, 12)],                   (* 10 *)
  [(7, 4), (12, 13)],                   (* 11 *)
  [(7, 3), (8, 5), (11, 10), (13, 14)], (* 12 *)
  [(8, 4), (12, 11)],                   (* 13 *)
  [(9, 5), (13, 12)]]                   (* 14 *)

(* ペグをセットする *)
fun set_bit( n ) = Word.<<( 0w1, Word.fromInt( n ) )

(* ペグの有無を返す *)
fun check_peg( board, n ) = Word.andb( board, set_bit( n ) ) <> 0w0

(* ペグを動かす *)
fun move_peg( board, from, del, to ) =
    Word.xorb( board, set_bit( from ) + set_bit( del ) + set_bit( to ) )

(* 手順を表示 *)
fun print_move( x, nil ) = print( ")\n" )
|   print_move( x, (from, to)::z ) =
    if x = ~1
    then (print( "(" ^ Int.toString(from) ^ "," ^ Int.toString(to) );
          print_move( to, z ))
    else if x = from
    then (print( "," ^ Int.toString(to)); print_move( to, z ))
    else (print( ")(" ^ Int.toString(from) ^ "," ^ Int.toString(to)); print_move( to, z ))

(* ジャンプのカウント *)
fun jump_count( jc, _, nil ) = jc + 1
|   jump_count( jc, from, (x, y)::z ) =
    if from = y then jc else jc + 1

(* 動かすペグを探す *)
fun search_peg( _, _, 0w0 ) = ()
|   search_peg( f, n, board ) = (
    if Word.andb( board, 0w1 ) <> 0w0 then f( n ) else ();
    search_peg( f, n + 1, Word.>>( board, 0w1 ) ))

(* 反復深化 *)
fun solve_id( board, n, jc, limit, history ) =
  if n = max_jump
  then if check_peg( board, 12 )
       then (print_move( ~1, rev( history ) ); raise Exit)
       else ()
  else
    search_peg(fn(from) =>
                app (fn(del,to) =>
                      if check_peg( board, del ) andalso not( check_peg( board, to ) )
                      then
                        let
                          val new_board = move_peg( board, from, del, to )
                          val new_jc = jump_count( jc, from, history )
                        in
                          if new_jc <= limit 
                          then solve_id(new_board, n + 1, new_jc, limit, (from,to)::history)
                          else ()
                        end
                      else ())
                    (Vector.sub( jump_table, from )),
               0, board)

(* チャイニーズ・チェッカーの解法 *)
fun solve() =
    let
      val limit = ref 1
      val board = Word.xorb( 0wx7fff, set_bit( 12 ) )
    in
      (while !limit <= max_jump do (
        print( Int.toString( !limit ) ^ "moves\n" );
        solve_id( board, 0, 0, !limit, nil );
        limit := !limit + 1)) handle Exit => ()
    end

●問題4「7パズル」の解答

それではプログラムを作りましょう。7パズルの盤面はリストで表します。盤面の位置と隣接リストは次のようになります。

  ┌─┬─┬─┬─┐  
  │0│1│2│3│  
  ├─┼─┼─┼─┤  
  │4│5│6│7│  
  └─┴─┴─┴─┘  
      盤面の位置      

  図 : 7パズルの盤面
リスト : 隣接リスト

val neighbor = [
    [1, 4],    (* 0 *)  
    [0, 2, 5], (* 1 *)
    [1, 6, 3], (* 2 *)
    [2, 7],    (* 3 *)
    [0, 5],    (* 4 *)
    [1, 4, 6], (* 5 *)
    [2, 5, 7], (* 6 *)
    [3, 6]]    (* 7 *)

あとは 幅優先探索とスライドパズル (2) で作成した「6パズル」の最長手数を求めるプログラムとほとんど同じです。特に難しいところはないので、説明は省略いたします。詳細は プログラムリスト をお読みくださいませ。

それでは実行結果を示します。

- solve();
max = 36
0 7 2 1 4 3 6 5
val it = () : unit

最長手数は 36 手で、その配置は 1 通りしかありません。図に示すと次のようになります。

┌─┬─┬─┬─┐    ┌─┬─┬─┬─┐  
│1│2│3│4│    │  │7│2│1│
├─┼─┼─┼─┤ => ├─┼─┼─┼─┤
│5│6│7│  │    │4│3│6│5│
└─┴─┴─┴─┘    └─┴─┴─┴─┘
      完成形            最長手数 (36手)

        図 : 7パズルの最長手数

興味のある方は 36 手で解く手順を求めてみてください。


●プログラムリスト

(*
 * seven.sml : 7 パズル
 *
 *                Copyright (C) 2005 Makoto Hiroi
 *)

(* ハッシュ法 *)
signature HASHITEM = sig
  type item
  val size : int
  val hash_func : item -> int
  val equal : item * item -> bool
end

functor makeHash( Item: HASHITEM ) = struct
  (* データ型の定義 *)
  datatype 'a hash = Hash of 'a array

  (* ハッシュ表の生成 *)
  fun create() = Hash( Array.array( Item.size, nil: Item.item list ) )

  (* データの探索 *)
  fun search( data, Hash( table ) ) =
      let
        val n = Item.hash_func( data )
        val L = Array.sub( table, n )
      in
        List.find (fn(x) => Item.equal( x, data )) L
      end

  (* データの挿入 *)
  fun insert( data, Hash( table ) ) =
      let
        val n = Item.hash_func( data )
        val L = Array.sub( table, n )
      in
        if List.exists (fn(x) => Item.equal( x, data )) L
        then false
        else (Array.update( table, n, data::L ); true)
      end

  (* データの削除 *)
  fun delete( data, Hash( table ) ) =
      let
        fun remove( nil ) = nil
        |   remove( x::xs ) =
            if Item.equal( data, x ) then xs else remove( xs )
        val n = Item.hash_func( data )
        val L = Array.sub( table, n )
      in
        Array.update( table, n, remove( L ) )
      end
end

(* int list *)
structure IntListItem : HASHITEM = struct
    type item = int list
    val size = 19997
    fun hash_func( x ) = 
        (foldl (fn(a, b) => b * 10 + a) 0 x) mod size
    fun equal( a, b ) = a = b
end

structure IntListHash = makeHash( IntListItem )

(* キューとハッシュ表の定義 *)
val max_state = 20160   (* 8! / 2 *)
val state_table = Array.array( max_state, nil: int list )
val move_table  = Array.array( max_state, 0 )
val space_table = Array.array( max_state, 0 )
val hash_table = IntListHash.create()

(* 隣接リスト *)
val adjacent = [
    [1, 4],    (* 0 *)
    [0, 2, 5], (* 1 *)
    [1, 6, 3], (* 2 *)
    [2, 7],    (* 3 *)
    [0, 5],    (* 4 *)
    [1, 4, 6], (* 5 *)
    [2, 5, 7], (* 6 *)
    [3, 6]]    (* 7 *)

(* 駒の移動 *)
fun move_piece( _, nil ) = nil
|   move_piece( a, x::xs ) =
    if x = a then 0 :: move_piece( a, xs )
    else if x = 0 then a :: move_piece( a, xs )
    else x :: move_piece( a, xs )

(* int list の表示 *)
fun print_intlist( nil ) = print( "\n" )
|   print_intlist( x::xs ) =
    ( print( Int.toString(x) ^ " " ); print_intlist( xs ) )

(* 最長手数の局面を表示 *)
fun print_answer( n ) =
    let
      val max = Array.sub( move_table, n )
      val i = ref n
    in
      print("max = " ^ Int.toString( max ) ^ "\n");
      while Array.sub( move_table, !i ) = max do (
        print_intlist( Array.sub( state_table, !i ) );
        i := !i - 1)
    end

(* 新しい局面を作る *)
fun make_new_state( rear, front, space, nil ) = rear
|   make_new_state( rear, front, space, x::xs ) =
    let
      val state = Array.sub( state_table, front );
      val new_state = move_piece( List.nth( state, x ), state )
    in
      if IntListHash.insert( new_state, hash_table )
      then (
        Array.update( state_table, rear, new_state );
        Array.update( move_table, rear, Array.sub( move_table, front ) + 1 );
        Array.update( space_table, rear, x );
        make_new_state( rear + 1, front, space, xs )
      ) else make_new_state( rear, front, space, xs )
    end

(* 幅優先探索 *)
fun solve_seven( front, rear ) =
    if front >= rear then print_answer( rear - 1 )
    else
      let
        val space = Array.sub( space_table, front )
      in
        solve_seven( front + 1, 
                     make_new_state( rear, front, space,
                                     List.nth( adjacent, space )))
      end

fun solve() = (
    (* 初期化 *)
    Array.update( state_table, 0, [1,2,3,4,5,6,7,0] );
    Array.update( move_table,  0, 0 );
    Array.update( space_table, 0, 7 );
    IntListHash.insert( [1,2,3,4,5,6,7,0], hash_table );
    solve_seven( 0, 1 ))

Copyright (C) 2005 Makoto Hiroi
All rights reserved.

[ PrevPage | SML/NJ | NextPage ]