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

Puzzle DE Programming

スライディングブロックパズル (2)

[ Home | Puzzle ]

問題の説明

今回は「15 パズルの変形版」を解いてみましょう。

[問題]
次に示す局面から始めて、最長手数となる配置を求めてください。何手になるでしょうか。
 ┌─┬─┬─┬─┐  ┌─┬─┬─┬─┐
 ││○││  ││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 │●│●│●│●│  ││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 │●│●│●│●│  ││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 ││●│●│  │  ││  │
 └─┴─┴─┴─┘  └─┴─┴─┴─┘ 
       (1)              (2)

                図:問題

(1) の場合、赤、青、黄色、水色、桃色、空白の場所がひとつずつで、残りが 10 個が黒ですから、駒の配置は 16 * 15 * 14 * 13 * 12 * 11 = 5,765,760 通りあります。(2) の場合は、空白の場所が 16 通りあり、緑の駒の配置は 155 = 3003 通り、赤の駒の配置は 105 = 252 通りあるので、駒の配置は 16 * 3003 * 252 = 12,108,096 通りあります。どちらも大きな数なので、今回は省メモリな方法を使いましょう。

最小完全ハッシュ関数とその逆関数を作成して、手数を格納する配列を用意します。最初に、配列を NIL (-1) に初期化しておきます。そして、初期状態(0 手)から始めて、1 手で到達する局面を生成して、配列に 1 を書き込みます。次に、配列から 1 手の局面を検索して、2 手で到達する局面を生成します。このとき、配列の値が NIL 以外の値であれば、すでに生成した局面であることがわかります。

あとは手数を延ばしていくだけです。巨大な配列を検索するのは時間がかかりますが、ここは高速 CPU にがんばってもらいましょう。


●問題 (1) の解法

それでは、問題 (1) から解いていきましょう。いつものように、空いている場所を 0 で表し、ほかの駒を 1 から 6 までの数字で表すことにします。そうすると、初期状態は下記リストのようになります。

リスト:初期状態

char init_state[SIZE] = {
  1, 2, 3, 4,
  6, 6, 6, 6,
  6, 6, 6, 6,
  5, 6, 6, 0,
};

最小完全ハッシュ関数は、幅優先探索の高速化(1) で説明した「N!通りのパターンに 0 から N - 1 までの番号を付ける方法」と同様に作成することができます。次のプログラムを見てください。

リスト:最小完全ハッシュ関数

/* 数値表 */
const int table[6] ={
  360360,   /* 15 * 14 * 13 * 12 * 11 */
  24024,    /* 14 * 13 * 12 * 11 */
  1716,     /* 13 * 12 * 11 */
  132,      /* 12 * 11 */
  11,
  1,
};

/* 盤面を数値に変換 */
int board_to_number( char *board )
{
  int buffer[6];       /* 0 - 5 までの位置を格納 */
  int i, j, value = 0;
  for( i = 0; i < SIZE; i++ ){
    int p = board[i];
    if( p < 6 ) buffer[p] = i;
  }
  for( i = 0; i < 6; i++ ){
    value += table[i] * buffer[i];
    for( j = i + 1; j < 6; j++ ){
      if( buffer[i] < buffer[j] ) buffer[j]--;
    }
  }
  return value;
}

最初のループで、0 から 5 までの位置を求めて buffer に格納します。次のループで、ハッシュ値を計算します。やっていることは、「N!通りのパターンに 0 から N - 1 までの番号を付ける方法」と同じです。これで局面を 0 から 5,765,759 までの数値に変換することができます。

次は、逆変換(数値を盤面に変換)を行う関数を作りましょう。

リスト:数値を盤面に変換する

int number_to_board( int num, char *board )
{
  int i, j;
  char buffer[6];
  memset( board, 6, SIZE );
  for( i = 0; i < 5; i++ ){
    buffer[i] = num / table[i];
    num %= table[i];
  }
  buffer[i] = num;
  for( i = 4; i >= 0; i-- ){
    for( j = i + 1; j < 6; j++ ){
      if( buffer[i] <= buffer[j] ) buffer[j]++;
    }
  }
  for( i = 0; i < 6; i++ ){
    board[ buffer[i] ] = i;
  }
  return buffer[0];    /* 空き場所の位置を返す */
}

