フリップ・イット

今回取り上げるパズルは、フリップ・イットです。 オリジナルとは少し異なっていますが、ルールは次の通りです。

 最短手数  8:         
   
 最短手数 18:         
   
 最短手数 16:         
   
スタート図  完成図

3つのスタート図は、JavaScript が有効で version 1.1 以降あれば、 実際に動かして遊べるようになっています。 (空き升目とそのとなりの升目を除く) 升目をマウスでクリックすると、石の升目は空き升目に移動し、 中間の升目は反転します。パズルの右に、次節の解法プログラムで求めた最短手数を表示しています。 解法プログラムを見る前に挑戦してみてください。

プログラム

use strict;
my $n = 5;
my (%head, %tail, @result);

foreach (0 .. ($n % 2 ? int($n / 2) : $n / 2 - 1)) {
  my $start = "B" x $n;
  substr($start, $_, 1, "_");
  (my $goal = $start) =~ s/B/W/g;
  my @board = ([$start, $n]);
  %head = ($start => 0);
  %tail = ($goal => 0);

OUT: while (@board) {
    my ($board, $p_pos) = @{shift(@board)};
    (my $r_board = $board) =~ tr/BW/WB/;
    my $c_pos = index($board, '_');
    my @n_pos = grep { $_ < ($c_pos - 1) or $_ > ($c_pos + 1) } 0 .. ($n - 1);
    foreach my $n_pos (@n_pos) {
      next if $p_pos == $n_pos;
      my $temp = $board;
      my $i = $c_pos < $n_pos ? $c_pos + 1 : $n_pos + 1;
      my $len = abs($c_pos - $n_pos) - 1;
      my $rev = substr($temp, $i, $len);
      $rev =~ y/BW/WB/;
      substr($temp, $i, $len, $rev);
      substr($temp, $c_pos, 1) = substr($temp, $n_pos, 1, "_");
      if (grep /$temp/, keys %tail) {
        push @result, [gather($board), gather($temp)];
        last OUT;
      } else {
        next if grep /$temp/, keys %head;
        $head{$temp} = $board;
        push @board, [$temp, $c_pos];
        $temp =~ tr/BW/WB/;
        if (grep /$temp/, keys %head) {
          push @result, [gather($temp), gather($r_board)];
          last OUT;
        } else {
          $tail{$temp} = $r_board;
        }
      }
    }
  }
}

sub gather {
  my $board = shift; my @work;
  if (grep /$board/, keys %head) {
    while ($board) {
      unshift @work, $board;
      $board = $head{$board};
    }
  } else {
    while ($board) {
      push @work, $board;
      $board = $tail{$board};
    }
  }
  return @work;
}

my $max = 0;
foreach (@result) {
  $max = @$_ if $max < @$_;
}

foreach my $i (0 .. ($max - 1)) {
  my $line = "";
  foreach my $j (0 .. $#result) {
    if ($i < @{$result[$j]}) {
      $line .= "    ${$result[$j]}[$i]";
      $line =~ s/    // if $j == 0;
    } else {
      $line = $line . "    " . " " x $n;
      $line =~ s/    // if $j == 0;
    }
  }
  print "$line\n";
}

プログラムの解説

今回のパズルでは、盤面を単純な文字列で扱います。黒石を 'B' で、白石を 'W' で、空き升目を '_' で表します。盤面は、'_BBBB' や 'BW_BB' のような文字列となります。 プログラムの最初で指定している $n は、盤面の大きさです。石の数は、$n よりも1つ少なくなります。 正解があるのは、盤面の大きさが5、石の数が4以上です。

今回のパズルは、ゴールの盤面が決まっています。通常はスタートの盤面からパズルを解きますが、 ゴールの盤面が決まっている場合は、ゴールの盤面からスタートの盤面を目指しても同様に解くこともできます。 しかし、単にゴールから探索して解を得たとしても、スタートから探索するのと同じで意味がありません。 双方向から探索して、途中で繋ぎ合わせることができたらどうでしょうか。 まずは、スタートとゴールの盤面から2手目までを進めた盤面を見てみましょう。

スタート図 スタートから1手  スタートから2手
  
  ---> 
  
  ---> 
  
     
   ---> 
  
  ---> 
  
     
   ---> 
  
  ---> 
  
     
     ---> 
  



ゴール図 ゴールから1手  ゴールから2手
  
  ---> 
  
  ---> 
  
     
   ---> 
  
  ---> 
  
     
   ---> 
  
  ---> 
  
     
     ---> 
  

スタート図から1手進めると、3つの盤面を生成することができます。 1手目の盤面からは、それぞれ1つから2つの2手目の盤面を生成することができます。 ゴールからの盤面の生成も同様です。ゴールから生成した盤面をよく見てみると、 スタート図から生成した盤面の ● と ○ を入れ替えたものと同じであることがわかります。 すなわち、スタート図から得られた盤面の ● と ○ を入れ替えれば、 ゴール図からの盤面を生成したことになります。

今回のパズルでは、盤面データの管理は単純なハッシュを利用しています。 スター図からの盤面は %head に、ゴール図からの盤面は %tail に格納します。 ハッシュのキーには生成した盤面を、値には生成元の盤面を格納しておきます。%tail への登録は、単に %head で生成したものを B と W を入れ替えるだけで済みます。 2手進めたときのハッシュは、次のようになります。

