数字並べ

さて、今回の問題は数字並べです。数字は、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)

TopPage