関数 number_to_board は空き場所の位置を返します。まず最初のループで、数値を 6 つの数字へ分解します。この段階では、数字はまだ駒の位置を表していません。次のループで、ハッシュ関数と逆の操作を行うことで、駒の位置を求めます。逆方向から数字をチェックし、数字が大きいか等しい場合は、その数字に 1 を加えます。これで駒の位置を復元することができます。あとは、盤面に駒をセットすればいいわけです。

次は駒を移動する処理を作りましょう。

リスト:駒の移動処理

int move_piece( int num, int move )
{
  char board[SIZE];
  int  c = 0;              /* 展開した局面数 */
  int  s, i, n, new_num;

  s = number_to_board( num, board );
  for( i = 0; (n = adjacent[s][i]) != -1; i++ ){
    /* 移動 */
    board[s] = board[n];
    board[n] = 0;
    new_num = board_to_number( board );
    if( check_table[new_num] == NIL ){
      /* 登録 */
      check_table[new_num] = move;
      c++;
    }
    /* 元に戻す */
    board[n] = board[s];
    board[s] = 0;
  }
  return c;
}

引数 num が局面を表す数値で、move が手数を表します。プログラムはとても簡単です。まず、number_to_board で数値を局面に変換し、それから駒を移動します。次に、新しい局面を board_to_number で数値に変換し、手数を格納する配列 check_table をチェックします。check_table は NIL (-1) で初期化してあるので、NIL 以外の値であれば古い局面、NIL であれば新しい局面であることがわかります。新しい局面であれば、check_table に move を書き込んで登録します。

探索処理を行う関数 search も簡単です。

リスト:探索処理

void search( void )
{
  int num, total = 1, move = 1;
  /* 初手を展開 */
  num = board_to_number( init_state );
  check_table[num] = 0;        /* 初期値 */
  total += move_piece( num, move );
  printf("%2d手 --- %d個\n", move, total );

  /* 2手目以降の展開 */
  for( ; ; move++ ){
    int i, c = 0;
    for( i = 0; i < MAX; i++ ){
      if( check_table[i] == move ){
        c += move_piece( i, move + 1 );
      }
    }
    if( c == 0 ) break;
    printf("%2d手 --- %d個\n", move + 1, c );
    total += c;
  }
  print_answer( move );
  printf("合計 %d 個\n", total );
}

最初に、初期状態を board_to_number で数値に変換し、配列 check_table に登録します。そして、初期状態から 1 手目を展開します。2 手目以降は、配列 check_table から move 手の局面を検索し、駒を移動して move + 1 手の局面を作成します。あとは move を 1 手ずつ増やしていき、新しい局面が作れなくなった時点でループから脱出します。最後に、最長手数となる局面を print_answer で出力します。

あとのプログラムは簡単なので説明は省略します。プログラムリスト1 を参照してください。

●実行結果

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

1手 --- 3個
2手 --- 3個
3手 --- 4個
・・・省略・・・
56手 --- 22個
57手 --- 3個
58手 --- 1個
0 6 6 5 6 6 6 6 6 6 6 6 4 2 3 1 
合計 5765760 個

実行時間はオンボロマシン(Pentium 166 MHz)で約 97 秒でした。結果を図で表すと次のようになります。

 ┌─┬─┬─┬─┐  ┌─┬─┬─┬─┐
 ││○││  │  │●│●││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 │●│●│●│●│  │●│●│●│●│
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 │●│●│●│●│  │●│●│●│●│
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 ││●│●│  │  ││○││
 └─┴─┴─┴─┘  └─┴─┴─┴─┘ 
     START           58 moves

            図:問題(1) 解答

●問題 (2) の解法

次は問題 (2) を解きます。空き場所を 0 とし、駒を数値 1, 2, 3 で表すと、初期状態は下記リストのようになります。

