三角形の和

下の図には、6つの三角形があります。三角形の頂点の A から J に 1 から 19 の奇数の数字を入れて、6つの三角形の頂点の数字の和が等しくなるようにして下さい、 というのが今回の問題です。言い換えれば、右側の等式を解いて下さい、というのと同じです。

    
A + B + C = B + D + E 
          = C + E + F 
          = D + G + H
          = E + H + I
          = F + I + J

今回のプログラムは、各数字の使用回数に着目して作成しました。 ここでの使用回数とは、各数字の小さな三角形への所属数です。 上の図で各アルファベットの出現回数はそれぞれ1ですが、右の式では違っています。 大きな三角形の各頂点 A, G, J は1になっており、内側にある E は3であり、それ以外は2になっています。 この点に注目して、プログラムを見て下さい。

プログラム

use strict;
my (%three, @triangle);
my %henkan = (a => 11, b => 13, c => 15, d => 17, e => 19);
print " A  B  C  D  E  F  G  H  I  J\n";
gen_3('', 1, 3, 5, 7, 9, 'a', 'b', 'c', 'd', 'e');

sub gen_3 {
  my ($num3, @list) = @_;
  foreach my $n (0..$#list) {
    my $work = $num3; $work .= $list[$n];
    if (length($work) == 3) {
      my $tmp = eval(join '+', map { /[a-e]/ ? $henkan{$_} : $_ } split //, $work);
      push @{$three{$tmp}}, $work;
    } else {
      gen_3($work, @list[$n + 1 .. $#list]);
    }
  }
}

foreach my $sum (sort { $a <=> $b } keys %three) {
  next if @{$three{$sum}} < 6;
  @triangle = ();
  gen_6(@{$three{$sum}});
}

sub gen_6 {
  my @list = @_;
  foreach my $n (0..$#list) {
    push @triangle, $list[$n];
    if (@triangle == 6) { check(@triangle); }
    else { gen_6(@list[$n + 1 .. $#list]); }
    pop @triangle;
  }
}

sub check {
  my @list = @_;
  return if join('', @list) !~
            /^(?=.*1)(?=.*3)(?=.*5)(?=.*7)(?=.*9)(?=.*a)(?=.*b)(?=.*c)(?=.*d)(?=.*e)/;
  my (%flag, @result);
  $flag{$_}++ foreach split //, join '', @list;
  return if join('', sort values %flag) !~ /^1{3}2{6}3$/;
  foreach (@list) {
    /(.)(.)(.)/;
    return if join('', sort($flag{$1}, $flag{$2}, $flag{$3})) !~ /22/;
  }
  my @sorted_keys = map  { $_->[0] }
                    sort { $a->[1] <=> $b->[1] or $a->[0] cmp $b->[0] }
                    map  { [ $_, $flag{$_} ] } keys %flag;
  @result[0, 4] = @sorted_keys[0, 9];
  my ($work) = grep /$result[0]/, @list;
  @list = grep ! /$work/, @list;
  my ($i, $j) = grep ! /$result[0]/, split //, $work;
  @result[1, 2] = ($i, $j);
  ($work) = grep /$result[1]/, @list;
  @list = grep ! /$work/, @list;
  ($i) = grep { ! /$result[1]/ and ! /$result[4]/ } split //, $work;
  $result[3] = $i;
  ($work) = grep /$result[2]/, @list;
  @list = grep ! /$work/, @list;
  ($i) = grep { ! /$result[2]/ and ! /$result[4]/ } split //, $work;
  $result[5] = $i;
  ($work) = grep /$result[3]/, @list;
  @list = grep ! /$work/, @list;
  if ($work =~ /$sorted_keys[1]/) {
    $result[6] = $sorted_keys[1]; $result[9] = $sorted_keys[2];
  } else {
    $result[6] = $sorted_keys[2]; $result[9] = $sorted_keys[1];
  }
  ($i) = grep { ! /$result[3]/ and ! /$result[6]/ } split //, $work;
  $result[7] = $i;
  ($work) = grep /$result[7]/, @list;
  ($i) = grep { ! /$result[4]/ and ! /$result[7]/ } split //, $work;
  $result[8] = $i;
  @result =  map { /[a-e]/ ? $henkan{$_} : $_ } @result;
  printf "%2d %2d %2d %2d %2d %2d %2d %2d %2d %2d\n", @result;
}

プログラムの解説

今回のパズルは3角形の和ですので、1から 19 の奇数の数字の内から3つの数字の組合せのリストを作成します。3つの数字は、文字列として連結します。 そのときに、このまま連結してしまっては、1桁の数字と2桁の数字の区別がつかなくなってしまいます。 そこで、2桁の数字は記号化して扱うことにしました。

my %henkan = (a => 11, b => 13, c => 15, d => 17, e => 19);

%henkan は、記号化したアルファベットと数字の変換テーブルです。 必要なときに、参照することになります。これで 1 から 19 の奇数の数字 10 個は、1文字として扱えるようになりましたので、3つの数字の組合せのリストを作成します。 作成されたリストは、ハッシュ %three に格納します。キーは、3つの数字の和になります。%three は、以下のようになります。

 9 => [135]
11 => [137]
13 => [139, 157]
15 => [13a, 159, 357]
17 => [13b, 15a, 179, 359]
19 => [13c, 15b, 17a, 35a, 379]
21 => [13d, 15c, 17b, 19a, 35b, 37a, 579]
23 => [13e, 15d, 17c, 19b, 35c, 37b, 39a, 57a]
25 => [15e, 17d, 19c, 1ab, 35d, 37c, 39b, 57b, 59a]
27 => [17e, 19d, 1ac, 35e, 37d, 39c, 3ab, 57c, 59b, 79a]
29 => [19e, 1ad, 1bc, 37e, 39d, 3ac, 57d, 59c, 5ab, 79b]
31 => [1ae, 1bd, 39e, 3ad, 3bc, 57e, 59d, 5ac, 79c, 7ab]
33 => [1be, 1cd, 3ae, 3bd, 59e, 5ad, 5bc, 79d, 7ac, 9ab]
35 => [1ce, 3be, 3cd, 5ae, 5bd, 79e, 7ad, 7bc, 9ac]
37 => [1de, 3ce, 5be, 5cd, 7ae, 7bd, 9ad, 9bc]
39 => [3de, 5ce, 7be, 7cd, 9ae, 9bd, abc]
41 => [5de, 7ce, 9be, 9cd, abd]
43 => [7de, 9ce, abe, acd]
45 => [9de, ace, bcd]
47 => [ade, bce]
49 => [bde]
51 => [cde]

ハッシュ %three の値は、3つの数字の組合せのリストです。 このうちリストの数が5以下のエントリーは、6つの三角形に割り当てることができないので探索から除外します。 上の太字で表示してある部分だけが、探索の対象になります。探索対象になるキーの値 21 から 39 のエントリーは、リストの6つの組合せをすべて生成して、check サブルーチンを呼び出します。ここで、冒頭の式を再掲します。

      A + B + C = B + D + E 
                = C + E + F 
                = D + G + H
                = E + H + I
                = F + I + J

check サブルーチンは、6つの数字の組合せのリストを受け取ります。 受け取ったリストが上の式に該当するか否かを、以下の手順でチェックします。

  1. まず最初に、10 種類の数字がすべて含まれているかを調べます。[チェック数: 1078   通過数: 552]
    return if join('', @list) !~
            /^(?=.*1)(?=.*3)(?=.*5)(?=.*7)(?=.*9)(?=.*a)(?=.*b)(?=.*c)(?=.*d)(?=.*e)/;
  2. 次に、全体の各数字の使用回数を調べます。使用回数の組合せは、1112222223 になっていなければなりません。[チェック数: 552   通過数: 130]
    $flag{$_}++ foreach split //, join '', @list;
    return if join('', sort values %flag) !~ /^1{3}2{6}3$/;
  3. 全体の使用回数の組合せが合っていたら、今度は三角形毎に使用回数の組合せを調べます。 使用回数の組合せの中には、"2" が2つ入っている必要があります。[チェック数: 130   通過数: 2]
    foreach (@list) {
      /(.)(.)(.)/;
      return if join('', sort($flag{$1}, $flag{$2}, $flag{$3})) !~ /22/;
    }

以上の3つのチェックで、残っているのは正解の2つだけになります。 ここからは、A から J に具体的な数字を割り当てる作業に移ります。 具体的な数字は、配列 @result に格納します。配列の要素の先頭から A, B, ... F, J の順で、具体的な数字を格納します。

  1. 現時点で割り当てることができる数字は、使用回数 3 の E と、交換可能である使用回数1の A, G, J になります。E には、使用回数 3 の数字を割り当てます。A, G, J にも数字を割り当てることができますが、とりあえず A のみに使用回数1の内の最小の数字を割り当てます。

    my @sorted_keys = map  { $_->[0] }
                      sort { $a->[1] <=> $b->[1] or $a->[0] cmp $b->[0] }
                      map  { [ $_, $flag{$_} ] } keys %flag;
    @result[0, 4] = @sorted_keys[0, 9];
  2. これで A, E に、2つの数字を割り当てることができました。割り当てた後の式の様子は、次のようになります。 太字で示したアルファベットが、割り当て済みの数字です。

          A + B + C = B + D + E
                    = C + E + F 
                    = D + G + H
                    = E + H + I
                    = F + I + J
  3. 次は、B と C の2つの数字を割り当てます。配列 @list には、上の6つの式に対応する要素が格納されています。A は使用回数が1ですので、A を含む要素は1つだけしかありません。それを取り出して、A 以外の2つの数字を B と C に割り当てます。この時点では、割り当てられている数字が A と E だけの2つで左右対称を保持しているので、どちらの数字を割り当てても構いません。@list の要素はすべて昇順に並んでいるので、結果的に小さな数字が B に、大きな数字が C に割り当てられることになります。

    my ($work) = grep /$result[0]/, @list;
    @list = grep ! /$work/, @list;
    my ($i, $j) = grep ! /$result[0]/, split //, $work;
    @result[1, 2] = ($i, $j);

    一番上の三角形の頂点 A, B, C には、すべて数字の割り当てが済みましたので、それに対応する @list の要素を除去しておきます。これは、残りの数字を割り当てる際に必要になります。

          A + B + C = B + D + E
                    = C + E + F 
                    = D + G + H
                    = E + H + I
                    = F + I + J
  4. @list の中には、上の右側の式に対応する5つの要素が入っています。最初に使用回数が2であった B と C は、この段階で @list の中には1つだけしかありません。幸いにも、D は B と同じ要素の中にありますので、その要素を取り出して B でもなく E でもない数字を D に割り当てます。F も D と同じ方法で、割り当てることができます。

          A + B + C = B + D + E
                    = C + E + F
                    = D + G + H
                    = E + H + I
                    = F + I + J
  5. G と J は、@sorted_keys の要素1あるいは 2 のいずれかです。そこで、D と同じ要素内にあるのがどちらであるのかを正規表現を使って調べます。その結果によって、G と J に数字を割り当てます。

    ($work) = grep /$result[3]/, @list;
    @list = grep ! /$work/, @list;
    if ($work =~ /$sorted_keys[1]/) {
      $result[6] = $sorted_keys[1]; $result[9] = $sorted_keys[2];
    } else {
      $result[6] = $sorted_keys[2]; $result[9] = $sorted_keys[1];
    }
  6. 残っているのは、H と I だけです。これは、3. と同じ方法で割り当てることができます。 これで、すべて数字の割り当てが終わりました。

正解は、配列 @result に入っています。@result の要素は、まだ記号化されたままの数字ですので、ここで変換して出力します。 今回のプログラムの出力は、次のようになります。 少し分かりにくい表示かも知れませんが、図と照らし合わせて確認して下さい。

 A  B  C  D  E  F  G  H  I  J
 3 11 15  5 13  1 17  7  9 19
 1 11 19 13  7  5  3 15  9 17

(2006/02/01)

TopPage