今回は、連なった円に数字を入れるパズルです。円の重なった部分の数字には、 両隣の数字の和が入ります。円が3つの場合は、1から 5 の数字が入ります。解の1つは、 下図のようになります。
円を1つ増やすと、入れる数は2つ増えます。円が4つの場合は、1から 7 の数字が入ります。今回のプログラムでは、円が 9 くらい (1 から 17) まではストレスなく解が得られます。 それ以上の数でも、解は得られますが少し時間が掛かるようになります。
use strict; my $max = 9; my ($count, @peices); for (my $i = 1; $i < $max; $i++) { for (my $j = 1; $j < $max; $j++) { next if $i == $j; push @peices, $i . ':' . ($i + $j) . ':' . $j if $i + $j <= $max; last if $i + $j > $max; } } my @maxs = grep { /:$max:/ and (split /:/)[0] < (split /:/)[2] } @peices; @peices = grep { ! /:$max:/ } @peices; foreach (@maxs) { /^(\d+):\d+:(\d+)$/; my @work = grep { ! /:$1:/ and ! /^$1:/ and ! /:$2:/ and ! /:$2$/ } @pieces; tail($_, @work); } sub tail { my ($chain, @list) = @_; head($chain, @list); my ($matubi) = $chain =~ /:(\d+)$/; foreach my $item (grep /^$matubi:/, @list) { my $tmp = $chain; $tmp =~ s/\d+$//; $tmp .= $item; if (($tmp =~ tr/://) == $max - 1) { $tmp =~ s/:/_/g; print "$tmp\n"; $count++; } else { $item =~ /^(\d+):(\d+):(\d+)$/; my @work = grep { ! /\b$1\b/ and ! /\b$2\b/ and ! /:$3:/ and ! /:$3$/} @list; $tmp =~ /^(\d+):.*:(\d+)$/; @work = grep { ! /^$2:\d+:$1$/ } @work; tail($tmp, @work); } } } sub head { my ($chain, @list) = @_; my ($atama) = $chain =~ /^(\d+):/; foreach my $item (grep /:$atama$/, @list) { my $tmp = $chain; $tmp =~ s/^\d+//; $tmp = $item . $tmp; if (($tmp =~ tr/://) == $max - 1) { $tmp =~ s/:/_/g; print "$tmp\n"; $count++; } else { $item =~ /^(\d+):(\d+):(\d+)$/; my @work = grep { ! /\b$2\b/ and ! /\b$3\b/ and ! /:$1:/ and ! /^$1:/ } @list; $tmp =~ /^(\d+):.*:(\d+)$/; @work = grep { ! /^$2:\d+:$1$/ } @work; head($tmp, @work); } } } print "解の数: $count\n";
今回のパズルを解くために、少し作戦を立ててみました。 正解を組み立てるために、まず最初に小さな部品を作ります。部品は、3つの数字を繋いだ 1:3:2 のような形になります。中央の値は、両隣の数字の和になっています。 この部品は2つの円に置くことができ、円の重なったところに中央の値が入ります。 このような部品を、2つの数字の順列を生成しながら、最大値を超えないものすべてを作ります。 そして、中央が最大値なっている部品 (最大値は必ず中央になる) を最初に配置して、その両端に繋げることのできる部品を繋ぎ、最終的に解を得ます。 部品は、配列 @pieces に格納しておきます。円が5つ (1 から 9) の場合の @pieces の内容は、次のようになります。
1:3:2, 1:4:3, 1:5:4, 1:6:5, 1:7:6, 1:8:7, 1:9:8, 2:3:1, 3:4:1, 4:5:1, 5:6:1, 6:7:1, 7:8:1, 8:9:1, 2:5:3, 2:6:4, 2:7:5, 2:8:6, 2:9:7, 3:5:2, 4:6:2, 5:7:2, 6:8:2, 7:9:2, 3:7:4, 3:8:5, 3:9:6, 4:7:3, 5:8:3, 6:9:3, 4:9:5, 5:9:4,
最初に配置できるのは、中央が最大値 9 になっている部品 1:9:8, 2:9:7, 3:9:6, 4:9:5 の4つです (逆順値の部品 8:9:1, 7:9:2, 6:9:3, 5:9:4 は、重複解となるので最初から排除しておきます)。この4つの部品を foreach ループを使って、1つずつ再帰型サブルーチンを使って探索していくことになります。 呼び出しおよびサブルーチンのループでは、現時点で使用不可能な部品を除いた部品 (繋げることができる部品ではない) を引数として渡します。例えば 3:9:6 で探索を始めた場合には、部品の分類は以下のようになります。
使用不可の部品 (中央の数字 (9) を含む部品と両端の数字 (3, 6) が中央値になる部品)
1:9:8, 8:9:1, 2:9:7, 7:9:2, 6:9:3, 4:9:5, 5:9:4
1:3:2, 1:6:5, 2:3:1, 5:6:1, 2:6:4, 4:6:2
繋ぐことができない部品 (3:9:6 と 3:4:1 のようなケース)
3:4:1, 3:5:2, 3:7:2, 3:8:2,
1:7:6, 2:8:6
繋ぐことができる部品
1:4:3, 2:5:3, 4:7:3, 5:7:3 (頭に)
6:7:1, 6:8:2 (末尾に)
未使用の数字のみで構成される部品
1:5:4, 1:8:7, 4:5:1, 7:8:1, 2:7:5, 5:7:2
tail の呼び出しおよび tail と head のループでは、1の "使用不可の部品" と2の "繋ぐことができない部品" を除外して、3の "繋ぐことのできる部品" と4の "未使用の数字のみで構成される部品" を引数として渡します。 ただこれのみで繋いでいったのでは、充分ではありません。最初の呼び出しでは現れないのですが、 ループの途中で出現してしまいます。例えば、3:9:6 に 6:8:2 を繋いで 3:9:6:8:2 を作った場合に、"繋ぐことができる部品" として 2:5:3 が残ってしまいます (実際には、前後どちらに繋いでも数字が重複してしまうため駄目なのですが)。 これをチェックするために、次のコードを入れています。
$tmp =~ /^(\d+):.*:(\d+)$/; @work = grep { ! /^$2:\d+:$1$/ } @work;
解を生成する探索は、tail と head の2つの再帰型サブルーチンを使います。tail は末尾に部品を繋ぎ、head は頭に部品を繋ぐ役割をします。tail で末尾に繋ぐことのできるパターンをすべて生成して、tail だけで完成した解を除いて残りのすべてを head に渡します。こうすることで、すべての解を得ることができます。tail と head の2つのサブルーチンは、正規表現さえ理解できれば易しいと思います。
今回のプログラムでは、部品の接続文字として : (コロン) を使って、表示の際に _ (アンダースコア) に変換しています。それでは、最初から _ にした方が良いと思うかも知れません。しかし _ は単語構成文字に含まれるために、 変数名や正規表現により神経を使わなければならなくなります。例えば、変数名の $matubi: は、${matubi}_ のように書かなければなりません。 また次の正規表現は、下の行のように書く必要があります。
my @work = grep { ! /\b$2\b/ and ! /\b$3\b/ and ! /:$1:/ and ! /^$1:/ } @list; my @work = grep { ! /(\b|_)$2(\b|_)/ and ! /(\b|_)$3(\b|_)/ and ! /_$1_/ and ! /^$1_/ } @list;
部品の各々の数字を捕捉したい場合に、\b だけを使ってマッチさせると 3_9_6 では単語境界が先頭と末尾だけになるので失敗します。: を使った 3:9:6 では、先頭と末尾と共に数字と : の接した位置も単語境界になります。 また、正規表現で選択を使うと、処理時間がより掛かるようになります。下の実行結果の $max = 19 の実行時間は 72 秒ですが、選択を使ったプログラムでは : から _ に変換する処理がないにもかかわらず 10 秒くらい多く掛かります。
プログラムを実行した結果は、以下のようになります。 実行時間は、例によって私の速くないパソコンでの結果です。
$max = 7 2_5_3_4_1_7_6 2_7_5_6_1_4_3 解の数: 2 $max = 9 1_5_4_7_3_9_6_8_2 1_4_3_9_6_8_2_7_5 7_8_1_3_2_6_4_9_5 解の数: 3 $max = 11 5_7_2_8_6_9_3_4_1_11_10 2_8_6_9_3_7_4_5_1_11_10 3_10_7_8_1_5_4_6_2_11_9 ・・・ 5_11_6_8_2_9_7_10_3_4_1 8_10_2_7_5_11_6_9_3_4_1 8_9_1_3_2_7_5_11_6_10_4 解の数: 21 $max = 13 8_10_2_7_5_11_6_9_3_4_1_13_12 5_11_6_8_2_9_7_10_3_4_1_13_12 8_11_3_10_7_9_2_6_4_5_1_13_12 ・・・ 3_4_1_10_9_11_2_8_6_13_7_12_5 6_13_7_12_5_8_3_4_1_10_9_11_2 10_11_1_3_2_8_6_13_7_12_5_9_4 解の数: 63 $max = 15 7_13_6_11_5_9_4_12_8_10_2_3_1_15_14 10_12_2_9_7_13_6_11_5_8_3_4_1_15_14 4_13_9_12_3_11_8_10_2_7_5_6_1_15_14 ・・・ 1_5_4_11_7_15_8_14_6_9_3_13_10_12_2 5_12_7_15_8_14_6_10_4_13_9_11_2_3_1 1_4_3_13_10_12_2_9_7_15_8_14_6_11_5 解の数: 198 2 seconds $max = 17 11_15_4_10_6_14_8_13_5_12_7_9_2_3_1_17_16 8_12_4_14_10_15_5_11_6_13_7_9_2_3_1_17_16 5_14_9_15_6_13_7_11_4_12_8_10_2_3_1_17_16 ・・・ 3_4_1_14_13_15_2_10_8_17_9_16_7_12_5_11_6 2_14_12_15_3_11_8_17_9_16_7_13_6_10_4_5_1 1_4_3_15_12_14_2_10_8_17_9_16_7_13_6_11_5 解の数: 893 11 seconds $max = 19 11_16_5_12_7_17_10_14_4_13_9_15_6_8_2_3_1_19_18 11_15_4_13_9_14_5_12_7_17_10_16_6_8_2_3_1_19_18 8_12_4_14_10_15_5_16_11_17_6_13_7_9_2_3_1_19_18 ・・・ 16_17_1_5_4_6_2_13_11_14_3_12_9_19_10_18_8_15_7 16_17_1_12_11_14_3_5_2_6_4_13_9_19_10_18_8_15_7 13_17_4_6_2_3_1_12_11_16_5_14_9_19_10_18_8_15_7 解の数: 3874 72 seconds $max = 21 10_19_9_14_5_17_12_16_4_15_11_18_7_13_6_8_2_3_1_21_20 13_17_4_15_11_18_7_12_5_14_9_19_10_16_6_8_2_3_1_21_20 13_18_5_12_7_16_9_19_10_14_4_15_11_17_6_8_2_3_1_21_20 ・・・ 10_21_11_20_9_17_8_14_6_18_12_13_1_16_15_19_4_7_3_5_2 5_6_1_4_3_13_10_21_11_20_9_17_8_15_7_19_12_14_2_18_16 10_21_11_20_9_17_8_15_7_19_12_16_4_6_2_3_1_14_13_18_5 解の数: 18981 505 seconds $max = 23 14_20_6_18_12_16_4_15_11_21_10_19_9_17_8_13_5_7_2_3_1_23_22 14_20_6_17_11_15_4_16_12_21_9_19_10_18_8_13_5_7_2_3_1_23_22 14_18_4_15_11_17_6_16_10_19_9_21_12_20_8_13_5_7_2_3_1_23_22 ・・・ 2_6_4_7_3_14_11_23_12_22_10_19_9_17_8_21_13_18_5_20_15_16_1 2_6_4_5_1_16_15_18_3_14_11_23_12_22_10_19_9_17_8_21_13_20_7 5_6_1_3_2_16_14_18_4_15_11_23_12_22_10_19_9_17_8_21_13_20_7 解の数: 106955 3687 seconds $max = 25 : 以下は解が1つ 11_18_7_20_13_21_8_22_14_23_9_19_10_15_5_17_12_16_4_6_2_3_1_25_24 2 seconds $max = 27 16_21_5_20_15_22_7_19_12_23_11_25_14_24_10_18_8_17_9_13_4_6_2_3_1_27_26 4 seconds $max = 29 17_24_7_23_16_21_5_20_15_26_11_25_14_27_13_22_9_19_10_18_8_12_4_6_2_3_1_29_28 15 seconds $max = 31 17_22_5_23_18_28_10_26_16_25_9_24_15_29_14_27_13_21_8_20_12_19_7_11_4_6_2_3_1_31_30 54 seconds $max = 33 19_31_12_30_18_26_8_25_17_28_11_27_16_29_13_23_10_24_14_21_7_22_15_20_5_9_4_6_2_3_1_33_32 141 seconds $max = 35 20_32_12_33_21_28_7_24_17_30_13_31_18_29_11_27_16_26_10_25_15_23_8_22_14_19_5_9_4_6_2_3_1_35_34 788 seconds
今回のプログラムは、内容がほぼ同じのサブルーチンが並んでいて、いかにも素人っぽく (まあ紛れもない素人だが) なってしまいました。最初に作ったプログラムの骨組みは、次のようになっていました。
sub search { ... foreach my item (grep { /^$matubi:/ or /:$atama$/ } @list) { ... if (($tmp =~ tr/://) == $max - 1) { ... } else { ... search($tmp, @work); } } }
この最初のプログラムを実行すると、頭に繋げる部品と末尾に繋げる部品を foreach の平坦なリストで扱っているために、同一解が発生してしまいます。例えば 3:9:6 の頭に 1:4:3 を繋ぎ、末尾に 6:8:2 を繋いで 1:4:3:9:6:8:2 を生成する順序は、どちらを先にするかによって2つあります。 1つの再帰型サブルーチンの中では、この繋ぐ順序の制御が非常に難しく今回は諦めました。 そのうちいいアイデアが浮かんだら、直すことにしましょう。
(2005/09/15)