リスト:初期状態

char init_state[SIZE] = {
  1, 1, 2, 2,
  1, 1, 2, 2,
  1, 3, 3, 2,
  3, 3, 3, 0,
};

最小完全ハッシュ関数は、スライディングブロックパズル(1) で説明した「組み合わせに番号をつける方法」の応用です。駒 1 の場合、空き場所は無視して 2 と 3 を同じ駒と考えれば、配置は 155 = 3003 通りになります。駒 2 の場合、駒 1 と空き場所を無視すると、配置は 105 = 252 通りとなります。これをプログラムすると、次のようになります。

リスト:最小完全ハッシュ関数

/* 盤面を数値に変換 */
int board_to_number( char *board )
{
  int space = 0, value1 = 0, value2 = 0;
  int i, n1 = 15, r1 = 5, n2 = 10, r2 = 5;
  for (i = 0; i < SIZE; i++){
    switch( board[i] ){
    case 0:
      space = i; break;
    case 1:
      if( n1 > r1 && r1 > 0 ){
        value1 += comb_table[--n1][r1--];
      }
      break;
    case 2:
      if( n2 > r2 && r2 > 0 ){
        value2 += comb_table[--n2][r2--];
      }
      n1--;
      break;
    default:
      n1--;
      n2--;
    }
  }
  return space * 3003 * 252 + value1 * 252 + value2;
}

n1, r1, value1 が駒 1 用の変数で、n2, r2, value2 が駒 2 用の変数です。配列 comb_table は「パスカルの三角形」を表しています。最初に、空き場所の位置を space にセットします。どちらの駒も空き場所は無視するので、n1 と n2 の値はそのままにします。

駒 1 の場合、駒 2 と 3 を同じ駒として扱うので、n1 の値は駒 2 だけではなく駒 3 の場合もデクリメントします。駒 2 の場合は駒 1 を無視するので、n2 の値は駒 1 ではそのままとし、駒 3 のときにデクリメントします。これで、駒 1 と駒 2 の配置を数値に変換することができます。

次は、数値を盤面に変換する関数 number_to_board を作ります。

リスト:数値を盤面に変換

int number_to_board( int num, char *board )
{
  int space, value1, value2, temp;
  space  = num / (3003 * 252);
  temp   = num % (3003 * 252);
  value1 = temp / 252;
  value2 = temp % 252;

  memset( board, 3, SIZE );
  board[space] = 0;
  decode( value1, board, 15, 5, 1 );
  decode( value2, board, 10, 5, 2 );
  return space;
}

最初に、空き場所の位置を space に、駒 1 の配置を value1 に、駒 2 の配置を value2 にセットします。次に、配列 board を駒 3 で埋めて、空き場所の位置をセットします。駒 1 と駒 2 のセットは関数 decode で行います。

リスト:組み合わせの数値を変換

void decode( int value, char *board, int n, int r, int piece )
{
  int i = 0;
  while( n > r && r > 0 ){
    int c = comb_table[--n][r];
    while( board[i] != 3 ) i++;
    if( value >= c ){
      board[i] = piece;
      r--;
      value -= c;
    }
    i++;
  }
  while( r > 0 ){
    while( board[i] != 3 ) i++;
    board[i++] = piece;
    r--;
  }
}

基本的には、ハッシュ関数の逆変換を行うだけです。変数 i が配列 board に書き込む位置を表します。無視しなければならない駒があるため、board の中で駒 3 以外の場所はスキップします。それから値をチェックして、組み合わせの数以上の場合は駒 piece を書き込みます。そうでなければ、その位置は駒 3 のままです。最後に、r が 0 より大きい場合は、残った駒を board にセットします。

あとのプログラムは「問題 (1)」のプログラムと同じです。説明は省略しますので、プログラムリスト2 を参照してください。

●実行結果

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

 1手 --- 3個
 2手 --- 4個
 3手 --- 9個
