X ナンバープレース

ナンバープレースまたは数独 (「数独」はニコリの登録商標) と呼ばれるパズルは、 有名なパズルですので皆さんもご存知のことと思います。通常のナンバープレースでは、9 x 9 のマス目で 1 〜 9 の数字を使います (左側の図)。一方、今回取り上げる「X ナンバープレース」は、5 x 5 のマス目で 1 〜 5 の数字を使っています (右側の図)。ルールの違いは、ブロックの代わりに対角線が制約条件になる点です。

ナンバープレースの問題例
  (Arto Inkala氏作)
 X ナンバープレースの問題例
  5 3      
8        2 
 7   1  5  
4     5 3  
 1   7    6
  3 2    8 
 6  5     9
  4      3 
      9 7  

• 縦と横の列に同じ数字を配置できない
• 3 x 3 のブロック内に同じ数字を配置できない

    
    
     
      
    
      

• 縦と横の列に同じ数字を配置できない
• 対角線上に同じ数字を配置できない

「X ナンバープレース」パズルを作るキッカケとなったのは、 プログラミング的な興味からです。盤面のサイズを変えたり、ルールを変えたときにどうなるかを探るのは、 面白いものです。失敗することのほうが多いのですが、ときにはパズルとして成立するものが見つかることもあります。 「X ナンバープレース」パズルの盤面サイズを 5 x 5 にしたことが、多少なりともパズルとして面白くしています。5 x 5 のサイズでは、5つの数字が4隅と中央に配置されるという関係が成り立ちます。例えば、サイズが 7 x 7 や 9 x 9 ではそういう関係が成り立ちません。

「X ナンバープレース」パズルは、盤面も小さく手軽に楽しむことができます。 暇があるときに遊んでいただければと思います。ルールや解き方の詳細は、 別のファイル「X ナンバープレースの解き方のヒント」に書きましたので、お読みください。

配置パターンの探索

まず最初に、パズルとしての可能性を探るため、配置パターンを調べてみました。 プログラムでは数字は使わず、アルファベットの A 〜 E を使います。ナンバープレースでは、 数字を入れ替えてもパズルそのものは成り立つため、直接数字を使う意味はありません。アルファベットで解を得たら、 それに対して 120 (5!) 通りの数字の割り当てがあります。

 1 23 45
1
2      
3      
4      
5      

