同じ形のポリオミノを正方形の枠に入れるパズルを制作した。
このパズルは枠に対して縦横には入らず、ピースが斜めにしか入らない。
過去にも、斜めにしか入らないパズルとしてSixes And Sevens - designed by Goh Pit Khiam *1などが発表されている。
このパズルについて解説しよう。
※日本パズル協会主催の第5回パズルオーディション*2に応募したが結果は落選だった。(答えは最後に)
例題
斜めに入るとはどういうことかを説明するため、以下の例題を見てほしい。8 x 8 の正方形にサイズ10のポリオミノ(デコミノ)を4つ入れる。
答えは以下である。\(1:3:\sqrt{10}\)の三角形の傾きで、一辺が\(7.906 \approx 25 / \sqrt{10}\) の正方形に接するように入る。枠と平行に縦横には入らない。
例題のパズルの解の一意性について、少なくともポリオミノを構成する正方形が格子に沿って入るパターンに関しては上記の解しかないことをプログラムにより確認している。
まず正方形の格子の取り方について解説しよう。
格子の取り方
例題の8 x 8の正方形を傾けたときに有効な格子の取り方は27パターン存在する*3 。これは正方形に入るようなフリーポリオミノの中で、他のフリーポリオミノに真に含まれない(極大となっているもの)の数である。フリーポリオミノとは回転鏡像を同一視するポリオミノである。ここからは「有効な格子」と「極大のポリオミノ」をほぼ同じ意味で使う。
そのような極大のポリオミノの数が有限であることはすぐに分かるが、列挙するにはどうすればよいか?
実は正方形を格子に対して傾けて、平行移動して見つける方法が知られている*4。
まず傾きについてであるが、tanが有理数となるケースを考えれば十分である。以下に証明する。
逆に、ポリオミノが与えられたときに、それを含む最小の正方形を見つけることを考える。最小の正方形にポリオミノがちょうどはまるときに、ポリオミノのいくつかの頂点が正方形の周上に存在することになる。このとき正方形のある辺(もしくはその延長線)にポリオミノの頂点(もしくは格子点)が複数存在することを示す。以下、正方形の周上に存在する頂点の個数で場合分けをする。
a) 5個以上の場合
正方形の辺が4つであるので、鳩ノ巣論法より、ある辺に2つ以上頂点が存在する。
b) 0個の場合
正方形を少し縮小でき最小性に反する。
c) 1個の場合
図のように、頂点が存在する辺の垂直外向きに少し正方形を移動すれば、正方形の周上の頂点は0個になる。よって最小性に反する。
d) 2個の場合
隣り合う辺に頂点が乗る場合、それぞれの辺で1個の場合と同じように移動すれば、0個に減る。
向かい合う辺に頂点が乗る場合、図のように一方の頂点で固定し、正方形を少し回転すると1個に減る。(回転の向きは頂点の場所による)
どちらも最小性に反する。
e) 3個の場合
隣り合う3つの辺に1つずつ頂点が存在するとしてよい。3つのうちの真ん中の辺に対し垂直外向きに正方形を少し移動すると、2個に減らせる。よって最小性に反する。
f) 4個の場合
正方形の4辺に1つずつ頂点が存在するとしてよい。図のように\(A,B,C,D\)と順番に名前を付ける。さら直線\(AC\)と\(BD\)の交点を\(P\)とし、\(\angle APD = \theta\)とする。ここで図のように\(DB\)に\(A\)からおろした垂線が、\(C\)が乗る正方形の辺(もしくはその延長線)と交わる点を\(C'\)とする。この時\(C'\)は格子点となる。\(\theta\)が90°でない場合は\(C\neq C'\)となり、\(C, C'\)の2つの格子点が同じ辺(もしくはその延長線)に乗る。
\(\theta\)が90°の場合、\(P\)を中心に少し回転させると、\(A,B,C,D\)はすべて正方形内部に移動し、最小性に反する。(回転の向きは\(AC, BD\)の傾きによる)
以上より、最小の正方形の傾きが有理数であることが証明された。(細かい証明は省いてある)
さらに先述の場合分け f) 4個の場合では、格子点\(C, C'\)間の距離は正方形の一辺の長さの2倍を超えない、さらに言うと2倍未満であることがわかる。他の場合も正方形のある一辺に2つのポリオミノの頂点が乗る。よって、辺、そしてその延長線にのる格子点間の距離は正方形の一辺の長さの2倍未満である。
以上の結果より、有効な格子を調べるための正方形の傾きについて、正方形の一辺の長さ\(n\)に従い有限の範囲で列挙できる。具体的には、\(a^2 + b^2 < 4n^2\)を満たす互いに素な正整数\(a\geq b\)について、\((0,0), (a,b)\)を通る傾きの辺を持つ場合を考えればよい。
次に平行移動について考える。図の実線のようにまず正方形の一辺が乗る線(横線と呼ぶ)を決める。これは格子点を通るとしてよく、先ほどのように\((0,0), (a,b)\)を通ると仮定する。さらにその一辺に隣り合う一辺が乗る線(縦線)を決める。縦線も格子点を通るものとしてよい。この縦線の取り方は\(a^2 + b^2\) 通りある。図では\(1:2:\sqrt{5}\)の傾きの横線に対し、\(1/\sqrt{5}\)の間隔で並んだ\(1^2 + 2^2=5\) 通りの縦線の取り方を点線で示している。横線と縦線が決まればあとは一辺の大きさを指定すれば正方形が確定する。
以上より、有効な格子の候補となる正方形の傾きと位置を有限個に絞ることができた。あとは内部に含まれる格子のセル(小さい正方形)を判定すれば正方形に含まれるポリオミノが得られる。それにはセルの4頂点すべてが正方形の内部に含まれるかを判定すればよい。
※判定は点と直線の距離の式の比較により行えるが、プログラムで行う場合の留意点として、浮動小数点数での比較を行うと、ちょうどぴったり重なる場合などに判定を間違うケースが想定される。式を二乗してうまく変形すると、整数同士の判定に帰着し回避できる。
なお、生成されるポリオミノはすべてが極大ではないため、他のポリオミノに含まれるものは除く必要がある。(今回私は愚直に1セルずつ、回転鏡像を含めて包含関係を確認したが、もう少しうまいやり方がある気がする)
有効な格子の列挙は、正方形の一辺の長さnがあたえられたときにスナップショット的に行うことも可能であるが、nが実数の範囲として与えられていても、その進行を列挙できる。傾きを決めて縦線を取った後に、縦線上の一辺と向かい合う一辺(の乗る線)を決める際にも格子点を通るとしてよいので、縦線の間隔が一定だったことを思い出すと、nが与えられなくても調査が必要な正方形の大きさも列挙可能であることがわかる。
上記のグラフの一辺の長さ5までのデータは以下である。
ソルバー
パズルを自動で解くにはソルバーが必要になる。そのアルゴリズムを紹介しよう。
ソルバーは盤面(先ほど列挙した有効な格子)とポリオミノのピースが与えられたときに、最大何個入るのか、そしてその入り方を見つける。盤面もまた、前述の通りポリオミノとなる。
パズルを解く頻出アルゴリズムとして「バックトラッキング」という枝刈付き深さ優先探索がある。数独や8クイーン問題を解く際にも使われるが、今回も使える。流れとしては、とりあえずピースを置けるだけ置いて、行き詰ったら、盤面上で最後に置かれた1ピースを除いてやり直す、という作業の繰り返しである。
さらに問題がExact Coverに帰着できるときに、双方向リストを使ってバックトラッキングを効率よく実装できる。これはDancing Link *5として知られており、今回の問題もこちらを応用できる。実際、Knuthの論文でも同一ポリオミノでのタイリングが取り上げられている。
Exact Coverとは集合\(X\)とその部分集合の属\(S\)が与えられたときに、\(S\)のいくつかの元を取ってきて、\(X\)の元がちょうど重複なく1回ずつどれかの元に含まれるという風にできるか、という問題である。ポリオミノのタイリングの場合は、盤面のセル1つ1つが\(X\)の要素となり、\(S\)はポリオミノ1ピースの置き方(セルの占め方)に相当する。ピースの置き方は回転鏡像、平行移動により列挙できる。
今回のパズルの留意点として、盤面がすべてポリオミノにより埋め尽くされるわけではなく、空白も存在する可能性がある。つまり厳密にはExact Coverではない。
しかしDancing Linkの実装を、空白も選択肢として取りうるように少し変えれば対応できる。実装としては、空白を選択する場合はその列のみカバーすればよい。(カバーとはDancing Linkの用語である。詳しい説明は原論文へ)
また、盤面の面積が\(A\)の時に、サイズnのポリオミノを\(i\)個入れると、盤面に空白は\(A-ni\)個存在することになるが、それより多く空白を作ってしまうとポリオミノが\(i\)個入ることはない。空白を選択した回数をカウントしておけば、空白の数の上限を超えた場合は枝刈りでき、探索回数(再帰呼出数)を減らせる。
Dancing LinkのColumnの選び方は原論文でもいくつか提唱されているが、本件では、選択肢の数が最小になるように選ぶことで、かなり再帰呼出数を減らせることを確認している。
ソルバーは以上である。盤面については前述の方法で列挙できるので、総当たりでソルバーを走らせれば解が見つかる。
また本件の派生として思いついたが、Exact Coverをもう少し拡張してもDancing Linkを応用して解けるだろう。Exact Coverのステートメントの「ちょうど1回ずつ含まれる」の部分に範囲を持たせて、「要素\(k\)が\(a_k\) ~ \(b_k\)個含まれる」と変え、さらに判定問題ではなく、全体の個数のMaxを求めるという問題に変える。今回のソルバーは\(\forall{k}: a_k=0, b_k=1\)の場合に相当する。
\(a_k, b_k\)が一般の場合は、要素\(k\)のColumn Headerに\(a_k, b_k\)を持たせて、Rowを選ぶたびに、カバーの代わりにRowで選ばれた要素\(j\)の\(a_j, b_j\)を減らしていく(戻すときは増やす)。\(b_j=0\)となったときにはColumn \(j\)のカバーを行う。
パズルの作り方
さきほどのソルバーはパズルを作る際にも利用できる。
微妙な違いとして、パズルを解く際は、何個ポリオミノが入るかあらかじめ分かっているが、作る際は分からない。それについては、何個入るかの目標値を0から初めて、解が見つかれば適宜インクリメントしていくことで、空白による枝狩りを利用しながらが、最大で入る個数を効率よく探すことができる。
あとはピースとなるポリオミノが列挙できれば、総当たりでパズルを探すことができる。ポリオミノの列挙に関しては多くの先行研究があるのでそちらに譲る*6 *7。
斜めにしか入らないものに限らず、解が一意になるパズルや、対称なパズル、解くのが難しいパズルなど、さまざま作ることができる。パズルの難易度はDancing Linkの再帰呼出数を目安とできるだろう。
パズルの答え
ご自由にお使いください。
*2:第5回パズルオーディション 作品募集 – 一般社団法人 日本パズル
*4:SamN.docx - Google ドキュメント ロシア語を翻訳して理解した
*5:Donald E. Knuth. "Dancing Links". https://arxiv.org/pdf/cs/0011047
*6:Redelmeier, D. Hugh (1981). "Counting polyominoes: yet another attack". Discrete Mathematics. 36 (2): 191–203