・・・省略・・・
55手 --- 119個
56手 --- 27個
57手 --- 2個
3 0 3 3 2 3 3 1 2 2 1 1 2 2 1 1
3 3 3 3 2 2 3 1 2 2 1 1 0 2 1 1
合計 12108096 個

実行時間はオンボロマシン(Pentium 166 MHz)で約 204 秒でした。結果を図で表すと次のようになります。

 ┌─┬─┬─┬─┐  ┌─┬─┬─┬─┐  ┌─┬─┬─┬─┐
 ││  ││  ││  ││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 ││  ││  ││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 ││  ││  ││
 ├─┼─┼─┼─┤  ├─┼─┼─┼─┤  ├─┼─┼─┼─┤
 ││  │  │  ││  ││
 └─┴─┴─┴─┘  └─┴─┴─┴─┘  └─┴─┴─┴─┘ 
     START           57 moves            57 moves

                      図:問題(2) 解答

●プログラムリスト1

/*
  15パズルの変形バージョン(その2)

                  Copryright (C) 2001 by M.Hiroi

  次の配置の最長手数を求める

  1 2 3 4
  6 6 6 6
  6 6 6 6
  5 6 6 0
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define TRUE  1
#define FALSE 0

#define NIL (-1)
#define SIZE 16
#define MAX  5765760

/* 隣接リスト

   座標
    0  1  2  3
    4  5  6  7
    8  9 10 11
   12 13 14 15

 */
const char adjacent[SIZE][5] = {
   1,  4, -1, -1, -1,  /* 0 */
   0,  2,  5, -1, -1,  /* 1 */
   1,  3,  6, -1, -1,  /* 2 */
   2,  7, -1, -1, -1,  /* 3 */
   0,  5,  8, -1, -1,  /* 4 */
   1,  4,  6,  9, -1,  /* 5 */
   2,  5,  7, 10, -1,  /* 6 */
   3,  6, 11, -1, -1,  /* 7 */
   4,  9, 12, -1, -1,  /* 8 */
   5,  8, 10, 13, -1,  /* 9 */
   6,  9, 11, 14, -1,  /* 10 */
   7, 10, 15, -1, -1,  /* 11 */
   8, 13, -1, -1, -1,  /* 12 */
   9, 12, 14, -1, -1,  /* 13 */
  10, 13, 15, -1, -1,  /* 14 */
  11, 14, -1, -1, -1,  /* 15 */
};

/* 初期状態 */
char init_state[SIZE] = {
  1, 2, 3, 4,
  6, 6, 6, 6,
  6, 6, 6, 6,
  5, 6, 6, 0,
};

/* 手数を格納するテーブル */
char *check_table;

/* 数値表 */
const int table[6] ={
  360360,   /* 15 * 14 * 13 * 12 * 11 */
  24024,    /* 14 * 13 * 12 * 11 */
  1716,     /* 13 * 12 * 11 */
  132,      /* 12 * 11 */
  11,
  1,
};

/* 盤面を数値に変換 */
int board_to_number( char *board )
{
  int buffer[6];       /* 0 - 5 までの位置を格納 */
  int i, j, value = 0;
  for( i = 0; i < SIZE; i++ ){
    int p = board[i];
    if( p < 6 ) buffer[p] = i;
  }
  for( i = 0; i < 6; i++ ){
    value += table[i] * buffer[i];
    for( j = i + 1; j < 6; j++ ){
      if( buffer[i] < buffer[j] ) buffer[j]--;
    }
  }
  return value;
}

/* 数値を盤面に変換:空白の位置を返す */
int number_to_board( int num, char *board )
{
  int i, j;
  char buffer[6];
  memset( board, 6, SIZE );
  for( i = 0; i < 5; i++ ){
    buffer[i] = num / table[i];
    num %= table[i];
  }
  buffer[i] = num;
  for( i = 4; i >= 0; i-- ){
    for( j = i + 1; j < 6; j++ ){
      if( buffer[i] <= buffer[j] ) buffer[j]++;
    }
  }
  for( i = 0; i < 6; i++ ){
    board[ buffer[i] ] = i;
  }
  return buffer[0];
}