配置パターンの探索は、上図のように横の1列に配置した状態から始めます。2列目からは、 入れることのできるアルファベットを試していきます。21 のマス目は、B, C, D, E を入れることができます。例えば、B を入れたとすると、残りの横の2列と縦の 1 列には B を入れることができなくなります。次の 22 以降のマス目も同様にアルファベットを入れて、 成立する配置パターンを見つけます。

 1  use strict;
 2  my %assign = (11 => 'A', 12 => 'B', 13 => 'C', 14 => 'D', 15 => 'E');
 3  my @cell = (11 .. 15, 21 .. 25, 31 .. 35, 41 .. 45, 51 .. 55);
 4  my (%group, %unassign);
 5  
 6  foreach my $cell (@cell) {    # %group の作成
 7    my ($col, $row) = split //, $cell;
 8    push @{$group{$cell}}, grep { $cell != $_ and (/^$col/ or /$row$/) } @cell;
 9    push @{$group{$cell}}, grep { $cell != $_ } (11, 22, 33, 44, 55) if $col == $row;
10    push @{$group{$cell}}, grep { $cell != $_ } (15, 24, 33, 42, 51) if $col + $row == 6;
11  }
12  
13  foreach my $cell (@cell[5 .. $#cell]) {   # %unassign の作成
14    my @list = 'A' .. 'E';
15    my @same = grep /^1/, @{$group{$cell}};
16    foreach my $key (@same) {
17      @list = grep { $assign{$key} ne $_ } @list;
18    }
19    $unassign{$cell} = [@list, []];
20  }
21  
22  search(sort keys %unassign);   # search サブルーチンを呼び出す
23  
24  sub search {   # マス目にアルファベットを割り当てる
25    my $pos = shift;
26    my @char = @{$unassign{$pos}}; pop @char;
27    foreach my $char (@char) {
28      $assign{$pos} = $char;
29      my @update;
30      foreach my $key (grep { $_ > $pos } @{$group{$pos}}) {
31        my ($idx) = grep { $char eq $unassign{$key}->[$_] } 0 .. $#{$unassign{$key}} - 1;
32        if (defined $idx) {
33          push @update, $key;
34          push @{$unassign{$key}->[-1]}, join('', $idx, splice(@{$unassign{$key}}, $idx, 1));
35        }
36      }
37      my $chk = join '', map { scalar(@{$unassign{$_}}) } @_;
38      if ($chk =~ /^2+$/) {
39        display(@_);      
40      } elsif ($chk !~ /1/) {
41        search(@_);
42      }
43      $assign{$pos} = undef;
44      foreach my $key (@update) {
45        my ($idx, $alph) = split //, pop @{$unassign{$key}->[-1]};
46        splice @{$unassign{$key}}, $idx, 0, $alph;
47      }
48    }
49  }
50  
51  sub display {   # 配置パターンのチェックと表示
52    my %check = %assign;
53    $check{$_} = $unassign{$_}->[0] foreach @_;
54  
55    foreach my $i (1 .. 5) {
56      my $chk = join '', map { $check{$_} } grep /^$i/, @cell;
57      return if $chk =~ /(.).*\1/;
58      $chk = join '', map { $check{$_} } grep /$i$/, @cell;
59      return if $chk =~ /(.).*\1/;
60    }
61    return if "$check{11}$check{22}$check{33}$check{44}$check{55}" =~ /(.).*\1/;
62    return if "$check{15}$check{24}$check{33}$check{42}$check{51}" =~ /(.).*\1/;
63  
64    print "$check{11}$check{12}$check{13}$check{14}$check{15}\n";
65    print "$check{21}$check{22}$check{23}$check{24}$check{25}\n";
66    print "$check{31}$check{32}$check{33}$check{34}$check{35}\n";
67    print "$check{41}$check{42}$check{43}$check{44}$check{45}\n";
68    print "$check{51}$check{52}$check{53}$check{54}$check{55}\n\n";
69  }
   Line 2, 3, 4: 必要な変数の宣言と初期化
• %assign -- 割り当て済みのマス目。最初に横の1列を割り当てる
• @cell -- マス目の番号。数値計算をしやすいように 11 から 55 の2桁の数字を用います
• %group -- キーにマス目の番号を、値にそのマス目の効き筋に属するマス目の番号を格納
• %unassign -- 未割り当てのマス目の割り当てることのできるアルファベット

   Line 6 〜 11: %group の作成
前述のように %group のキーには、マス目の番号を割り当てます。マス目の番号は2桁の数字になっているので、 効き筋のマス目を探すのが簡単になります。下図の白のマス目は効き筋が縦横のみ、 グレーのマス目は対角線上のマス目が加わります。対角線上のマス目は、2桁の数字が同じ数字か、 2桁の数字を加えたものが 6 になるものです。効き筋のマス目を抽出したら、無名ハッシュに保存しておきます。

    
1112 131415
212223 2425
313233 3435
414243 4445
515253 5455
  
%group = (11 => [12, 13, 14, 15, 21, 31, 41, 51, 22, 33, 44, 55],
          12 => [11, 13, 14, 15, 22, 32, 42, 52],
          ..... );          

   Line 13 〜 20: %unassign の作成
最初に横の1列に A 〜 E を配置したので、%unassign は 21 〜 55 のマス目のみ作ります。 ハッシュの値には、置くことのできるアルファベットのリストと無名配列を無名配列に入れます。 最後の無名配列は、割り当てることのリストの退避用に使います。
    %unassign = (21 => ['B', 'C', 'D', 'E', []],
                 22 => ['C', 'D', 'E', []],
                 ...
                 33 => ['B', 'D', []],
                 ...
                 55 => ['B', 'C', 'D', []]);
   Line 22: search サブルーチンを呼び出す
search サブルーチンを呼び出す際に引数には、21 から 55 までのマス目の番号を渡します。search サブルーチンは、再帰呼び出し型のサブルーチンで 21 から順番に 55 までをアルファベットを割り当てていきます。

   Line 24 〜 49: search サブルーチンは本文で説明

   Line 51 〜 69: 配置パターンのチェックと表示
display サブルーチンは、配置パターンをチェックして成立すればコンソールに表示します。 チェックは、それぞれの縦列、横列、対角線に重複したアルファベットがないことを確認します。 このサブルーチンは難しいところがないので、見ていただければわかると思います。

プログラムの中心となる search サブルーチンについて、少し詳しく説明します。search サブルーチンの呼び出しは、未割り当てのすべてのマス目の番号を引数として渡します。 その際、未割り当てのマス目が簡単にわかるように、番号順に並べています。

22  search(sort keys %unassign);
23  
24  sub search {
25    my $pos = shift;
26    my @char = @{$unassign{$pos}}; pop @char;
27    foreach my $char (@char) {

呼び出されたサブルーチンは、引数の先頭を取り出し、 そのマス目番号に割り当てることのできるアルファベットを %unassign から取り出し @char に格納します。取り出されたアルファベットは、foreach ループを使ってすべて試すことになります。

最初に呼び出された search サブルーチンは、21 のマス目にアルファベットを割り当てます。 foreach ループの1回目は、21 のマス目に割り当てることのできる B, C, D, E のうちの B が置かれます (下図)。B が置かれたときに更新されるハッシュと、そのときに使われるコードは以下のようになります。

 1 23 45
1
2     
3      
4      
5      
    
%assign = (11 => 'A', 12 => 'B', 13 => 'C', 14 => 'D', 15 => 'E',
           21 => 'B');

%unassign = (22 => ['C', 'D', 'E', []],
             23 => ['A', 'D', 'E', ['1B']],
             24 => ['A', 'C', ['1B']],
             25 => ['A', 'C', 'D', ['1B']],
             31 => ['C', 'D', 'E', ['0B']],
             41 => ['C', 'D', 'E', ['0B']],
             51 => ['C', 'D', ['0B']],
             # 更新の対象: マス目の効き筋に属する未割り当てのマス目
            );
28      $assign{$pos} = $char;
29      my @update;
30      foreach my $key (grep { $_ > $pos } @{$group{$pos}}) {
31        my ($idx) = grep { $char eq $unassign{$key}->[$_] } 0 .. $#{$unassign{$key}} - 1;
32        if (defined $idx) {
33          push @update, $key;
34          push @{$unassign{$key}->[-1]}, join('', $idx, splice(@{$unassign{$key}}, $idx, 1));
35        }
36      }

21 のマス目に B を置いたら、%assign と %unassign を上記の太字で示した部分を更新します。%assign の更新は、キー 21 に B を割り当てるだけです。%unassign の更新は少し複雑で、B を置いた 21 のマス目ではなく、21 の効き筋に属する未割り当てのマス目が対象になります。それらのマス目では、B を置くことができなくなるので、 リストの中に B が含まれている場合は、B を削除して退避する処理をします。処理の対象となるマス目のうち、22 には最初から B が含まれていないので何もせず、その他のマス目では B を退避させます。 この一連の処理は、ハッシュの配列になっているのでわかりにくいと思います。 マス目 23 を例にして通常の配列で書き直すと、次と同じになります。

my @array = ('A', 'B', 'D', 'E', []);
my ($idx) = grep { 'B' eq $array[$_] } 0 .. $#array - 1;
push @{$array[-1]}, join('', $idx, splice(@array, $idx, 1));
print "(", join(', ', @array[0 .. $#array - 1]), ", [", @{$array[-1]}, "])\n";   # (A, D, E, [1B]) と表示
.....
my ($idx, $char) = split //, pop @{$array[-1]};
splice @array, $idx, 0, $char;
print "(", join(', ', @array[0 .. $#array - 1]), ", [", @{$array[-1]}, "])\n";   # (A, B, D, E, []) と表示

再帰呼び出し型のサブルーチンでは、元の状態に戻せることが重要です。そのため、単に B を退避させるだけでなく、元の位置を表す添字の情報も B と一緒にして退避させています。grep で取得した添字と B を join で繋いで、配列の末尾の無名配列に入れています。再帰呼び出しサブルーチンから戻ってきたら、 後半部分ののコードを実行して配列を復元できます。pop で無名配列から取り出して、4引数の splice を使って配列に挿入しています。

マス目にアルファベットを置いて %assign と %unassign を更新したら、その都度現在の盤面の状態を判断するコードがきます。現在の盤面の状態を判断するには、 未割り当てのマス目の配列の要素数を調べます。

37      my $chk = join '', map { scalar(@{$unassign{$_}}) } @_;
38      if ($chk =~ /^2+$/) {
39        display(@_);      
40      } elsif ($chk !~ /1/) {
41        search(@_);
42      }

%unassign の配列は、マス目に置けるアルファベットのリストと無名配列です。 そのため、要素数はマス目に置けるアルファベットの数よりも1多くなります。上のコードの Line 37 では、未割り当てのマス目の配列の要素数を数えて、join で連結して $chk に格納します。$chk には、次の3つの状態があり、それぞれの状態で処理を分岐します。

  1. 未割り当てのマス目の要素数がすべて2: if ($chk =~ /^2+$/)
    要素数が2の場合は、そのうちの1つが退避用の無名配列なのでアルファベットは1つになります。 すべての未割り当てのマス目の要素数が2になった場合は、display サブルーチンを呼び出し、 配置パターンが成立を確認して、成立していれば表示します。

  2. 未割り当てのマス目の要素数が3以上を含む: elsif ($chk !~ /1/)
    要素数が3以上のマス目がある場合は、次のマス目にアルファベットを割り当てるために search サブルーチンを再帰呼び出しをします。

  3. 未割り当てのマス目の要素数に1を含む:
    要素数が1になったマス目は、割り当てるためのアルファベットがなくなったことを意味します。 探索として失敗したので、この時点で探索を打ち切ります。特に何もする必要がないので、 処理するためのコードはなく、スルーするだけすみます。

search サブルーチンの残りのコードは、%assign と %unassign を元に戻すコードで、更新するコードの裏返しになっています。

プログラムを実行すると、次の8つの配置パターンがあることがわかります。 配置パターンを数字に変換すると、1つの配置パターンに 120 (5!) 通りの割り当てがあるので、総数で 960 通りとなります。

(1)
 1 23 45
1
2
3
4
5
    (2)
 1 23 45
1
2
3
4
5
    (3)
 1 23 45
1
2
3
4
5
    (4)
 1 23 45
1
2
3
4
5
 
(5)
 1 23 45
1
2
3
4
5
    (6)
 1 23 45
1
2
3
4
5
    (7)
 1 23 45
1
2
3
4
5
    (8)
 1 23 45
1
2
3
4
5

パズル問題の作成

ここから、パズル問題の作成の話題に移ります。パズルを出題するには、 最少でも4マス、4種類の数字をあらかじめ表示する必要があります。 すなわち、4マスではすべて違った数字を、5マス以上では4種類を満たせば同じ数字があってもよいことになります。 問題作成にあたって、盤の大きさも考慮して、表示マス数を4つまたは5つとしました。

パズル問題を作成するプログラムはいろいろと考えられますが、 この節のプログラムは前節で得られた8つの配置パターンを生かしたものです。25 のマス目の中から、4つまたは5つの表示するマス目を選択したら、配置パターンに当てはめてみます。 4つと5つとは違いますので、まず4つの場合を検討します。下図の3つの例を見てください。

解のない配置パターン 複数解の配置パターン  単一解の配置パターン
 1 23 45
1       
2      
3      
4      
5      
    
 1 23 45
1       
2      
3      
4       
5      
    
 1 23 45
1       
2      
3      
4      
5      

12, 14, 21, 41 のマス目に配置した「解のない配置パターン」では、(1) から (8) までのどのパターンでもアルファベットが重複してしまいます。「解のない配置パターン」という表現は不適切ですが、 4種類のアルファベットが揃っていないので、パズルの問題とはなり得ません。

12, 14, 21, 43 のマス目に配置した「複数解の配置パターン」では、(1), (2), (3), (5), (6), (7) が4種類のアルファベットが揃っていません。(4) はマス目順に B, D, C, E になっており、また (8) は B, D, C, A の配置になっています。このような場合は、数字に変換するときに任意の数字と変換できるので、 パズルの問題としては解が複数あることになり出題に適しません。

12, 14, 21, 42 のマス目に配置した「単一解の配置パターン」では、(7) のみが4つのアルファベット (B, D, E, A) が揃っています。このケースのように、 4種類のアルファベットが揃っていて、1つの配置パターンにのみ該当する場合は、 パズルの問題とすることができます。

5マス以上の配置では、4マス配置した場合と解の求め方が異なります。 4種類以上のアルファベットが必要なのは同じですが、同じアルファベットが複数あることが許されます。 次の図は、上の「解のない配置パターン」にもう1カ所追加したところです。8つの配置パターンのうち (4), (6), (7), (8) は、4種類のアルファベットが揃っていないためパズルの問題となり得ません。

 1 23 45
1       
2      
3      
4      
5      
    
(1) B, D, B, C, E   WSWSS
(2) B, D, B, E, C   WSWSS
(3) B, D, C, B, E   WSSWS
(4) B, D, C, D, B   -
(5) B, D, D, E, C   SWWSS
(6) B, D, D, C, B   -
(7) B, D, E, D, E   -
(8) B, D, E, B, E   -

(1) はマス目番号順に B, D, B, C, E の配置になっており、(2) は B, D, B, E, C になっています。アルファベットは前述したように入れ替え可能であるため、E と C を入れ替えると同じ配置になってしまいます。このような場合は、解が複数あることになり、 パズルの問題として適しません。

残る (3) と (5) は、別のパズルの問題として成立します。(3) の B, D, C, B, E のアルファベットを入れ替えても、(5) の B, D, D, E, C と同じにすることはできません。 重複しているアルファベットの位置が異なるためです。

5マス以上配置した場合は、単独のアルファベットを 'S'、 重複して使われているアルファベットを 'W' に置き換えてパターンにしてみると判別が容易になります (上の例の右端)。(1) と (2) はパターンが WSWSS で同じであるため、 解が複数あることになりパズルの問題としては適しません。(3) の WSSWS と (5) の SWWSS は、他に同じパターンがないためパズルの問題として成立します。

次が、4マスと5マス配置のパズルの問題を生成するプログラムです。5マス配置の問題は、 4マス配置でパズルの問題を生成のできなかったもののみを探索しています。

 1  use strict;
 2  my @pattern = qw(ABCDEBDECAECBADCADEBDEABC ABCDEBEDACDCBEAEDACBCAEBD ABCDECDEABEABCDBCDEADEABC
 3                   ABCDECEBADEADCBDCEBABDAEC ABCDEDEABCBCDEAEABCDCDEAB ABCDEDEBCAECDABCAEBDBDAEC
 4                   ABCDEECABDBEDCADABECCDEAB ABCDEECDABDEBCABDAECCAEBD);
 5  my @pos = (11 .. 15, 21 .. 25, 31 .. 35, 41 .. 45, 51 .. 55);
 6  my (%assign, @work, %result);
 7  
 8  search(0 .. 2);
 9  
10  sub search {
11    foreach my $i (@_) {
12      my $chk = join ':', sort map { scalar reverse $_ } @pos[@work, $i];                           # 対角線 (11 〜 55)
13      next if exists $assign{$chk};
14      $chk = join ':', sort map { substr($_,0,1) . (6 - substr($_,1,1)) } @pos[@work, $i];          # 縦線
15      next if exists $assign{$chk};
16      $chk = join ':', sort map { (6 - substr($_,0,1)) . substr($_,1,1) } @pos[@work, $i];          # 横線
17      next if exists $assign{$chk};
18      $chk = join ':', sort map { (6 - substr($_,1,1)) . substr($_,0,1) } @pos[@work, $i];          # 左 90 度回転
19      next if exists $assign{$chk};
20      $chk = join ':', sort map { substr($_,1,1) . (6 - substr($_,0,1)) } @pos[@work, $i];          # 右 90 度回転
21      next if exists $assign{$chk};
22      $chk = join ':', sort map { (6 - substr($_,0,1)) . (6 - substr($_,1,1)) } @pos[@work, $i];    # 180 度回転
23      next if exists $assign{$chk};
24      $chk = join ':', sort map { (6 - substr($_,1,1)) . (6 - substr($_,0,1)) } @pos[@work, $i];    # 対角線 (15 〜 51)
25      next if exists $assign{$chk};
26  
27      push @work, $i;
28      $assign{join(':', @pos[@work])} = 1;
29      if (@work == 1) {
30        search($work[0] == 0 ? 1 .. 24 : $work[0] == 1 ? (2, 3, 5 .. 19, 21 .. 23)
31                                                       : (6 .. 8, 10 .. 14, 16 .. 18, 22));
32      } elsif (@work < 4) {
33        search(grep { $_ > $i } @_);
34      } elsif (@work >= 4) {
35        my %analyze;
36        foreach my $patt (@pattern) {
37          my (%char, @char);
38          foreach my $i (@work) {
39            my $char = substr $patt, $i, 1;
40            $char{$char}++; push @char, $char;
41          }
42          if (keys %char >= 4) {
43            my $key = join '', map { $char{$_} == 1 ? 's' : 'w' } @char;
44            push @{$analyze{$key}}, $patt;
45          }
46        }
47        my @ques = grep { @{$analyze{$_}} == 1 } keys %analyze;
48        if (@ques) {
49          foreach my $key (@ques) {
50            push @{$result{scalar(@work)}}, join('|', join(':', @work), $analyze{$key}->[0]);
51          }
52        } else {
53          search(grep { $_ > $i } @_) if @work == 4;
54        }
55      }
56      pop @work;
57    }
58  }
59  
60  print join("\n", @{$result{$_}}), "\n" foreach sort keys %result;
   Line 2 〜 6: 必要な変数の宣言と初期化
• @pattern -- 前節で作成した配置パターンを格納
• @pos -- マス目の番号。@pattern の各要素の文字位置と @pos の添字は対応関係にある
• %assign -- 重複配置チェック用
• @work -- 配置済みの位置を格納する配列
• @result -- 生成したパズル問題を格納

   Line 8, 10 〜 58: search サブルーチン (本文で説明)

   Line 60: 生成したパズル問題を表示

4マスあるいは5マスを配置する位置は、再帰型の search サブルーチンで生成します。 配置する位置が少ないので、サブルーチンの呼び出しは深くなりません。search サブルーチンの引数は、マス目番号を格納した @pos の添字を渡しています。最初の search サブルーチン呼び出し (Line: 8) の引数は 0, 1, 2 なので、次の図の3ヶ所のうちの1つが最初に配置するマス目になります。

 1 23 45
1       
2      
3      
4      
5      

最初に配置するのマス目が3ヶ所だけなのは、重複解を避けるためです。 3ヶ所以外の外周部のマス目 (14, 15, 21, 25, 31, 35, 41, 45, 51, 52, 53, 54, 55) は、対称関係 (対角線、縦、横) であったり、回転 (90, 180, 270 度) したときに同じ位置と評価できるので除外しています。 また、22 の位置は、外周部を除くマス目 (22, 23, 24, 32, 33, 34, 42, 43, 44) だけでは解がないため除外しています。2番目の配置マス目の位置は、1番目の位置によって異なります (Line: 29, 30, 31)。

29      if (@work == 1) {
30        search($work[0] == 0 ? 1 .. 24 : $work[0] == 1 ? (2, 3, 5 .. 19, 21 .. 23)
31                                                       : (6 .. 8, 10 .. 14, 16 .. 18, 22));

最初に 11 に配置したときは残り全部のマス目を、12 に配置したときは 13 〜 55 から角のマス目 (15, 51, 55) を除外したマス目を、13 に配置したときは 14 〜 55 から 14, 15, 21, 25, 41, 45, 51, 52, 54, 55 のマス目を除外して search の引数として呼び出します。 なお、3番目以降のマス目は、受け取った引数から現在の位置までを除外して search の引数にして配置します。

search サブルーチンの冒頭部は、重複配置の除外のコードになっています (Line: 12 〜 25)。すでに生成した盤面の配置は %assign に登録しておいて、 新たに生成した盤面が対照的な配置になっていないかチェックします。チェックする項目としては、対角線 (11〜55, 15〜51)、縦線、横線、回転 (左右の 90 度、180 度) があります。 次の図は、最初に 11 に配置し、2番目に 51 に配置しようとしているところです。

 1 23 45
1       
2      
3      
4      
5      

すでに %assign には、11→15 の配置が登録されています。11→51 の配置は、 対角線対称であり、右 90 度回転でもあるため、重複配置と評価され除外されます。 重複配置の除外をしなければ、4マスと5マスの配置数は合わせて 19,350 になります。内訳は、4マスが 3,384、5マスが 15,966 です。下の表は、除外項目を個別に数えたものと、累積で数えたものです。 上の図のように、複数の項目に該当するものがあるため、累積での除外数は少なくなります。結果として、10,916 の配置が除外され 8,434 が残ることになります。

 対角線(11〜55)縦線横線左 90 度 右 90 度180 度対角線(15〜51)
個別6,9021,8461,6051,2152,105 1,8671,627
累積6,9021,242(8,144)489(8,623)729(9,352) 587(9,929)480(10,409)507(10,916)

search サブルーチンを再帰呼び出して、配列 @work に配置位置を収集していきます。@work の要素数が4つになったらパズルの問題が生成されているか試します。パズルの問題が生成されていない場合は、 さらにもう1つの配置位置を追加するため、search サブルーチンを呼び出します。 配置数が4マスまたは5マスになったときは、%analyze ハッシュを使ってチェックを行います。

34      } elsif (@work >= 4) {
35        my %analyze;
36        foreach my $patt (@pattern) {
37          my (%char, @char);
38          foreach my $i (@work) {
39            my $char = substr $patt, $i, 1;
40            $char{$char}++; push @char, $char;
41          }
42          if (keys %char >= 4) {
43            my $key = join '', map { $char{$_} == 1 ? 's' : 'w' } @char;
44            push @{$analyze{$key}}, $patt;
45          }
46        }
47        my @ques = grep { @{$analyze{$_}} == 1 } keys %analyze;
48        if (@ques) {
49          foreach my $key (@ques) {
50            push @{$result{scalar(@work)}}, join('|', join(':', @work), $analyze{$key}->[0]);
51          }

ここでの処理は、@pattern に格納されている各配置パターンから @work で示す位置のアルファベットを抜き出し、パターン化して %analyze に登録していきます。 抜き出したアルファベットは順番に @char に格納し、アルファベットの出現個数を %char で数えます。アルファベットの個数が4個未満の場合は %analyze への登録をスキップし、4個以上の場合のみ %analyze に登録します。%analyze のキーの生成は、@char のアルファベットを出現回数に応じて "s" と "w" に変換して繋げます。したがって、4マスでは "ssss" となり、5マスでは "sssss" あるいは "w" が2つ混在したものになります。値には無名配列を割り当て、 その中に各配置パターンを格納します。

%analyze を作成し終わったら、パズルの問題となる (無名配列の要素数が1つ) ものを抽出します。抽出したパズルの問題は、パズル出題の際に表示するマス目の位置となる @work を ":" で繋げて、さらに "|" で配置パターンを繋げたものを、4マスと5マスを分けて %result ハッシュに保管します。保管しておいた %result は、プログラムの最後に出力します。

0:1:5:8|ABCDEDEBCAECDABCAEBDBDAEC
0:1:5:14|ABCDECDEABEABCDBCDEADEABC
0:1:7:8|ABCDEBDECAECBADCADEBDEABC
0:1:7:9|ABCDEBEDACDCBEAEDACBCAEBD
0:1:7:16|ABCDECDEABEABCDBCDEADEABC
・・・・・
2:10:12:13:18|ABCDEDEBCAECDABCAEBDBDAEC
2:10:13:17:18|ABCDEBDECAECBADCADEBDEABC
2:10:13:17:18|ABCDEECDABDEBCABDAECCAEBD
2:10:13:17:18|ABCDEDEBCAECDABCAEBDBDAEC
2:10:13:17:18|ABCDEBEDACDCBEAEDACBCAEBD

プログラムの実行を終了すると、4マスでは 246 の問題が、5マスでは 8,217 の問題が生成されていました。上のデータフォーマットは、実際のパズルで用いるのと同じで、 後はアルファベットを数字に変換することだけです。アルファベットから数字への変換では、 ランダムに割り付けることができます。

これでパズル問題が作成できたので、後はランダムに選んだものを出題するだけと思ったのですが、 そうはいきませんでした。というのは、問題の質に大きな違いがあるのです。 まったく考えることが必要のない易しいものから、かなり難しいものまでさまざまです。 プログラムでランク分けできたらいいのですが、この種のプログラムは難しく手も足もでません。 というわけで、自分で問題を解きながらパズル問題を選択する作業をしました。 時間のかかる作業で問題数も多いため、いまだに終わっていません。近いうちに終わればいいのですが...。

(2011/06/01)

TopPage