数独の例題生成

ネットワークシステム研究室
指導教員:坂本 直志
07EC099 中嶋 勇気

目次

1. はじめに
2. 研究準備
2.1. 数独の基本ルール
2.2. 基本的な問題の解き方
2.3. 問題の解き方(応用編)
3. 問題の生成
3.1. プログラムによる解法
3.2. 問題生成
3.3. 難問作成
3.4. 抽出方法
4. 出力結果の評価
5. まとめ
6. 参考文献
7. ソース

1.はじめに

数独(別名:ナンバープレイス、ナンプレ、etc…)とはペンシルパズルと呼ばれるパズルゲームの一種である。この数独はパズル制作会社「ニコリ」が商標登録したもので、海外でもsudokuなどと呼ばれ、広く親しまれている。もともとの由来は「数字は独身に限る」。というところから来ている。

数独の原型は18世紀にスイスの数学者レオンハルト・オイラーが考案した、ラテン方陣あるいはオイラー方陣と呼ばれるものである。これに新たな制限を付け加えて生まれたのが数独である。日本で初めて紹介されたのは1980年頃だが、近年再燃し、一躍大ブームとなったのでこのパズルを知っている、もしくは解いたことがあるという方も多いだろう。

下図は基本的な数独の問題例となっている。この空欄を全て埋めることができればゲームクリアとなる。

図1

図1 数独問題例

詳しくは次項で説明するが、行、列、3×3のブロックに注目したときそれぞれ1〜9の数字が被らないように埋められていればいい。

また、問題を解く上では気にすることはないが、穴に埋めることができる数字は1つでなくてはならず、そうでないと解が複数存在することになってしまい、問題としては不適切となる。本研究では問題の生成が目標となるため、この点はまず念頭に置いておかねばならないだろう。

本研究ではこの数独の問題を自動生成するプログラムを開発することを目標とし、研究開発を行った。

2.研究準備

2.1 数独の基本ルール

数独の基本は、3×3のブロックに区切られた9×9の正方形の枠内にそれぞれ1〜9までの数字を空欄に埋めていく。

下図左は基本的な数独問題例である。図の左に対し、右のように全ての空欄を条件に沿うよう埋めることができればゲームクリアとなる。条件は行、列、3×3のブロックに注目したときそれぞれ1〜9の数字が被らないように埋めていけばいい。見事全ての空欄を埋めることが出来ればゲームクリアとなる。

図2

図2 数独問題

2.2 基本的な問題の解き方

基本的な問題の解き方について説明する。

初めに特定の数字に着目して解く方法を説明する。下図の左上AXの部分に注目してほしい。

図3

図3 基本的な問題の解き方(数字)

図で左上の3×3の四角AXの中で1が入る場所を考える。左上の四角のなかにはまだ1が使われていないので9マスの中のどこかに1が入るわけだが、横を見るとBX2行目とBX3行目に1が使われている。そのためAXの2,3行目には1を入れる事ができない。同様に下を見るとAYの2列目、AZの3列目にも1が使われているため、1が入れられるのは左上のマスのみとなる。従ってAXは左上のマス以外には1を入れることができない。

次に特定の場所に注目して解いてみる。下図のAX1行、1列目に入る数字を考える。すると使われていない数字1,7,8,9のいずれかが入ることがわかる。しかし、右のCXを見てみると、1が既に使われているため、CX1行目の右の3つの空欄には1を入れることができない。従ってAXの左上の空欄には1が入る。

図4

図4 基本的な問題の解き方(場所)

2.3 問題の解き方(応用編)

先に説明した2つの手法を用いることで初級レベルの問題ならば十分解くことができる。しかし、中級、上級レベルの難しい問題となると、この2つだけでは少々心許無い。高難易度問題を解くための手法は数多く存在する。ここでは1例を紹介する。

図5

図5 問題の解き方(応用編)