/* 初期化 */
void init( void )
{
  check_table = malloc( MAX );
  if( check_table == NULL ){
    fprintf(stderr, "out of memory\n"); exit( 1 );
  }
  memset( check_table, NIL, MAX );
}

/* 駒を移動する */
int move_piece( int num, int move )
{
  char board[SIZE];
  int  c = 0;                   /* 展開した局面数 */
  int  s, i, n, new_num;

  s = number_to_board( num, board );
  for( i = 0; (n = adjacent[s][i]) != -1; i++ ){
    /* 移動 */
    board[s] = board[n];
    board[n] = 0;
    new_num = board_to_number( board );
    if( check_table[new_num] == NIL ){
      /* 登録 */
      check_table[new_num] = move;
      c++;
    }
    /* 元に戻す */
    board[n] = board[s];
    board[s] = 0;
  }
  return c;
}

/* 最長手数の局面を表示 */
void print_answer( int move )
{
  int i;
  for( i = 0; i < MAX; i++ ){
    if( check_table[i] == move ){
      int j;
      char board[SIZE];
      number_to_board( i, board );
      for( j = 0; j < SIZE; j++ ){
        printf("%d ", board[j] );
      }
      printf("\n");
    }
  }
}

/* 探索 */
void search( void )
{
  int num, total = 1, move = 1;

  /* 初手を展開 */
  num = board_to_number( init_state );
  check_table[num] = 0;             /* 初期値 */
  total += move_piece( num, move );
  printf("%2d手 --- %d個\n", move, total );

  /* 2手目以降の展開 */
  for( ; ; move++ ){
    int i, c = 0;
    for( i = 0; i < MAX; i++ ){
      if( check_table[i] == move ){
        c += move_piece( i, move + 1 );
      }
    }
    if( c == 0 ) break;
    printf("%2d手 --- %d個\n", move + 1, c );
    total += c;
  }
  print_answer( move );
  printf("合計 %d 個\n", total );
}

int main()
{
  int start, end;
  start = clock();
  init();
  search();
  end = clock();
  printf("時間 %d\n", end - start );
  return 0;
}

●プログラムリスト2

/*
 *  変形版スライドパズル(その3)
 *
 *            Copyright (C) 2001 by M.Hiroi
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define TRUE  1
#define FALSE 0

#define NIL  (-1)
#define SIZE 16
#define MAX  12108096

/* 隣接リスト

   座標
    0  1  2  3
    4  5  6  7
    8  9 10 11
   12 13 14 15

 */
const char adjacent[SIZE][5] = {
   1,  4, -1, -1, -1,  /* 0 */
   0,  2,  5, -1, -1,  /* 1 */
   1,  3,  6, -1, -1,  /* 2 */
   2,  7, -1, -1, -1,  /* 3 */
   0,  5,  8, -1, -1,  /* 4 */
   1,  4,  6,  9, -1,  /* 5 */
   2,  5,  7, 10, -1,  /* 6 */
   3,  6, 11, -1, -1,  /* 7 */
   4,  9, 12, -1, -1,  /* 8 */
   5,  8, 10, 13, -1,  /* 9 */
   6,  9, 11, 14, -1,  /* 10 */
   7, 10, 15, -1, -1,  /* 11 */
   8, 13, -1, -1, -1,  /* 12 */
   9, 12, 14, -1, -1,  /* 13 */
  10, 13, 15, -1, -1,  /* 14 */
  11, 14, -1, -1, -1,  /* 15 */
};

/* 初期状態 */
char init_state[SIZE] = {
  1, 1, 2, 2,
  1, 1, 2, 2,
  1, 3, 3, 2,
  3, 3, 3, 0,
};

/* 手数を格納するテーブル */
char *check_table;

/* パスカルの三角形 */
int comb_table[15][15];

/* 初期化 */
void init_comb_table( void )
{
  int i, j;
  comb_table[0][0] = 1;
  for( i = 1; i < 15; i++ ){
    comb_table[i][0] = comb_table[i][i] = 1;
    for( j = 1; j < i; j++ ){
      comb_table[i][j] = comb_table[i -1][j - 1] + comb_table[i - 1][j];
    }
  }
}

