逆三角形の円

今回のパズルは、逆三角形に並べた円 (6, 10 15 個) に数字を配置する問題です。 下の図は、逆三角形に並べた円6つに1から6を配置したものです。 下の円は、上2つの円の数字の差になっています。

zu

今回のパズルは、何を手掛かりにして解いたらいいか少し分かりませんでした。 結局は、最大の数字は1番上の段に配置されますので、その数字を含む順列を生成して1番上の段に配置して、 2番目の段からは計算で求めるプログラムにしました。ちなみに、難しい理屈は分かりませんが、 6段以上の逆三角形には解がないそうです。良かったというべきかも..... 。

プログラム

use strict;
my $m = 10;    # 6 or 10 or 15
my @elms = (11);

while (@elms < $m) {
  if ($elms[0] =~ /(.)\1/) { unshift @elms, ((split //, $elms[0])[0] . '1') + 10; }
  else { unshift @elms, $elms[0] + 1; }
}

my $top = (split //, $elms[0])[0] - 3;
my (@work, $head, $tail);

foreach my $i (1 .. $m - 2) {
  foreach my $j ($i + 1 .. $m - 1) {
    $head = $i; $tail = $j;
    if ($top == 0) {
      check($head, $tail, $m);
      check($head, $m, $tail);
      check($m, $head, $tail);
    } else {
      perm(grep { ! /^$head$/ and ! /^$tail$/ } 1 .. $m - 1);      
    }
  }
}

sub perm {
  my @nums = @_;
  foreach my $i (0 .. $#nums) {
    push @work, $nums[$i];
    if (@work == $top) { 
      my @tmp = ($head, @work, $tail);
      my $n = $m - 1;
      if (grep /^$n$/, @tmp) {
        foreach (0 .. $#tmp + 1) {
        my @temp = @tmp;
        splice @temp, $_, 0, $m;
        check(@temp);
        }
      } elsif (grep /^1$/, @tmp) {
        my $index; my $j = 0;
        foreach (@tmp) {
          if (/^1$/) {
            $index = $j;
            last;
          }
          $j++;
        }
        my @temp = @tmp;
        splice @temp, $j, 0, $m;
        check(@temp);
        my @temp = @tmp;
        splice @temp, $j + 1, 0, $m;
        check(@temp);
      }
    } else {
      perm(@nums[0 .. $i - 1, $i + 1 .. $#nums]);
    }
    pop @work;
  }
}

sub check {
  my @tmp = @_;
  my %result;
  @result{@elms} = @tmp;
  my (@before, $i, $j, $k, $l); $i = $elms[0]; $j = $i - 1; $k = $j - 10;
  @before = @tmp;
  while ($j > 11) {
    $l = abs($result{$i} - $result{$j});
    if (grep /^$l$/, @before) { return; }
    else {
      $result{$k} = $l; push @before, $l;
      if ($i =~ /2$/) {
        $i = ((split //, $i)[0] - 1) . ((split //, $i)[0] - 1);
        $j = $i - 1; $k = $j - 10;
      } else {
        $i--; $j--; $k--;
      }
    }
  }
  my $hs = ' ';
  foreach (sort { $b <=> $a } keys %result) {
    print "$result{$_} " if $_ =~ /[^1]$/;
    if ($_ =~ /1$/) {
      print "$result{$_}\n$hs";
      $hs .= ' ';
    }
  } 
  print "\n";
}

プログラムの解説

ここからは、円の数を 10 個配置した逆三角形を例にして話を進めていきます。 まずは、円の1つ1つに番号を付けます。番号は、下の図のように付けます。10 の位は段の位置を表し、1の位は右から数えた位置を表します。

zu
my @elms = (11);

while (@elms < $n) {
  if ($elms[0] =~ /(.)\1/) { unshift @elms, ((split //, $elms[0])[0] . '1') + 10; }
  else { unshift @elms, $elms[0] + 1; }
}

図の右側は、自動的に円に番号を生成するコードです。コードを実行すると @elms には、円の番号 44, 43, ... 21, 11 が格納されます。 このコードは、9段までの逆三角形の円に番号を付けることができます。 また @elms の先頭の要素を見れば、正しい逆三角形になっているか判別することもできます。@elms の最初の要素は、正しい逆三角形では同じ数字のゾロ目になります。

次は、順列を生成して最上段 44, 43, 42, 41 に数字を配置します。 順列生成のコードは、最初は単純な以下のようなものでした。

my $m = 10; my $n = (split //, $elms[0])[0];
my @work = ();
search(1 .. $m);

sub search {
  my @list = @_;
  foreach my $i (0 .. $#list) {
    push @work, $list[$i];
    if (@work == $n) {
      check(@work) if $work[0] < $work[-1];
    } else {
      search(@list[0 .. $i - 1, $i + 1 .. $#list]);
    }
    pop @work;
  }
}

上のコードでは、円が6個の場合には 120、円が 10 個の場合には 5040、円が 15 の場合には 360360 個の順列が生成されます。円の数が6個と 10 個の場合は、プログラムはすぐに終了します。 しかしながら、円が 15 の場合は、私のパソコンで 29 秒くらい掛かってしまいました。

そこで、もう少し工夫することにしました。ここで、順列の性質を考えてみましょう。 順列は、正並びの順列と逆並びの順列が1つの組になっています。1, 2, 3, 4 の正並びの順列に対する、4, 3, 2, 1 という逆並びの順列が必ず存在します。今回のパズルでは、重複解となるので片方を除外する必要があります。 上のコードの if $work[0] < $work[-1] は、逆並びの順列を除外するためのものです。しかしながら、上のコードでは生成した後で除外しています。 この逆並びの順列を最初から生成しなければ、半分の時間で実行できることになります。 次のコードは、正並びの順列のみを生成するものです。

foreach my $i (1 .. $m - 1) {
  foreach my $j ($i + 1 .. $m) {
    # $i と $j を除外した残りの数字の順列を生成する
  }
}

$i は先頭の数字、$j は末尾の数字になります。$i と $j を除いた残りの数字の順列を生成して、$i と $j の間に入れます。これで、正並びの順列のみを生成できます。

次に、最大値の $m について考えてみましょう。$m は、必ず最上段に入ります。 そこで、1つ小さいサイズの順列を生成して、後から $m を配置するようにすれば、 生成する順列数を少なくすることができます。次に大きな値の ($m - 1) は、最上段に入るか、あるいは最上段が $m, 1 (または 1, $m) になっている必要があります。この点についても、チェックするようにしています。 今回のパズルの順列の生成の仕組みは、以上の通りです。 これで、円の数が 15 個の場合も、4秒くらいでプログラムが終わるようになりました。

順列を生成したら、check サブルーチンを呼び出します。check サブルーチンでは、渡された引数を %result ハッシュに格納します。%result のキーは円の番号で、値は配置される数字になります。 渡された引数は最上段の数字だけですので、最初の %result は下図のようになります。

zu       zu
失敗の例 成功の例

上から2段目からの円の数字は、計算によって求めていくことになります。 その部分のコードを、以下に再掲します。

my (@used, $i, $j, $k, $l); $i = $elms[0]; $j = $i - 1; $k = $j - 10;
@used = @tmp;
while ($j > 11) {
  $l = abs($result{$i} - $result{$j});
  if (grep /^$l$/, @used) { return; }
  else { $result{$k} = $l; push @used, $l; }
  if ($j =~ /1$/) {
    $i = ((split //, $i)[0] - 1) . ((split //, $i)[0] - 1);
    $j = $i - 1; $k = $j - 10;
  } else {
    $i--; $j--; $k--;
  }
}

まず最初に、必要な変数を定義します。@used には、すでに使用済みの数字を格納しておきます。この @used を使って、計算によって得られた値の数字が重複していないかを確認します。上から2段目からの数字は、while を使って配置していきます。上の図の "失敗の例" では、円の番号 33 から 9, 2, 4 と数字を配置していき、22 に 7 を配置しようとしたときに、数字が重複しているために失敗します。右側の図では、11 まで数字を配置できますので、正解ということになります。

プログラムの実行結果は、以下のようになります。

$m = 6         $m = 10           $m = 15
 1 6 4          6 10 1 8          6 14 15 3 13
  5 2            4 9 7             8 1 12 10
   3              5 2               7 11 2
                   3                 4 9
 6 1 4                                5
  5 3           6 1 10 8
   2             5 9 2
                  4 7
 2 6 5             3
  4 1
   3            8 10 3 9
                 2 7 6
 6 2 5            5 1
  4 3              4
   1
                8 3 10 9
                 5 7 1
                  2 6
                   4

(2005/11/01)

TopPage