さて、今回の問題は数字並べです。数字は、1から N までの2組使います。 言葉で説明するよりも、例を見てもらいましょう。下の例は、1から4まで並べたものです。
1と1の間には他の数字が1つ、2と2の間には他の数字が2つ、3と3の間には他の数が3つ、 4と4の間には他の数字が4つ、というように並んでいます。では1から7までの数字2組を使って、 同じルールで並べ下さいというのが、今回の問題です。
use strict; my ($count, @result); my $max = 7; my $lens = $max * 2; search($max, 0); sub search { my ($num, $added) = @_; my $work = $num . '0' x $num . $num; my $flag = $work; $flag =~ s/0/./g; foreach (0..($lens - length($work))) { my $tmp = $work . '0' x $_; my $add = $added + $tmp; if ($add =~ /$flag/) { if ($num == 1) { my $turn = reverse $add; last if grep /$turn/, @result; push @result, $add; $count++; last; } else { search($num - 1, $add); } } else { next; } } } if ($count) { print join("\n", @result), "\n"; print "解の数: $count\n"; } else { print "$max: 解なし\n"; }
さて今回の問題は、どのようにしたら解けるでしょうか? 順列を生成してしらみ潰しに正解を見つける方法もありますが、桁数が 14 になりますのでそれも難しいでしょう。しばらく考えていましたが、ある時に、 単純な足し算に還元できることを思いつきました。次の式は、最初に取り上げた 1 から4までの例を分解したものです。
400004 400004(0 が0個から2個) 3000300 30003(0 が0個から3個) 20020000 2002(0 が0個から4個) + 1010 + 101(0 が0個から5個) ----------- -------------------------- 23421314
上の左側は、正解から同じ数字を抜き出して、間と末尾の他の数字を 0 に置き換えたものです。当然それを足し算すると、正解が得られますね。ですから正解を得るためには、 右側に示してあるように、末尾の 0 の数の組み合わせを調べればよいことがわかります。 このアルゴリズムを実現したのが、上のプログラムです。
プログラムとしては、ごく易しいものなので簡単に説明します。 プログラムの中心部は、search サブルーチンで、それがすべてです。その search サブルーチンの大まかな流れは、以下の通りです。
sub search { 2つの引数を受け取り、それを元に2つの変数を作成する $num -- 今回処理する数字 (例: 5) $added -- 前の数字までの加算結果 $work -- 5000005 $flag -- 5.....5 正規表現チェック用 foreach (末尾の 0 の数のリスト) { $tmp -- $work をコピーして末尾に 0 を付加する $add = $added + $tmp -- 前の結果に今回の数字列を加える if ($add =~ /$flag/) { -- 加算が成功 if ($num == 1) { -- 数字並べ終了 重複解をチェックして、保存する } else { -- 計算の途中の場合は、search を再帰呼び出しする search($num - 1, $add) -- 次の数字と計算結果を渡す } } else { 加算が失敗した場合は、次へ } } }
それでは、プログラムを実行してテストしてみます。 $max に 3 から 9 までの数値を指定したときの出力は、以下の通りです。
231213 解の数: 1 23421314 解の数: 1 5: 解なし 6: 解なし 14156742352637 15146735423627 15163745326427 52462754316137 25623745361417 ... 53672352461714 46171435623725 16172452634753 46171452632753 56171354632742 解の数: 26 8: 解なし 9: 解なし
3 と 4 には、解が1つ (重複解を含めると2つ) あります。5 と 6 には解がなく、7 には 26 の解があります。また、8 と 9 には解がありません。 これで、今回は終わりのはずでしたが ..... 。
実は、上の出力には誤りがあります。本当は、8 には解があります (9 の場合は、過程に誤りがあるが結果的には正しい)。 プログラムの実行の様子を覗いて見ると、思いがけないところに意外な落とし穴がありました。 原因は、足し算の過程で桁数が 16 桁になると、指数表記になってしまうためと分かりました (この点については、Perl の実行環境によって異なるのかも知れませんが)。 原因が分かったところで、プログラムを修正してみましょう。 ここでは、興味深い方法がありますので、それを紹介します。
use strict; my ($count, @result); my $max = 8; my $lens = $max * 2; search($max, '0' x $lens); sub search { my ($num, $added) = @_; my $work = $num . '0' x $num . $num; my $flag = $work; $flag =~ s/0/./g; foreach (0..($lens - length($work))) { my $tmp = $work . '0' x $_; $tmp = '0' x ($lens - length($tmp)) . $tmp; my $add = "$added" | "$tmp"; if ($add =~ /$flag/) { if ($num == 1) { my $turn = reverse $add; last if grep /$turn/, @result; push @result, $add; $count++; last; } else { search($num - 1, $add); } } else { next; } } } if ($count) { print join("\n", @result), "\n"; print "解の数: $count\n"; } else { print "$max: 解なし\n"; }
元のプログラムからの変更箇所は、太字で表示してあります。 ほんの少しの変更です。元のプログラムで足し算で行っていたところを、 このプログラムでは文字列同士の論理和で行っています。私自身も今まで一度も使ったことがなく、 おぼろげに覚えていただけでした。文字列同士の論理和を使う場合には、文字数 (桁数) を揃えることと、明示的に文字列として指定する必要があります。 文字列同士の論理和は、同じ位置にある文字同士の文字コードの論理和がとられます。 ちなみに、数字の文字コードは以下の通りになります。
0 0011 0000 1 0011 0001 2 0011 0010 3 0011 0011 4 0011 0100 5 0011 0101 6 0011 0110 7 0011 0111 8 0011 1000 9 0011 1001
上の文字コードを見ると、0 と他の数字の論理和をとると他の数字になることが分かります。 ですから、0 + 1 と "0" | "1" は、同じ結果が得られます。なお、今回のプログラムのチェック ($add =~ /$flag/) のコードの場合には、大きな数字から論理和をとる必要があります。 例えば 5 の場合、6 以上の数字と論理和をとると必ず 5 以外になりますが、4 以下の数字と論理和をとると 4 と 1 では結果が 5 となり、0 と論理和をとったか否かが不明になります。
今度こそ、本当に終わりです。最後に、$max が 8 のときの出力結果を示します。
1514678542362738 1516478534623728 3564378546121728 3645378465121728 1316738524627548 ..... 3568371516428724 1618274265348735 5618175264238743 6418174625328735 6238273651418754 解の数: 150
(2005/05/10)