/* 盤面を数値に変換 */
int board_to_number( char *board )
{
  int space = 0, value1 = 0, value2 = 0;
  int i, n1 = 15, r1 = 5, n2 = 10, r2 = 5;
  for (i = 0; i < SIZE; i++){
    switch( board[i] ){
    case 0:
      space = i; break;
    case 1:
      if( n1 > r1 && r1 > 0 ){
        value1 += comb_table[--n1][r1--];
      }
      break;
    case 2:
      if( n2 > r2 && r2 > 0 ){
        value2 +=comb_table[--n2][r2--];
      }
      n1--;
      break;
    default:
      n1--;
      n2--;
    }
  }
  return space * 3003 * 252 + value1 * 252 + value2;
}

/* 変換 */
void decode( int value, char *board, int n, int r, int piece )
{
  int i = 0;
  while( n > r && r > 0 ){
    int c = comb_table[--n][r];
    while( board[i] != 3 ) i++;
    if( value >= c ){
      board[i] = piece;
      r--;
      value -= c;
    }
    i++;
  }
  while( r > 0 ){
    while( board[i] != 3 ) i++;
    board[i++] = piece;
    r--;
  }
}

/* 数値を盤面に変換 */
int number_to_board( int num, char *board )
{
  int space, value1, value2, temp;
  space  = num / (3003 * 252);
  temp   = num % (3003 * 252);
  value1 = temp / 252;
  value2 = temp % 252;

  memset( board, 3, SIZE );
  board[space] = 0;
  decode( value1, board, 15, 5, 1 );
  decode( value2, board, 10, 5, 2 );
  return space;
}

/* 初期化 */
void init( void )
{
  check_table = malloc( MAX );
  if( check_table == NULL ){
    fprintf(stderr, "out of memory\n"); exit( 1 );
  }
  memset( check_table, NIL, MAX );
  init_comb_table();
}

/* 駒を移動する */
int move_piece( int num, int move )
{
  char board[SIZE];
  int  c = 0;                   /* 展開した局面数 */
  int  s, i, n, new_num;

  s = number_to_board( num, board );
  for( i = 0; (n = adjacent[s][i]) != -1; i++ ){
    /* 移動 */
    board[s] = board[n];
    board[n] = 0;
    new_num = board_to_number( board );
    if( check_table[new_num] == NIL ){
      /* 登録 */
      check_table[new_num] = move;
      c++;
    }
    /* 元に戻す */
    board[n] = board[s];
    board[s] = 0;
  }
  return c;
}

/* 最長手数の局面を表示 */
void print_answer( int move )
{
  int i;
  for( i = 0; i < MAX; i++ ){
    if( check_table[i] == move ){
      int j;
      char board[SIZE];
      number_to_board( i, board );
      for( j = 0; j < SIZE; j++ ){
        printf("%d ", board[j] );
      }
      printf("\n");
    }
  }
}

/* 探索 */
void search( void )
{
  int num, total = 1, move = 1;

  /* 初手を展開 */
  num = board_to_number( init_state );
  check_table[num] = 0;             /* 初期値 */
  total += move_piece( num, move );
  printf("%2d手 --- %d個\n", move, total );

  /* 2手目以降の展開 */
  for( ; ; move++ ){
    int i, c = 0;
    for( i = 0; i < MAX; i++ ){
      if( check_table[i] == move ){
        c += move_piece( i, move + 1 );
      }
    }
    if( c == 0 ) break;
    printf("%2d手 --- %d個\n", move + 1, c );
    total += c;
  }
  print_answer( move );
  printf("合計 %d 個\n", total );
}

int main()
{
  int start, end;
  start = clock();
  init();
  search();
  end = clock();
  printf("時間 %d\n", end - start );
  return 0;
}

Copyright (C) 2000-2003 Makoto Hiroi
All rights reserved.

[ Home | Puzzle ]