図でAXの左下のマスに入る候補はXの下の行から3、5、6以外、Aの右の列から7、8、9以外なので1、2、4のいずれかとなる。しかし、Aの上の行に注目すると、3〜9はすでに使われているので、AXの上の行には必ず1か2が入る。そのためAXの左下のマスには1と2は使用することができない。よってのこりの4が入ることがわかる。

このように解を求める際の推理のしかたとしては様々な方法があり、これらを組み合わせて各マスの数字を決定していく。

この組み合わせの数が多ければ高度な推理であり、少なければ容易となる。

数独問題の難易度はある程度の経験者の平均回答時間など、あくまでも人間の主観によるものであるが、解く上で必要となる推理の深さや、埋めなければならないマスの個数などにより決められると考えられる。

3.問題の生成

3.1 プログラムによる解法

前章により、人間が数独を解く場合、深い推理が必要となることがあり、パズルとして楽しめるものであることを示した。しかし、コンピュータによる解法はこれとは異なる。

ここで、check関数(ソース参照)を説明する。

このプログラムは左上から順に数をいれていくだけのバックトラックのアルゴリズムを使用している。数をいれてはcanBePlaced関数でその数を入れたことが正当かどうかをチェックする。この関数は単純に行、列、3×3のマスにダブりがないかをチェックするだけの関数である。

通常のパズルでは単純な全探索を行うと組み合わせ論的爆発を伴うが、この数独の場合、ゲームの制約が効率的に働いてゲーム木が先細りになるようで、このような単純な全探索で十分効率的解を得ることができる。

3.2 問題生成

基本方針としては全てのマスが埋まった完成形を作成し、そこに穴を開けていくことで問題を作成する事を考えた。任意に選択したマスに仮に穴を開け、そこに入る数字が1つのみならば問題として成立している。そしてそれを繰り返すことで問題を作成した。

この穴を開けるプログラムはmaker1関数(ソース参照)となる。これは完全にランダム順に穴を開けていくプログラムで、まず、ランダムに指定した場所を仮に0で置き換える。そこに1〜9の数字を入れていき、canBePlaced関数を用いてそこに入れることができるかを調べ、その結果そこに入る数字が元々入っていた数字のみならばそこは0で置き換えたままで、そうでない場合は元の数字に戻す。これを全ての穴に対し試みる。

その他の関数は次のことを行う。

printResult(ソース参照):
作成した問題をコマンドプロンプト上で表示するための関数。ただし、ソースではhtml出力を行う設定のため使用していない。
count0(ソース参照):
配列内の0の数を数える関数で、作成した問題の空欄の数を調べるのに使用している。for文で配列内の要素を確認し、0であった場合にはカウントをプラスすることで0の数、すなわち空欄の数を数えている。
cloneBoard(ソース参照):
配列のコピーを行う関数。全く同じ要素を持つ配列を複製するのに使用する。コピーしたい元の配列とコピー先の配列を変数として指定することで、変数をコピーして返す。
rand9(ソース参照):
1〜9までの数字が順に入った配列を作成し、その中身をシャッフルすることで、1〜9までの数字がランダム順に格納された配列を作成する関数。シャッフルは1番目とランダム番目をスワップ、2番目とランダム番目をスワップ…という行為を9番目まで行うことでシャッフルしている。また、nは乱数のシードを変えるための値。
rand81(ソース参照):
rand9とほぼ同一のもので、こちらは0〜80までの数字がランダム順に格納された配列を作成する。これはmaker1でランダム順に穴を空けていくために使用している。
main(ソース参照):
プログラムの出力や問題を作成する反復回数などを指定する関数。ソースではhtml出力を行うようになっているため、コマンドプロンプト上で表示したい場合には一部コメントになっている部分を書き換える必要がある。また、maker1とmaker2のどちらを使用するかもこの関数で切り替える。

3.3 難問作成

問題を作成するうえで、当然考慮しなくてはならないのが、問題の難易度である。しかし、ここでネックとなるのが、数独の難易度は必ずしも空欄の数ではないという事である。もちろん極端に穴の数が違う場合などには難易度に差がでる傾向はあるが、少数の差では必ずしも穴が多いほうが難しいとは限らない。これは数独をはじめとする一般的なパズルゲームの難易度は、その問題を解くために用いる手法の複雑さによるためである。

