今回は、"ナイトの交換" を取り上げます。以前に取り上げた "ナイトの巡歴" では1つのナイトを使いましたが、今回のパズルでは 3x4 の盤面に6つのナイト (黒3つ、白3つ) を使います。下図の初期盤面 (Start) に配置された黒のナイトと白のナイトを1つずつ動かしながら、その位置を入れ替えて Goal に至る経路を探索するのが今回の問題です。
|
--> |
| ||||||||||||||||||||||||
Start | Goal |
ナイトの巡歴では、3x4盤面は短時間で探索することができました。 今回のナイトの交換は盤面こそ同じで小さいですが、ナイトの数が6個と増えているために、 ナイトの巡歴のプログラムを拡張しただけではデータ量が膨大になり行き詰まってしまいます。 そこで今回は、先にナイト毎の個別の経路のリストを作成しておき、その組合せによるプログラムとしました。
use strict; my (%route, @mirror); { my %move = ('0' => [5, 7], '1' => [6, 8], '2' => [3, 7], '3' => [2, 8, 10], '4' => [9, 11], '5' => [0, 6, 10], '6' => [1, 5, 11], '7' => [0, 2], '8' => [1, 3, 9], '9' => [4, 8], '10' => [3, 5], '11' => [4, 6]); my @work = (0, 1, 2); my $limit = 4; while (@work) { my $work = shift(@work); my ($cp) = $work =~ /\b(\d+)$/; foreach my $i (@{$move{$cp}}) { next if $work =~ /\b$i\b/; my $tmp = $work . ':' . $i; if ($i == 9 or $i == 10 or $i == 11) { my ($key) = $tmp =~ /^(\d+)/; push @{$route{$key}}, $tmp; $key = 11 - $1; push @{$route{$key}}, join(':', map { 11 - $_ } split(/:/, $tmp)); } push @work, $tmp if $tmp =~ tr/:// < $limit; } } } my @order = (0, 1, 2, 9, 10, 11); my @alph = qw(A B C X Y Z); my @before; my $start = "ABC------XYZ"; my $goal; comb(0); sub comb { my $no = shift; my @list = @_; foreach my $item (@{$route{$order[$no]}}) { next if join('_', @list, $item) =~ /:(\d+)_.*:\1$/; if (@list) { my ($flag, $chk); foreach my $tmp (@list) { if ($tmp =~ tr/:// >= $item =~ tr/://) { $chk = join '', map { '(?=.*\b' . $_ . '\b)' } split /:/, $item; $flag = $tmp =~ /^$chk/; last if $flag; } else { $chk = join '', map { '(?=.*\b' . $_ . '\b)' } split /:/, $tmp; $flag = $item =~ /^$chk/; last if $flag; } } next if $flag; } my @work = @list; push @work, $item; if ($no == 5) { my $chk = join ' ', sort { $a <=> $b } map { join ':', map { $_ % 3 == 0 ? $_ + 2 : $_ % 3 == 2 ? $_ -2 : $_ } split /:/ } @work; next if grep /$chk/, @mirror; push @mirror, join(' ', @work); my $n = 0; my @temp; foreach (@work) { /(\d+)$/; push @temp, $1 . $alph[$n]; $n++; } @temp = sort { $a <=> $b } @temp; s/\d//g foreach @temp; $goal = join '', @temp[0, 1, 2], '------', @temp[3, 4, 5]; s/^(\d+):/!$1!/ foreach (@work); @before = ($start); search([$start], @work); } else { comb($no + 1, @work); } } } sub search { my $ref = shift; my $board = $ref->[0]; my @routes = @_; foreach (@routes) { my ($i, $j) = /!(\d+)!(\d+)\b/; if (substr($board, $j, 1) eq '-') { my @tmp = split //, $board; @tmp[$i, $j] = @tmp[$j, $i]; my $temp = join '', @tmp; next if grep /^$temp$/, @before; push @before, $temp; if ($temp =~ /$goal/) { disp([$temp, $ref]); return; } else { my $route = $_; my @work = grep { $_ !~ /^$route$/ } @routes; if ($route =~ /^!/) { $route =~ s/!(\d+)!(\d+):?/$1!$2!/; } else { $route =~ s/!(\d+)!(\d+):?/:$1!$2!/; } push @work, $route unless $route =~ /!$/; search([$temp, $ref], @work); } } elsif (/:\d+!\d+!/ and check($board, @routes)) { ($j, $i) = /:(\d+)!(\d+)!/; next unless substr($board, $j, 1) eq '-'; my @tmp = split //, $board; @tmp[$i, $j] = @tmp[$j, $i]; my $temp = join '', @tmp; next if grep /^$temp$/, @before; push @before, $temp; my $route = $_; my @work = grep { $_ !~ /^$route$/ } @routes; $route =~ s/:(\d+)!(\d+)!/!$1!$2:/; push @work, $route; search([$temp, $ref], @work); } } } sub check { my $board = shift; foreach (@_) { /!\d+!(\d+)\b/; if (substr($board, $1, 1) eq '-') { return 0; } } return 1; } sub disp { print "$mirror[-1]\n\n"; my $tmp_ref = shift; my @work; while ($tmp_ref) { my $board; ($board, $tmp_ref) = @$tmp_ref; unshift @work, $board; } my ($count, $line1, $line2, $line3, $line4); foreach (@work) { /(...)(...)(...)(...)/; $count++; if ($count % 7 != 0 and $count < @work) { $line1 .= $1; $line1 .= ' '; $line2 .= $2; $line2 .= ' => '; $line3 .= $3; $line3 .= ' '; $line4 .= $4; $line4 .= ' '; } elsif ($count % 7 == 0 and $count < @work) { $line1 .= $1; $line2 .= $2; $line2 .= ' => '; $line3 .= $3; $line4 .= $4; print "$line1\n$line2\n$line3\n$line4\n\n"; $line1 = $line2 = $line3 = $line4 = ''; } else { $line1 .= $1; $line2 .= $2; $line3 .= $3; $line4 .= $4; print "$line1\n$line2\n$line3\n$line4\n\n"; } } print "\n"; }
探索に成功した場合を考えると、6つのナイトはそれぞれに個別のルートを持っています。 その個別のルートは、他のナイトがない状態で探索したものの中に含まれているはずです。 先にすべてのナイトについて個別のルートを求めておき、 それを組み合わせて探索すれば正解を得られることになります。 この考え方で作成したのが、今回のプログラムです。
今回のプログラムでは、盤面のマス目には 0 から 12 の番号を付けます。また、冒頭の図では、黒のナイトの名前を B、白のナイトの名前を W と同じ名前で扱っていますが、これからは別々の名前で扱うことにします。黒のナイトの名前を A, B, C、白のナイトの名前を X, Y, Z とします。下の図は、盤面の番号及び初期盤面です。
|
|
個別のルートは、簡単に求めることができます。 始点は1つで、終点は3つの内のいずれかになります。 ルートの途中で、同じマス目を2度通ることはできません。 次のコードが、個別ルートを生成するコードです。
my %route;
{ my %move = ('0' => [5, 7], '1' => [6, 8], '2' => [3, 7],
'3' => [2, 8, 10], '4' => [9, 11], '5' => [0, 6, 10],
'6' => [1, 5, 11], '7' => [0, 2], '8' => [1, 3, 9],
'9' => [4, 8], '10' => [3, 5], '11' => [4, 6]);
my @work = (0, 1, 2); my $limit = 4; # ルートの長さを指定
while (@work) {
my $work = shift(@work);
my ($cp) = $work =~ /\b(\d+)$/;
foreach my $i (@{$move{$cp}}) {
next if $work =~ /\b$i\b/;
my $tmp = $work . ':' . $i;
if ($i == 9 or $i == 10 or $i == 11) {
my ($key) = $tmp =~ /^(\d+)/;
push @{$route{$key}}, $tmp;
$key = 11 - $1;
push @{$route{$key}}, join(':', map { 11 - $_ } split(/:/, $tmp));
}
push @work, $tmp if $tmp =~ tr/:// < $limit;
}
}
}
個別のルートは、ハッシュ %route に格納しておきます。 キーは、ルートの始点となる盤面の番号になります。値は、ルートを格納した配列のリファレンスです。 生成するルートの長さ (手数) は、$limit で指定します。$limit に 4 を指定すると、4 手以下で終点に至るすべてのルートを生成します。 今回のコードでは、A, B, C のルートのみを生成して、そのルートを変換して X, Y, Z のルートを生成しています (なお、A のみのルートを生成して変換すれば、C, X, Z のルートを生成することもできます)。以下は、A と B のすべてのルートです。
A B 0:5:10 1:6:11 0:5:6:11 1:8:9 0:7:2:3:10 1:6:5:10 -------------- 1:8:3:10 0:5:6:1:8:9 1:6:11:4:9 0:5:6:11:4:9 1:8:9:4:11 0:5:10:3:8:9 ------------------- 上: $limit = 4 で生成されるルート 0:7:2:3:8:9 1:6:5:10:3:8:9 0:5:6:1:8:3:10 1:8:3:10:5:6:11 0:5:6:1:8:9:4:11 1:6:5:0:7:2:3:10 0:5:10:3:8:1:6:11 1:6:11:4:9:8:3:10 0:5:10:3:8:9:4:11 1:8:3:2:7:0:5:10 0:7:2:3:8:1:6:11 1:8:9:4:11:6:5:10 0:7:2:3:8:9:4:11 1:6:5:0:7:2:3:8:9 0:7:2:3:10:5:6:11 1:6:5:10:3:8:9:4:11 0:5:6:11:4:9:8:3:10 1:8:3:2:7:0:5:6:11 0:7:2:3:8:1:6:5:10 1:8:3:10:5:6:11:4:9 0:5:10:3:8:1:6:11:4:9 1:6:5:0:7:2:3:8:9:4:11 0:7:2:3:8:1:6:11:4:9 1:8:3:2:7:0:5:6:11:4:9 0:7:2:3:10:5:6:1:8:9 1:6:11:4:9:8:3:2:7:0:5:10 0:7:2:3:10:5:6:11:4:9 1:8:9:4:11:6:5:0:7:2:3:10 0:7:2:3:8:9:4:11:6:5:10 0:7:2:3:10:5:6:1:8:9:4:11
個別ルートを生成したら、次には6つのルートの組合せを comb サブルーチン生成でします。comb サブルーチンの構造は、枝刈り等のコードがありますので複雑に見えますが、実際は至ってシンプルです。
my @order = (0, 1, 2, 9, 10, 11); comb(0); sub comb { my $no = shift; my @list = @_; foreach my $item (@{$route{$order[$no]}}) { my @work = @list; push @work, $item; if ($no == 5) { search([$start], @work); } else { comb($no + 1, @work); } } }
上のコードを実行すると、$limit が 4 の場合には、2916 (3 x 6 x 3 x 3 x 6 x3) の組合せが生成されます。組合せの過程では、次の枝刈りを使って、ダメなものを排除します。
終点が重複する。(例: 0:5:10 と 1:6:5:10)
next if join('_', @list, $item) =~ /:(\d+)_.*:\1$/;
ルートが重複する。(例: 0:5:10 と 10:5:0、 0:5:10 と 10:5:0:7:2)
if (@list) { my ($flag, $chk); foreach my $tmp (@list) { if ($tmp =~ tr/:// >= $item =~ tr/://) { $chk = join '', map { '(?=.*\b' . $_ . '\b)' } split /:/, $item; $flag = $tmp =~ /^$chk/; last if $flag; } else { $chk = join '', map { '(?=.*\b' . $_ . '\b)' } split /:/, $tmp; $flag = $item =~ /^$chk/; last if $flag; } } next if $flag; }
短い (あるいは等しい) ルートの通過地点が長い (あるいは等しい) ルートにすべて含まれいる場合には、お互いに終点に至ることはできません。$chk には、肯定先読みの正規表現が格納されます。0:5:10 のときの $chk は、(?=.*\b0\b)(?=.*\b5\b)(?=.*\b10\b) になります。
重複解には鏡像解というものがある場合もありますが、 今回のパズルでは鏡像ルートというべきものがあります。次の例を、見て下さい。
0:5:10 1:6:11 2:3:8:9 9:8:1 10:3:2 11:6:5:0 2:3:10 1:8:9 0:5:6:11 11:6:1 10:5:0 9:8:3:2 (0:5:6:11, 1:8:9, 2:3:10, 9:8:3:2, 10:5:0, 11:6:1)
1行目のルートを鏡に写すと、その下の行のように見えるはずです。 厳密には重複解と言えるかどうかは分かりませんが、一応除外することにしました。 鏡像ルートを生成するコードは、次の通りです。
my $chk = join ' ', sort { $a <=> $b } map { join ':', map { $_ % 3 == 0 ? $_ + 2 : $_ % 3 == 2 ? $_ -2 : $_ } split /:/ } @work; next if grep /$chk/, @mirror; push @mirror, join(' ', @work);
鏡像ルートは、左の列 (0, 3, 6, 9) に 2 をプラスし、中央の列 (1, 4, 7, 10) はそのままにし、右の列 (2, 5, 8, 11) に 2 をマイナスすると得られます。$chk を生成するコードは、続けて書いてしまうと分かりづらいため、シュウォーツ変換風に書いてみました。@work の内容が上の3行目のときには、$chk は1行目と同じになります。1行目はすでに @mirror に格納されているので、スキップされます。
以上のチェックを通過したら、search を呼び出すための準備をします。 準備としては、ゴール配置盤面の生成、現在地点の設定、配列 @before の初期化があります。 まずゴール配置盤面ですが、配列 @work から次のコードで生成します。
my $n = 0; my @temp;
foreach (@work) {
/(\d+)$/;
push @temp, $1 . $alph[$n]; # @alph = qw(A B C X Y Z)
$n++;
}
@temp = sort { $a <=> $b } @temp;
s/\d//g foreach @temp;
$goal = join '', @temp[0, 1, 2], '------', @temp[3, 4, 5];
配列 @work が (0:5:10, 1:6:11, 2:3:8:9, 9:8:1, 10:3:2, 11:6:5:0) のときに、先頭から A, B, C, X, Y, Z の経路になります。また、その末尾の数字は終点です。 末尾の数字と名前を繋げて数値でソートすると、(0Z, 1X, 2Y, 9C, 10A, 11B) となります。これで、数字を削除すればゴール配置盤面を作成することができます。
ここまでは各ナイトのルートは、0:5:10 のようにになっていますが、 ここで現在地点を設定します。現在地点は、最初は初期配置盤面ですので、始点になります。 現在地点は、! (感嘆符) で数字を囲みます。現在地点の設定をした後の @work は、(!0!5:10, !1!6:11, !2!3:8:9, !9!8:1, !10!:3:2, !11!6:5:0) のようになります。 配列 @before は、search サブルーチンを呼び出す毎に初期化します。
これでいよいよ search サブルーチンを呼び出して、探索することになります。search サブルーチンに渡す引数は、初期配置盤面と各ナイトのルートを格納した配列 @work になります。
search サブルーチンでは再帰呼び出しを使って、 各ナイトをルートの始点から終点に向けて1つずつ動かしていきます。 この探索では、盤面の配置と各ナイトの現在地点の2つのデータを管理します。 2つのデータの変化の様子は、次のようになります。
0) ABC------XYZ (!0!5:10, !1!6:11, !2!3:8:9, !9!8:1, !10!3:2, !11!6:5:0) 1) -BC--A---XYZ (!1!6:11, !2!3:8:9, !9!8:1, !10!3:2, !11!6:5:0, 0!5!10) 2) --C--AB--XYZ (!2!3:8:9, !9!8:1, !10!3:2, !11!6:5:0, 0!5!10, 1!6!11) ・・・・・ n) ZXY------CAB (0:5!10!, 1:6!11!, 2:3:8!9!, 9:8!1!, 10:3!2!, 11:6:5!0!)
上のように1つのナイトのルートを1つ進めたら、盤面の配置を更新するとともに、 ナイトの現在地点も更新します。このようにして、ナイトの動きのすべての組合せを調べてゴールを探索するのですが、 残念ながらそれだけでは正解を得られない場合があります (もちろん、正解を得られる場合もあります)。 ここで、最短手数の正解を示します。
0:5:10 1:6:11 2:3:8:9 9:8:1 10:3:2 11:6:5:0 ABC -BC -B- -B- -B- -B- -B- --- => --A => C-A => C-A => --A => Y-A => Y-- => --- --- --- Z-- Z-C Z-C Z-C XYZ XYZ XYZ XY- XY- X-- XA- -B- --- --Y Z-Y Z-Y Z-Y Z-Y Y-Z => Y-Z => --Z => --- => --- => C-- => C-- => --C B-C B-C B-C --C --- --X XA- XA- XA- XA- XAB XAB -AB ZXY ZXY ZXY C-- => --- => --- --- --C --- -AB -AB CAB または、 ABC ABC ABC AB- AB- A-- AX- --- => --- => --- => C-- => C-Z => C-Z => C-Z => --- Z-- Z-X Z-X --X B-X B-- XYZ XY- -Y- -Y- -Y- -Y- -Y- AX- AX- AX- AX- AXY AXY -XY --Z => Y-Z => Y-Z => Y-Z => --Z => --- => --A => B-C B-C --C --- --- Z-- Z-- -Y- --- --B C-B C-B C-B C-B -XY -XY ZXY --- => --Z => --- Z-- --- --- CAB CAB CAB
6つのナイトのルートの手数の合計は 14 ですが、正解の手数は 16 になっています。 このことは、各ナイトのルートを1手ずつ始点から終点まで進めても正解を得られない場合があることを示しています。 最初の正解例では C の動きが 2 -> 3 -> 8 -> 3 -> 8 -> 9 なっていて、次の正解例では Z の動きが 11 -> 6 -> 5 -> 6 -> 5 -> 0 になっています。この "一時退避" の現象は、パズルとしては面白い動きですが、プログラムを作る上では非常に厄介な問題です。 そこで、以下のルールでプログラムに組み込んでみました。
ルートの始点には戻らない。
始点から1つ進んだときのルートデータは 2!3!8:9
になり、2つ進んだときは 2:3!8!9 になります。2つのデータの違いは、現在地点の前の :
の有無です。現在地点の前に : がないときには、戻ることを禁止します。
elsif (/:\d+!\d+!/ and check($board, @routes))
ルートの終点からは戻らない。
終点に到達したルートデータは、2:3:8!9! のように末尾が
! になります。末尾が ! になったルートデータは、リストから出してしまいます。
push @work, $route unless $route =~ /!$/;
他のナイトが先に進めないことをチェックする。
他のナイトがまだ先に進める場合は、そちらを動かすようにします。
elsif (/:\d+!\d+!/ and check($board, @routes)) sub check { my $board = shift; foreach (@_) { /!\d+!(\d+)\b/; if (substr($board, $1, 1) eq '-') { return 0; } } return 1; }
次は、同一配置のチェックについて述べます。 このパズルでは、手順前後による同一配置の盤面がたくさん発生します。 この同一配置をチェックするために探索の過程で生じた配置は、すべて配列 @before に保存しておき、以前に探索済みのときはそこで打ち切るようにしています。
1) --C A, B --A B, A --B XYZ 2) --- A, B, C C-A C, B --B B, A, C XYZ C, A C, A, B B, A
上の2つの例は、ナイトを動かす順番が違うだけで同じ盤面になったものです。1) では2つ、2) では6つの手順前後による同一配置が発生します。同一配置は、最初に現れたときだけ探索して、 2番目以降はそこで探索を打ち切ります。
今回のパズルでは、スタートからゴールまでのすべての盤面配置を記憶しておく必要があります。 盤面配置データの管理方法は、以前取り上げた "ナイトの巡歴" と同じですので参照下さい。探索に成功して正解が得られたら、disp サブルーチンを呼び出して表示します。disp サブルーチンは、易しいので解説は省略します。以下は $limit を 4 にしたときの、プログラムの実行結果です。 なお、$limit を 3 にしたときには、最初の1つだけ表示されます。
0:5:10 1:6:11 2:3:8:9 9:8:1 10:3:2 11:6:5:0 ABC -BC -B- -B- -B- -B- -B- --- => --A => C-A => C-A => --A => Y-A => Y-- => --- --- --- Z-- Z-C Z-C Z-C XYZ XYZ XYZ XY- XY- X-- XA- -B- --- --Y Z-Y Z-Y Z-Y Z-Y Y-Z => Y-Z => --Z => --- => --- => C-- => C-- => --C B-C B-C B-C --C --- --X XA- XA- XA- XA- XAB XAB -AB ZXY ZXY ZXY C-- => --- => --- --- --C --- -AB -AB CAB 0:5:10 1:8:9:4:11 2:3:8:9 9:4:11:6:1 10:3:2 11:6:5:0 ABC -BC --C --- --- --- --- --- => --A => --A => C-A => CXA => CXA => CXA => --- --- --B --B --B Z-B Z-- XYZ XYZ XYZ XYZ -YZ -Y- BY- --- --- --- --- --- --- --- -XA => YXA => YX- => Y-- => Y-Z => YBZ => YBZ => Z-C Z-C Z-C Z-C --C --C --- BY- B-- BA- BAX BAX -AX CAX --Y --Y Z-Y Z-Y ZXY -BZ => -BZ => -B- => --- => --- --- X-- X-- X-- --- CAX CA- CA- CAB CAB 0:5:6:11 1:8:9 2:7:0:5:10 9:8:3:2 10:3:2:7:0 11:6:1 ABC -BC -B- -B- -B- -B- CB- --- => --A => --A => --A => Y-A => Y-A => Y-A => --- --- -C- -CX -CX ZCX Z-X XYZ XYZ XYZ -YZ --Z --- --- CBY CBY C-Y CZY CZY -ZY -Z- --A => X-A => X-A => X-A => X-- => X-C => X-C => Z-X Z-- Z-B --B A-B A-B AYB --- --- --- --- --- --- --- -ZX -ZX -ZX -ZX YZX --C => --C => --C => --- => --- AYB AY- -Y- -Y- --- --- B-- B-A BCA BCA 0:5:6:11 1:6:11:4:9 2:7:0:5:10 9:8:3:2 10:3:2:7:0 11:4:9:8:1 ABC -BC --C --- --- --- --- --- => --A => --A => --A => --A => Y-A => YZA => --- --- B-- BC- BCX BCX BCX XYZ XYZ XYZ XYZ -YZ --Z --- --- --- C-- C-Y C-Y C-Y C-Y YZA => YZ- => YZ- => -Z- => XZ- => X-- => XB- => -CX ACX A-X A-X A-- A-- A-- --B --B --B --B --B Z-B Z-- C-Y --Y --- --X --X --X --X XB- => XBC => XBC => -BC => -BC => --C => --- => --- --- -Y- -Y- -YZ -YZ -YZ Z-A Z-A Z-A Z-A --A B-A BCA Y-X YZX --- => --- --Z --- BCA BCA
最後に、長い手数 (最長?) の正解です。それぞれのナイトの手数が7手かかり、 一度一時退避がありますので 44 手かかっています。
0:5:10:3:8:9:4:11 1:8:9:4:11:6:5:10 2:7:0:5:10:3:8:9 9:4:11:6:1:8:3:2 10:3:2:7:0:5:6:1 11:6:1:8:3:10:5:0 ABC -BC --C --- --- --- --- --- => --A => --A => --A => -XA => YXA => YXA => --- --- --B -CB -CB -CB ZCB XYZ XYZ XYZ XYZ -YZ --Z --- --- --- C-- C-- C-Y CZY CZY YX- => YX- => YX- => Y-- => --- => --- => A-- => ZCB ZC- Z-- Z-- Z-- --- --- -A- BA- BA- BAX BAX BAX B-X CZY -ZY -ZY -Z- -Z- -Z- -Z- AB- => ABC => ABC => ABC => -BC => --C => --- => --- --- X-- XY- XYA XYA XYA --X --X --- --- --- --B -CB YZ- YZ- YZ- -Z- -Z- -Z- -Z- --- => --- => C-- => C-Y => CAY => -AY => -AY => X-A X-- X-- X-- X-- X-C X-- -CB ACB A-B A-B --B --B C-B --- -X- -X- -X- --- -Y- -Y- -AY => -AY => -A- => ZA- => ZA- => ZA- => ZA- => X-Z --Z Y-Z Y-- Y-X --X B-X C-B C-B C-B C-B C-B C-B C-- -Y- -Y- -Y- -Y- -YX -YX -YX Z-- => --- => X-- => X-B => --B => --- => --Z => B-X B-X B-- --- --- B-- B-- C-A CZA CZA CZA CZA CZA C-A ZYX ZYX ZYX --- => --B => --- B-- --- --- C-A C-A CBA
(2006/11/01)