%head = ( '_BBBB' => undef,         %tail = ( '_WWWW' => undef,
          'BW_BB' => '_BBBB',                 'WB_WW' => '_WWWW',
          'BWW_B' => '_BBBB',                 'WBB_W' => '_WWWW',
          'BWWW_' => '_BBBB',                 'WBBB_' => '_WWWW',
          'BWBW_' => 'BW_BB',                 'WBWB_' => 'WB_WW',
          'B_BWB' => 'BWW_B',                 'W_WBW' => 'WBB_W',
          'BW_BW' => 'BWWW_',                 'WB_WB' => 'WBBB_',
          'B_BBW' => 'BWWW_',                 'W_WWB' => 'WBBB_',
      );                                  );

2つのハッシュでは、生成した盤面から最初の盤面までたどるのほ容易です。 %head ハッシュから新しい盤面を生成したら、まず %tail ハッシュのキーを調べます。生成した盤面が %tail ハッシュのキーにある場合は、2つのハッシュの経路を繋ぎ合わせれば、正解を得ることができます。%tail キーに生成した盤面がない場合は、%head ハッシュのキーを調べます。 生成した盤面がすでにハッシュのキーに登録してあるときには、 短い手数かまたは同じ手数で生成済みなので登録をスキップします。%tail への登録は、単に B と W を入れ替えるだけです。

ここで、現在の盤面から次の盤面を生成する仕組みを説明しましょう。 以下は、その部分のコードです。

my ($board, $p_pos) = @{shift(@board)};    # $p_pos -- 前の空き升目の位置
(my $r_board = $board) =~ tr/BW/WB/;
my $c_pos = index($board, '_');            # $c_pos -- 現在の空き升目の位置
my @n_pos = grep { $_ < ($c_pos - 1) or $_ > ($c_pos + 1) } 0 .. ($n - 1);
foreach my $n_pos (@n_pos) {               # $n_pos -- 次の空き升目の位置
  next if $p_pos == $n_pos;
  my $temp = $board;
  my $i = $c_pos < $n_pos ? $c_pos + 1 : $n_pos + 1;   # 反転する石の開始位置
  my $len = abs($c_pos - $n_pos) - 1;      # 反転する石の数
  my $rev = substr($temp, $i, $len);       # 反転部を取り出す
  $rev =~ y/BW/WB/;                        # 反転
  substr($temp, $i, $len, $rev);           # 反転したものを元の文字列に組み込む
  substr($temp, $c_pos, 1) = substr($temp, $n_pos, 1, "_");   # 空き升目と移動先の石を入れ替える

次の盤面を生成するには、2つの作業が必要です。1つ目は、中間の石を反転することです。 盤面文字列全体の石を反転するのは簡単ですが、一部だけ反転するのには少し工夫が必要です。 ここでは盤面文字列から反転する部分文字列を取り出して、部分文字列を反転して、元の盤面文字列を置き換える、 という方法で行っています。2つ目は、現在の空き升目と次の移動先の石とを入れ替えることです。 引数4つの substr と左辺値としての substr を使用すると、一度に入れ替えることができます。

今回の探索では、while ループを使用しています。条件部は、探索途中のボードを格納した配列 @board です。探索の開始時には、スタートの盤面だけが入っています。探索を開始すると、スタートの盤面を @board から取り出して、1手目の盤面をすべて生成して @board に格納します。2手目以降も同じような方法で生成して、 最短手順の解が見つかったら、そこで while ループを終了させます。

以上が今回のパズルの探索の仕組みです。 今回のパズルを実行すると、以下のような結果になります。

$n: 5                           $n: 6
  _BBBB    B_BBB    BB_BB         _BBBBB    B_BBBB    BB_BBB
  BWW_B    BBWW_    BBBW_         BW_BBB    BBW_BB    _WBBBB
  B_BWB    BB_BW    _WWBB         BWBW_B    BBWBW_    BBW_BB
  BBWB_    _WBBW    WB_BB         _BWBBB    _WBWBB    BBWBW_
  BB_WW    BBW_W    WBBW_         WW_BBB    WBW_BB    _WBWBB
  _WBWW    B_BBW    W_WBB         WWBW_B    WBWBW_    WBW_BB
  WBWB_    BWWW_    WBB_B         W_WBWB    W_BWBB    WBWBW_
  WB_WW    BW_BW    _WWWB         WBBWB_    WWW_BB    W_BWBB
  _WWWW    _BBBW    WB_WB         WBB_WW    WWWBW_    WWW_BB
           BWW_W    WBBB_         _WWWWW    WW_WBW    WWWBW_
           B_BWW    W_WWB                   WWBB_W    WW_WBW
           BWWB_    WWB_B                   W_WWWW    WWBB_W
           BW_WW    _BWWB                             _BWWWW
           _BBWW    WW_WB                             WW_WWW
           WWWB_    WWBB_
           WW_WW    _BWWW
           _BWWW    WW_WW
           WWB_W
           W_WWW

(2007/07/01, 2008/07/01更新)

TopPage