手作業で問題を作成する場合には、比較的容易に問題を難しくすることもできるのだが、今回はプログラムで問題を自動生成するため、プログラムでも簡単に問題を難しくすることができる方法がないか模索した。

3.4 抽出方法

作成したプログラムに難易度を与えるための手段としてはおおまかに以下の3つの手法が考えられる

  1. 完成形を生成する際条件を追加する。
  2. 穴を開ける順序や、条件を指定する。
  3. 問題作成後に数字の位置をずらすなどの操作を行う。

今回はこの中の(2)穴を開ける順序により、難易度に変化を与えることができないかを試みた。

これを行うのがmaker2関数(ソース参照)となる。これは穴を開ける順所を完全に指定し、真ん中→上→左→右→下→四隅の順に穴を開けるようにしたもので、81個の要素をもつ配列を全て手入力で入力することで順番の指定を行った。その他の動作はランダム順と全く同じで、canBePlacedで穴を空けられるなら空けるということを全ての穴に対し行う。

その結果、次項で紹介する通り、一定の難易度の付与することができた。

4.出力結果の評価

作成したプログラムにより、以下の2つの問題を生成し、難易度の評価を行った。

なお、これらの生成時間は

CPU:Intel Core i7 980(3.33Ghz)

OS: Windows7 Professional 64bit

使用ソフト:Visual Studio 2010 Professional

環境において、ランダム生成が410ms、順番指定が400msほどであった。

図6

図6 作成した問題

図6の左はmaker1を使用して作成した問題で、完全にランダム順で穴を開けていったものとなる。それに対し右は、maker2を使用して作成したもので、穴を開ける順所を指定し、中央→上→左→右→下→四隅の順に穴を開けるようにした。それをランダム、順番指定共に各1000問ずつ問題を生成し、その中で最も穴の数が多かったものをピックアップしたのが上図となる。

この問題を実際に研究室の数名の有志の方に解いて頂いたところ、右の問題のほうが明らかに難易度が高いと感じる人のほうが多く、順序を指定することで問題にある程度の難易度を持たせることができることがわかった。

ところが、この問題を解くために用いる手法を調べてみたところ、実は両者とも前述の基本的な解き方二つだけで全ての穴を埋める事ができた。一方、最初に埋まっている数字だけで埋められる箇所を数えたところ、ランダムが8個あるのに対し、順番指定のほうは4個だった。更に、この穴を埋めた状態から埋められる箇所を数える…という動作を全てのマスが埋まるまで行ったところ、ランダムが8→7→10→8→7→5→2→2、順番指定が4→8→11→10→10→3→4→2でクリアまでに必要な回数は共に8回となり、両者にあまり差異は発生しなかった。

以上のことから両者に難易度の差が感じられたのは、初期状態で埋められる箇所が少なかったことが原因の一つとしては考えられるが、そのほかにも難易度を難しくしている要因があるように思われる。これは今後の課題としたい。

5.まとめ

本研究では数独問題を自動で簡単に生成するプログラムを開発することを目標として研究を行った。結果、簡単な問題生成プログラムの開発並びに、生成の際に難易度を付与することに成功した。

今後の展望としては、他に難易度を変化させる手法が無いか、また、作成した問題の難易度を客観的に評価する方法がないかを模索していきたい。

6.参考文献

  1. ウィキペディア https://ja.wikipedia.org/wiki/数独
  2. 松岡 毅  OpenMPによる数独パズルの並列化2007 立命館大学 卒業論文
  3. スパコンで力任せに数独の難しい問題を作る(った)つもりが簡単な問題だった件 http://apollon.issp.u-tokyo.ac.jp/~watanabe/sample/sudoku/index_j.html
  4. 智慧 http://www.pori2.net/index.html
  5. Newt Net http://newtgecko.blog.fc2.com/

7. ソース