今回は、1から N までの連続した整数を、隣り合った整数の和が平方数になるように並べます。 下の例は、最初が1から 15 までを1列に並べたものです。1から 15 までを並べたものでは、最初の数字 8 と最後の数字 9 の和は平方数になっていません。N の値を大きくすることで、最初の数字と最後の数字の和が平方数になるものが得られます。 長方形で示してあるのが、1から 32 までを並べたものです。
8 1 15 10 6 3 13 12 4 5 11 14 2 7 9 1 15 10 26 23 2 14 22 27 9 16 20 8 29 28 7 21 18 4 31 32 17 19 30 6 3 13 12 24 25 11 5
use strict; my $n = 15; my @sq = map { $_ ** 2 } 2 .. int(sqrt($n + ($n - 1))); my %otonari; my $start = 1; foreach my $i (1 .. $n) { foreach my $sq (@sq) { next if $sq - $i < 1; next if $sq - $i == $i; last if $sq - $i > $n; push @{$otonari{$i}}, $sq - $i; } } foreach (sort { $a <=> $b } keys %otonari) { $start = $_ if @{$otonari{$_}} < @{$otonari{$start}}; } if ($n <= 14) { exit; } elsif (@{$otonari{$start}} == 1) { search("$start:${$otonari{$start}}[0]"); } else { print "ring only:\n"; search("${$otonari{$start}}[0]:$start:${$otonari{$start}}[1]"); } sub search { my $part = shift; my ($i) = $part =~ /\b(\d+)$/; foreach my $j (@{$otonari{$i}}) { next if $part =~ /\b$j\b/; my $work = $part . ':' . $j; if ($work =~ tr/:// == $n - 1) { if (@{$otonari{$start}} == 1) { $work =~ s/:/ /g; print "$work\n"; } else { $work =~ /^(\d+).*\b(\d+)$/; if (sqrt($1 + $2) !~ /\./) { $work =~ s/^(.*):(1:.*)$/$2:$1/; my @work = map { $_ <= 9 ? ' ' . $_ : $_ } split /:/, $work; my ($line, $blank); if ($n % 2) { $line = join(' ', @work[0 .. ((($n - 8) + 1) / 2 - 1)]); @work = @work[(($n - 8) + 1) / 2 .. $#work]; } else { $line = join(' ', @work[0 .. (($n - 8) / 2 - 1)]); @work = @work[($n - 8) / 2 .. $#work]; } print " $line\n"; $blank = ' ' x length($line); print "$work[$#work]$blank$work[0]\n"; @work = @work[1 .. ($#work -1)]; print "$work[$#work]$blank$work[0]\n"; @work = @work[1 .. ($#work -1)]; print "$work[$#work]$blank$work[0]\n"; @work = @work[1 .. ($#work -1)]; print "$work[$#work]$blank$work[0]\n"; @work = reverse @work[1 .. ($#work -1)]; if ($n % 2) { print " @work\n\n"; } else { print " @work\n\n"; } } } } else { search($work); } } }
パズルの中には、データの量が膨大になってパソコンでなければ解答を得られないものと、 紙と鉛筆を使って手作業で解けるものがあります。今回の1から N までを1列に並べるパズルは、N の値が小さいときには、手作業でも十分に解くことができます。ここで、1から 15 までを手作業で並べてみましょう。 たた単に、数字の並び替えの試行錯誤で解くのは大変です。次のような「お隣さんリスト」を作成します。
1: 3, 8, 15 6: 3, 10 11: 5, 14 2: 7, 14 7: 2, 9 12: 4, 13 3: 1, 6, 13 8: 1 13: 3, 12 4: 5, 12 9: 7 14: 2, 11 5: 4, 11 10: 6, 15 15: 1, 10
お隣さんリストは、各数字の隣りに置くことのできる数字のリストです。 このお隣さんリストの威力は絶大で、これを使って簡単に並べることができます。リストの 8 と 9 に注目してください。8 と 9 の隣りに置ける数字は、1つしかありません。 ということは、途中には置けずに端に置く必要があることを意味します。 先頭に 8 (または 9) を置いて、リストを見ながら並べます。
_6_10_15 x | _3_13_12_4_5_11_14_2_7_9 x | 8_1_15_10_6_3_13_12_4_5_11_14_2_7_9 o
どうですか、 簡単でしょう。ほどんど選択肢もなく、並べることができました。 絶大な威力のある (お隣さんリスト改名) 隣接リストは、プログラムでも使わない手はありません。 次は、隣接リストを表示するプログラムと、N が 14, 15, 30, 31 のときの出力です。
use strict; my $n = 15; my @sq = map { $_ ** 2 } 2 .. int(sqrt($n + ($n - 1))); my %otonari; foreach my $i (1 .. $n) { foreach my $sq (@sq) { next if $sq - $i < 1; next if $sq - $i == $i; last if $sq - $i > $n; push @{$otonari{$i}}, $sq - $i; } } foreach (sort { $a <=> $b } keys %otonari) { print "$_ => [", join(', ', @{$otonari{$_}}), "]\n"; }
$n: 14 $n: 15 $n: 30 $n: 31 1 => [3, 8] 1 => [3, 8, 15] 1 => [3, 8, 15, 24] 1 => [3, 8, 15, 24] 2 => [7, 14] 2 => [7, 14] 2 => [7, 14, 23] 2 => [7, 14, 23] 3 => [1, 6, 13] 3 => [1, 6, 13] 3 => [1, 6, 13, 22] 3 => [1, 6, 13, 22] 4 => [5, 12] 4 => [5, 12] 4 => [5, 12, 21] 4 => [5, 12, 21] 5 => [4, 11] 5 => [4, 11] 5 => [4, 11, 20] 5 => [4, 11, 20, 31] 6 => [3, 10] 6 => [3, 10] 6 => [3, 10, 19, 30] 6 => [3, 10, 19, 30] 7 => [2, 9] 7 => [2, 9] 7 => [2, 9, 18, 29] 7 => [2, 9, 18, 29] 8 => [1] 8 => [1] 8 => [1, 17, 28] 8 => [1, 17, 28] 9 => [7] 9 => [7] 9 => [7, 16, 27] 9 => [7, 16, 27] 10 => [6] 10 => [6, 15] 10 => [6, 15, 26] 10 => [6, 15, 26] 11 => [5, 14] 11 => [5, 14] 11 => [5, 14, 25] 11 => [5, 14, 25] 12 => [4, 13] 12 => [4, 13] 12 => [4, 13, 24] 12 => [4, 13, 24] 13 => [3, 12] 13 => [3, 12] 13 => [3, 12, 23] 13 => [3, 12, 23] 14 => [2, 11] 14 => [2, 11] 14 => [2, 11, 22] 14 => [2, 11, 22] 15 => [1, 10] 15 => [1, 10, 21] 15 => [1, 10, 21] 16 => [9, 20] 16 => [9, 20] 17 => [8, 19] 17 => [8, 19] 18 => [7] 18 => [7, 31] 19 => [6, 17, 30] 19 => [6, 17, 30] 20 => [5, 16, 29] 20 => [5, 16, 29] 21 => [4, 15, 28] 21 => [4, 15, 28] 22 => [3, 14, 27] 22 => [3, 14, 27] 23 => [2, 13, 26] 23 => [2, 13, 26] 24 => [1, 12, 25] 24 => [1, 12, 25] 25 => [11, 24] 25 => [11, 24] 26 => [10, 23] 26 => [10, 23] 27 => [9, 22] 27 => [9, 22] 28 => [8, 21] 28 => [8, 21] 29 => [7, 20] 29 => [7, 20] 30 => [6, 19] 30 => [6, 19] 31 => [5, 18]
隣接リストを分析すると、いろいろなことが分かります。1列に並べることのできる N の最小値は、15 になります。では、なぜ N が 14 以下では、並べることができないのでしょうか。14 の隣接リストを見てみると、接続対象数1(隣りに置ける数字が1つ) の数字が 8, 9, 10 と3つあります。端は2つしかないため、並べることは不可能となります。N が 13 以下も同様です。接続対象数1の数字が2つ以下であることが、1列に並べるための1つの必要条件になります。 しかし、この必要条件を満たしていても、必ずしも並べることができるわけではありません。 N が 15, 16, 17 では並べることができますが、(18 は接続対象数1の数字が3つ) 19, 20, 21, 22 では並べることができません。次の 23 では並べることができ、24 では並べることができなく、25 からは調べた限りではずっと並べることができます。
次は、リング状に並べることを考えます。リング状に並べるためには、 どの数字の両側にも数字を置かなければならないので、接続対象数1の数字がないことが必要です。N が 30 以下では、すべて1つ以上の接続対象数1の数字があり、リング状に並べることができません。N が 31 になって初めて接続対象数1の数字がなくなり、リング状に並べるための1つの要件を備えたことになります。 実際には、N が 31 では並べることができなく、32 からは並べることができます。
プログラムの最初で、この隣接リストを作成します。 隣接リストを作成したら、このリストを利用して数字並べを行います。 その前に隣接リストを解析して、探索を開始するための数字を決定します。そして、その数字を変数 $start に入れておきます。
foreach (sort { $a <=> $b } keys %otonari) { $start = $_ if @{$otonari{$_}} < @{$otonari{$start}}; } if ($n <= 14) { exit; } elsif (@{$otonari{$start}} == 1) { search("$start:${$otonari{$start}}[0]"); } else { print "ring only:\n"; search("${$otonari{$start}}[0]:$start:${$otonari{$start}}[1]"); }
$start には、接続対象数の最小の数字のうちの、最も小さな数字が入ります。上の表で N が 15 のときには接続対象数1の 8, 9 のうちの 8 が、N が 31 のときには接続対象数2の 16, 17, 18, 25, 26, 27, 28, 29, 30, 31 のうちの 16 が $start に入ることになります。探索を開始する数字 $start が決まったら、数字並べを探索するサブルーチン search を呼び出します。search に渡す引数は、接続対象数1の場合には「$start と $start に接続できる数字」を、 接続対象数2の場合には「$start の両側に接続できる数字」をコロンで結んだものとなります。
search サブルーチンは、リング表示のコードが少し長いだけで、 構造はそれほど複雑ではありません。以下が search サブルーチンの構造になります。
sub search { my $part = shift; my ($i) = $part =~ /\b(\d+)$/; foreach my $j (@{$otonari{$i}}) { next if $part =~ /\b$j\b/; my $work = $part . ':' . $j; # 末尾に次の数字を接続 if ($work =~ tr/:// == $n - 1) { # 探索が成功 if (@{$otonari{$start}} == 1) { # ライン表示 $work =~ s/:/ /g; print "$work\n"; } else { # ライン表示ではない $work =~ /^(\d+).*\b(\d+)$/; if (sqrt($1 + $2) !~ /\./) { # 先頭と末尾の和が平方数 # リング表示 } } } else { # 探索の途中、再帰呼び出しをする search($work); } } }
search サブルーチンは、探索途中の文字列 (数字とコロンを連結したもの) を受け取ります。そして、文字列の末尾の数字に繋げることのできる数字を隣接リストから取得して、 コロンを間に挟んで繋げます。連結した文字列がまだ探索途中である場合は、さらに search を再帰呼び出しします。
探索が成功した場合は、$start の数字の接続対象数を調べます。 接続対象数が1のときは、ライン表示になります。ライン表示は、コロンをスペースに置き換えて、 そのまま出力します。接続対象数が1でない (2である) ときには、ライン表示は行わずにリング表示のみを行います。 そのために、先頭と末尾の数字の和が平方数になっていることを確認します。
プログラムの実行結果は、次のようになります。
$n: 15 8 1 15 10 6 3 13 12 4 5 11 14 2 7 9 $n: 16 8 1 15 10 6 3 13 12 4 5 11 14 2 7 9 16 $n: 17 16 9 7 2 14 11 5 4 12 13 3 6 10 15 1 8 17 $n: 18, 19, 20, 21, 22 --- 解なし $n: 23 18 7 2 23 13 12 4 21 15 10 6 19 17 8 1 3 22 14 11 5 20 16 9 18 7 9 16 20 5 11 14 2 23 13 12 4 21 15 10 6 19 17 8 1 3 22 18 7 9 16 20 5 11 14 22 3 1 8 17 19 6 10 15 21 4 12 13 23 2 $n: 24 --- 解なし $n: 25 解の数 10 18 7 2 23 13 12 24 25 11 14 22 3 1 8 17 19 6 10 15 21 4 5 20 16 9 18 7 9 16 20 5 4 21 15 10 6 19 17 8 1 3 22 14 2 23 13 12 24 25 11 ..... $n: 26 解の数 12 18 7 2 14 22 3 13 23 26 10 6 19 17 8 1 15 21 4 12 24 25 11 5 20 16 9 18 7 9 16 20 5 4 21 15 1 8 17 19 6 10 26 23 2 14 11 25 24 12 13 3 22 ..... $n: 27 解の数 35 18 7 2 14 11 5 20 16 9 27 22 3 13 23 26 10 6 19 17 8 1 15 21 4 12 24 25 18 7 2 14 11 25 24 1 8 17 19 6 3 22 27 9 16 20 5 4 12 13 23 26 10 15 21 ..... $n: 28 解の数 52 18 7 2 14 11 5 20 16 9 27 22 3 6 19 17 8 28 21 4 12 13 23 26 10 15 1 24 25 18 7 2 14 11 25 24 1 3 6 19 17 8 28 21 15 10 26 23 13 12 4 5 20 16 9 27 22 ..... $n: 29 解の数 19 18 7 29 20 16 9 27 22 3 13 12 4 5 11 14 2 23 26 10 6 19 17 8 28 21 15 1 24 25 18 7 29 20 16 9 27 22 3 13 12 4 5 11 25 24 1 15 21 28 8 17 19 6 10 26 23 2 14 ..... $n: 30 解の数 20 18 7 29 20 16 9 27 22 3 13 12 4 5 11 14 2 23 26 10 6 30 19 17 8 28 21 15 1 24 25 18 7 29 20 16 9 27 22 3 13 12 4 5 11 25 24 1 15 21 28 8 17 19 30 6 10 26 23 2 14 ..... $n: 31 --- 解なし(ここからリング表示のみ、ライン解はある) ring only: $n: 32 ring only: 1 15 10 26 23 2 14 22 27 9 16 20 8 29 28 7 21 18 4 31 32 17 19 30 6 3 13 12 24 25 11 5 $n: 33 ring only: 1 8 28 21 4 32 17 19 30 6 3 13 12 15 24 10 25 26 11 23 5 2 14 22 27 9 16 33 31 18 7 29 20 $n:34 解の数 11 9seconds ring only: 1 8 28 21 15 10 26 23 13 12 24 25 11 3 5 33 4 31 32 18 17 7 29 20 16 9 27 22 14 2 34 30 6 19 1 15 21 28 8 17 32 4 5 11 25 24 12 3 13 33 23 31 26 18 10 7 29 20 16 9 27 22 14 2 34 30 19 6 ..... $n: 35 解の数 57 27seconds ring only: 1 35 29 20 16 33 3 22 27 9 7 18 31 5 8 4 28 32 21 17 15 19 10 26 23 13 12 24 25 11 14 2 34 30 6 1 35 29 20 16 33 3 13 12 24 25 11 14 22 8 27 28 9 21 7 15 18 10 26 23 2 34 30 6 19 17 32 4 5 31 ..... $n: 36 解の数 31 50seconds ring only: 1 35 29 20 16 33 3 6 19 30 34 2 23 26 8 10 17 15 32 21 4 28 5 31 18 7 9 27 22 14 11 25 24 12 13 36 1 8 17 32 4 12 13 36 28 21 15 10 26 23 24 2 25 34 11 30 5 19 31 18 7 9 27 22 14 35 29 20 16 33 3 6 ..... $n: 37 解の数 20 93seconds ring only: 1 35 29 20 16 33 3 22 14 2 23 26 10 6 19 24 30 25 34 11 15 5 21 31 18 7 9 27 37 12 13 36 28 8 17 32 4 1 8 28 36 13 23 26 10 6 30 19 17 32 4 21 35 15 29 34 20 2 5 14 31 18 7 9 16 33 3 22 27 37 12 24 25 11 .....
(2006/03/15)