フィルマットというパズルがあります。ありますが皆さんご存知です?
まいなすよんは永遠の若手ですので、当然ちゃ当然なのですが、私もニコリ本誌で現役の頃を知らないくらいです。とはいうもののニコリのページの紹介がありますので、ご興味の向きはそちらをどうぞ。
そして例題はこちら。小さいですが。
今回は、フィルマットをSugar制約ソルバーで解くことを考えたいと思います。
以前、ぼんさんやカーブデータを解いた時は、盤面に番号を振ってそれを変数として探す、という手法をとりましたが、今回はトラディッショナル(?)に盤面の各マスに変数を割り振ります。
以下のような感じ。左側が変数、右側が最終的に答えとなる数字です。
で、天下り的ですが、補助的な変数を導入します。左側が変数、右側が最終的に答えとなる数字なのは同じですが、数字5と6が設定されています。各数字の役割は以下のようになります。
数字3→面積3のブロックに含まれ、その両端になる。
数字4→面積4のブロックに含まれ、その両端になる。
数字5→面積3のブロックに含まれ、その両端にならない。
数字6→面積4のブロックに含まれ、その両端にならない。
本質的には要らないっちゃ要らないんですが、たぶん変数b_xx_yyを導入したほうが見やすいと思うのですよね。Sugar慣れした方には、もうこれで伝わるものは伝わったかと思うのですが、説明を続けます。
まずは変数の定義。
( int a_00_00 1 4 ) ; 中略 ( int a_03_03 1 4 ) ( int b_00_00 1 6 ) ; 中略 ( int b_03_03 1 6 )
次に、この2種類の変数の基本的な関係を記述します。x_00_00の位置の関係のみ列挙します。
( => ( = b_00_00 1 ) ( = a_00_00 1 ) ) ( => ( = b_00_00 2 ) ( = a_00_00 2 ) ) ( => ( = b_00_00 3 ) ( = a_00_00 3 ) ) ( => ( = b_00_00 4 ) ( = a_00_00 4 ) ) ( => ( = b_00_00 5 ) ( = a_00_00 3 ) ) ( => ( = b_00_00 6 ) ( = a_00_00 4 ) )
表出の記述。例題では4つだけなので全部書いてみます。
( = a_00_00 1 ) ( = a_00_01 3 ) ( = a_02_02 2 ) ( = a_00_03 3 )
各マスについて、そのマスに1から4の数字が入った場合について場合分けして、そのマスの上下左右のマスが満たすべき条件を記述します。さらに、3が入る場合と4が入る場合は、それぞれについて端かそうでないかで場合分けします。説明をすっ飛ばして感覚的に書くと、そのマスに1から6の数字が入った場合について場合分けします。
辺や隅の処理は面倒なので、以下では例としてx_01_01のマスについて記述します。辺や隅は条件をとりうる条件を絞るだけですので、適宜読み替えてください。
まずそのマスに1が入る場合。これは簡単です。隣接するマスには1が入らないということをSugarに教えます。
( => ( = b_01_01 1 ) ( and ( != a_01_00 1 ) ( != a_01_02 1 ) ( != a_00_01 1 ) ( != a_02_01 1 ) ) )
次にそのマスに2が入る場合。これも簡単。隣接するマスには2がただひとつだけ入るということをSugarに教えます。
( => ( = b_01_01 2 ) ( or ( and ( = a_01_00 2 ) ( != a_01_02 2 ) ( != a_00_01 2 ) ( != a_02_01 2 ) ) ( and ( = a_01_02 2 ) ( != a_01_00 2 ) ( != a_00_01 2 ) ( != a_02_01 2 ) ) ( and ( = a_00_01 2 ) ( != a_01_00 2 ) ( != a_01_02 2 ) ( != a_02_01 2 ) ) ( and ( = a_02_01 2 ) ( != a_01_00 2 ) ( != a_01_02 2 ) ( != a_00_01 2 ) ) ) )
次にそのマスに3が入る場合。隣接するマスには5がただひとつだけ入り、その5が入るマス以外には3も5も入りません。
( => ( = b_01_01 3 ) ( or ( and ( = b_01_00 5 ) ( != a_01_02 3 ) ( != a_00_01 3 ) ( != a_02_01 3 ) ) ( and ( = b_01_02 5 ) ( != a_01_00 3 ) ( != a_00_01 3 ) ( != a_02_01 3 ) ) ( and ( = b_00_01 5 ) ( != a_01_00 3 ) ( != a_01_02 3 ) ( != a_02_01 3 ) ) ( and ( = b_02_01 5 ) ( != a_01_00 3 ) ( != a_01_02 3 ) ( != a_00_01 3 ) ) ) )
変数a_xx_yyと変数b_xx_yyが混在して見にくいと思いますけどすみません。もっと説明うまい人がSugar使いになってくれるといいんですけど。
とはいうものの、泣いてばかりいたってなんとかはなんとかなので、説明を続けます。次はひとつ飛ばしてそのマスに5が入る場合。この時は、「上下にはいずれも3が入り、左右いずれにも3も5も入らない」か、「左右にはいずれも3が入り、上下いずれにも3も5も入らない」が制約となります。以下のとおり。
( => ( = b_01_01 5 ) ( or ( and ( = 3 b_01_00 ) ( = 3 b_01_02 ) ( != 3 a_00_01 ) ( != 3 a_02_01 ) ) ( and ( = 3 b_00_01 ) ( = 3 b_02_01 ) ( != 3 a_01_00 ) ( != 3 a_01_02 ) ) ) )
4と6の場合は3と5の場合と同様ですね。6に関しては片方が4、片方が6になるので、逆転する場合も記述しなくてはいけません。以下のとおり。
( => ( = b_01_01 4 ) ( or ( and ( = b_01_00 6 ) ( != a_01_02 4 ) ( != a_00_01 4 ) ( != a_02_01 4 ) ) ( and ( = b_01_02 6 ) ( != a_01_00 4 ) ( != a_00_01 4 ) ( != a_02_01 4 ) ) ( and ( = b_00_01 6 ) ( != a_01_00 4 ) ( != a_01_02 4 ) ( != a_02_01 4 ) ) ( and ( = b_02_01 6 ) ( != a_01_00 4 ) ( != a_01_02 4 ) ( != a_00_01 4 ) ) ) )
( => ( = b_01_01 6 ) ( or ( and ( = 4 b_01_00 ) ( = 6 b_01_02 ) ( != 4 a_00_01 ) ( != 4 a_02_01 ) ) ( and ( = 6 b_01_00 ) ( = 4 b_01_02 ) ( != 4 a_00_01 ) ( != 4 a_02_01 ) ) ( and ( = 4 b_00_01 ) ( = 6 b_02_01 ) ( != 4 a_01_00 ) ( != 4 a_01_02 ) ) ( and ( = 6 b_00_01 ) ( = 4 b_02_01 ) ( != 4 a_01_00 ) ( != 4 a_01_02 ) ) ) )
以上で、同じ面積が隣り合わない1xnのブロックを配置する、という制約が記述できました。ここでちょっとした注意として、四隅には5も6も入らないという制約が必要になります。ブロックの端が盤面外に出てしまうような状況を許すからです。こういうの忘れがち。
「境界線が十字にならない」は割と簡単で、2x2のマスを取り出した時、十字いずれかが境界線にならなければいいですね。左上4マスを取り出します。
( or ( = a_00_00 a_01_00 ) ( = a_00_01 a_01_01 ) ( = a_00_00 a_00_01 ) ( = a_01_00 a_01_01 ) )
これはルールをすべて記述できていないからで、「数字が二つ以上入るブロックはない」を記述すれば良いです。数字が二つ以上入るブロックが生じるためには、パズルがきちんと成立しているという仮定を置くなら、
・表出で、3にひとマス空いて3が入っている
・表出で、4にひとマス空いて4が入っている
・表出で、4にふたマス空いて4が入っている
という状況ですね。この状況に対し、「数字が二つ以上入るブロックはない」を実現するためには、その「?マス空いて」のところに別の数字が入ればよいということになります。
例題だと、「a_02_00のマスに3が入らない」ということを記述すればいいですね。
( != a_02_00 3)
以上で、全部の制約は記述できたはずなのですが、最後のところが、個人的には見通し悪いなーと思ってしまうわけです。
なんかうまい処理はないかなっと思っていますの。