読者です 読者をやめる 読者になる 読者になる

ナンバーリンクをプログラマチックに解こうとする話

LobiでAndroidをやってますうえだです。
Tech KAYAC Advent Calendar 2016の10日目の記事を書かせていただきます。 よろしくお願いします!

業務で携わった「Androidでアプリを録画した話」について書こうか、
完全業務外の「ナンバーリンクというやつをいい感じに解こうとした話」について書こうか迷いましたが、
(個人的に)ロマンが溢れてる後者について書くことにしました。

あらすじ

最近、ナンバーリンクというペンシルパズルのアプリを作っています。
ナンバーリンクとは以下のようなパズルです。

こんな風に盤面に数字が配置されていて、
f:id:uedaa17:20161207000605p:plain:w300

こんな風にいい感じに同じ数字を繋げればクリアです。
f:id:uedaa17:20161207000700p:plain:w300

ルールはこんな感じです。

  1. 線は隣接したマスを縦横に通っていく。
  2. 1マスを2本以上の線が通ってはいけない。交差したり別れたりしてはいけない。

(「すべてのマスを線が通ってなければいけない」というルールがあることもあります。)

さて、このような問題をただひたすら解き続けるアプリを作っているのですが、その中で一番辛かったのが問題の量産でした。
小さい問題から大きい問題まで200題ほどを、コツコツコツコツ作っていたのですが、それがまったく楽しくない作業だったのです。
そして今後、「デイリーで問題配信したい」などと構想し始め 、ちまちま問題を手作りなどしていられなくなりました。

なので、問題を自動で量産する仕組みを作ろうと考え始めました。
しかしこれは相当厄介で、 ナンバーリンクは巷では「かなり問題を作りづらいし、解きづらいペンシルパズル」と言われていたりします。
実際、それっぽい問題を自動生成するアルゴリズムを考えてはみたものの、単純でしょうもないカスのような問題しか産みだしてくれませんでした。

そこから、いろいろ試行錯誤したりぐぐったりした結果、いい感じの問題が作れそうな手順を導き出しました。
これです。

  1. 盤面にランダムに数字を配置!
  2. それがいい感じの問題か判定!
  3. いい感じの問題でなかったら、1からやり直す。

とても馬鹿っぽいですが、

  • ランダム性によってくせのない問題になる(かもしれない)
  • デイリーで問題配信となると、短時間で膨大な数を作る必要はないため、多少時間がかかってもいい

と考えた故です。

(1.については、もちろん完全ランダムに数字を配置するのではなく、少しいろいろ工夫していますが、 そこらへんの話は省略しています。)

ここで重要なのは、2の「いい感じの問題か判定する」方法です。
ここももちろん、人力でなく、自動かつ高速でやらなければ話になりません。

長くなってしまいましたが、この「いい感じの問題の判定方法」についてが 「ナンバーリンクをプログラマチックに解こうとする話」になります。

この課題に、組合せ最適化問題の考え方を用いてアプローチしました。

組合せ最適化問題について

かなりざっくりにいうと、組合せ最適化問題とは、(非連続な)いくつか状態の組合せから、条件を満たし、かつ最適なものを見つける問題です。
ナンバーリンクは組合せ最適化問題といえます。

例えば次の問題

f:id:uedaa17:20161207004020p:plain:w150

を組合せ最適化問題の要素である、「状態」、「条件」、「最適化するもの」に当てはめると、以下のようになります。

状態:

各マスが取りうる状態です。 ナンバーリンクは「空白マス」と「数字マス」があり、
空白マスは以下のいずれかの状態をとります。

f:id:uedaa17:20161207010428p:plain:w50
f:id:uedaa17:20161207010432p:plain:w50 f:id:uedaa17:20161207010434p:plain:w50 f:id:uedaa17:20161207010803p:plain:w50 f:id:uedaa17:20161207010807p:plain:w50 f:id:uedaa17:20161207010811p:plain:w50 f:id:uedaa17:20161207010813p:plain:w50
f:id:uedaa17:20161207010950p:plain:w50 f:id:uedaa17:20161207010957p:plain:w50 f:id:uedaa17:20161207011002p:plain:w50 f:id:uedaa17:20161207011006p:plain:w50 f:id:uedaa17:20161207011218p:plain:w50 f:id:uedaa17:20161207011009p:plain:w50

また数字1のマス、2のマスは以下のいずれかの状態をとります。

f:id:uedaa17:20161209080344p:plain:w50 f:id:uedaa17:20161207012557p:plain:w50 f:id:uedaa17:20161207012600p:plain:w50 f:id:uedaa17:20161207012602p:plain:w50 f:id:uedaa17:20161207012604p:plain:w50
f:id:uedaa17:20161207014336p:plain:w50 f:id:uedaa17:20161207013048p:plain:w50 f:id:uedaa17:20161207013051p:plain:w50 f:id:uedaa17:20161207013052p:plain:w50 f:id:uedaa17:20161207013055p:plain:w50

これら状態を組合せは、

f:id:uedaa17:20161207014719p:plain:w150 f:id:uedaa17:20161207014722p:plain:w150
f:id:uedaa17:20161207014726p:plain:w150 f:id:uedaa17:20161207014723p:plain:w150

といったものになったりします。

条件:

状態の組合せが、ナンバーリンクの解として成立していること。 上図で言えば下の2つです。

最適化するもの:

空白マスの数をなるべく少なくする(空白マスの数が0である解を探すため)。

このような要件のとき、条件を満たしてかつ状最適解と呼べる状態の組合せは、

f:id:uedaa17:20161207014726p:plain:w150

だけになります。

なぜ組合せ最適化問題として扱うか

なぜナンバーリンクを組合せ問題として扱うか。
それは、プログラムで解きやすいためです。

組合せ最適化問題の実態は、整数だけの最適化問題です。 つまり、

 \displaystyle
\mbox{最大化する式} \;\;s = 3x + 2y + 5z \\
\mbox{条件} \; \; 5x + 10y + 2z \leq 40 \\
\; \; \; \; \; \; \; \; \; x - 2y + 10z \leq 20 \\
\; \; \; \; \; \; \; \; \; 9x + 4y + 6z \leq 230 \\
\; \; \; \; \; \; \; \; x、y、z\mbox{は整数}

のような問題です。 条件を満たし、かつ sを最大化するような x y zを求めよ、というやつです。

ナンバーリンクの実態はこういうやつです。 そしてその最適化問題を解けば、ナンバーリンクの解が得られます。

数式が出てきて嫌な感じになる人もいると思いますが、 こういう問題を解くソフトウェアは、フリーのものから商用のものまでたくさんあるので無問題です(GLPK、SCIP、CPLEXなど)。

ここで重要なのは、ナンバーリンクの定式化の方法さえ知っていれば、 ナンバーリンクの解き方は知らなくても問題は解けるということです。 そしてこの考え方は、世の中の組合せ最適化問題の構造を持つあらゆる問題に通じています。

世の中には組合せ最適化問題といえるものがたくさんあります。 同じペンシルパズルである魔法陣、ピクロス、数独や、将棋、囲碁、オセロのようなゲーム、 さらには経路探索、電車の乗り継ぎ、仕事のスケジューリング、翻訳などなど。

ただしそれらの問題すべてそういうフローで解決できるかというと、実際はまったくそんなことはありません。 以下の壁に突き当ることが多いためです。

  1. 問題の正確な定式化が難しい。
  2. 問題が大きくなると、だいたい計算量がえらい勢いで増える。

実はナンバーリンクも例外でなく、2.の問題を抱えています。

ナンバーリンクを組合わせ最適化問題として扱うことの落とし穴

ナンバーリンクは、問題の盤面の大きさや配置する数字の数が増加すると、
組合せ数がえらい勢い増えて(いわゆる組合せ爆発して)しまい、
必要な計算量もえらい勢いで増えて(いわゆる指数関数的に増加して)しまいます。

なので、なるべく状態数が少なくなるように定式化する、といった工夫をしなければ、
現代のコンピュータでも、15×15の盤面の問題を解くのにさえ100年以上かかったりしてしまいます。

ちなみに、ナンバーリンクを効率的に解く方法を人類はまだ見つけていません。
もしナンバーリンクを効率的に解く方法を見つけることができたら、数学史に名を残せます!
(詳しくは、「ナンバーリンク NP完全性」でググってみてください。だいぶロマンが溢れています。)

結局、ナンバーリンクの問題生成は自動化はできるのか。

これからやってみるのでまだ分かりません(すみません......)が、 そこまで盤面が大きくなければ、できそうな気はします。

  1. 盤面にランダムに数字を配置。
  2. それがいい感じの問題か判定する。
  3. いい感じの問題でなかったら、1からやり直す。

の「2. いい感じの問題か判定する」に組合せ最適化問題の考え方を用いますが、ここがよさげに高速できるかどうかにかかっています。
おそらくいろいろ工夫すれば、10×10くらいまでの問題なら、なんとか自動生成できるのではと考えています。

ナンバーリンクをプログラマチックに解こうとする

上の方で示した組合せ最適問題への定式化は、だいぶ概念的でした。
もう少ししっかりプログラム的に解けるよう定式化し、「いい感じの問題か判定する」をどのようにしようとしているか、 最後にそれを示して終わりたいと思います。

いい感じの問題について

f:id:uedaa17:20161209052653p:plain:w150

のように、空白マスを残したままの解を、ナンバーリンク用語で短絡解といいます。 このような短絡解が存在する問題は、ナンバーリンク界では「いい感じでない問題」とされることがあるため、それに倣います。

なので、「それがいい感じの問題か判定する」は、「解が存在し、かつ短絡解が存在しない問題か判定する」とします。

  1. 盤面にランダムに数字を配置する。
  2. 1の問題に、解が存在し、かつ短絡解が存在しないか判定する。
  3. そんな問題でなかったら、問題を作り直す。

(もちろん2を満たすからといって優れた問題になっているとは限りませんが、 ここではこれ以上細かいことには触れないことにします。)

定式化

先ほどと同じように、「状態」、「条件」、「最適化するもの」を考えてます。
先ほどはだいぶざっくりしていましたが、もう少し数学的な変数を使ったりしてみます。
また、短絡解が存在しない問題であるか判定するを考慮するようにします。

以下のナンバーリンクの問題を例として考えてみます。

f:id:uedaa17:20161209052608p:plain:w150

数字は1、2、マスは s_1から s_{12}までです。 数字の集合、マスの集合を I Sとします。

 \displaystyle
  I = \{1, 2\} \\
  S = \{s_1, s_2, \dots, s_{12}\}

また、あるマス sの隣にあるマスの集合を A(s)とします
(例えば A(s_1) = \{s_2, s_4\})。

状態:

以下の変数で表します。

  •  N_{s, i} = 0 \mbox{ or } 1
    マス sに数字 iの線(数字 iのマスから伸びる線)が通るなら1、そうでないなら0。

  •  L_{s, s', i} = 0 \mbox{ or } 1
    マス sから s'へ数字 iの線が通るなら1、通らないなら0。

例えば

f:id:uedaa17:20161209060350p:plain:w100

の状態のとき、

  •  s_1は数字1の線が通るため、N_{s_1, 1} = 1N_{s_1, 2} = 0

  •  s_1 s_2は数字1の線でつながっているため、L_{s_1, s_2, 1} = 1 L_{s_1, s_2, 2} = 0

  •  s_1 s_4は数字1の線でつながっているため、L_{s_1, s_4, 1} = 1 L_{s_1, s_4, 2} = 0

になります。

なぜどの状態も0 or 1で表しているかというと、次の条件を考えるとき、その方がやりやすいためです。

条件:

ここではざっくり考えます(計算量を減らすには、しっかり考える必要があります)。

条件1 あるマス sにはある数字の線が通る、または空白のまま。

 sをある数字の線が通る」は、「 sには、1か2の線いずれか1本通る」なので、 N_{s, 1} + N_{s, 2} = 1
また sは空白のままは、 N_{s, 1} + N_{s, 2} = 0。 とできます。

この条件式は
 N_{s, 1} + N_{s, 2} = 0 \mbox{ or } 1
とまとめられます。

条件2 あるマス sが数字マスでないとき、

  • 数字 iの線が通る場合、 s A(s)のいずれか2マスと iの線とつながっている。
  •  iの線が通らない場合、 A(s)のどのマスとも iの線でつながっていない。

 A(s) = \{s', s'', s'''\}とすると、
 s A(s)いずれか2つマスと iの線でつながっている」は  L_{s, s', i} +  L_{s, s'', i} + L_{s, s''', i} = 2
逆に「 iの線でつながってない」は、  L_{s, s', j} +  L_{s, s'', j} + L_{s, s''', j} = 0
という式で表せます。 これも次の1つの式にまとめられます。

 2N_{s, i} - (L_{s, s', i} + L_{s, s'', i} + L_{s, s''', i}) = 0

さらにもう少し一般的に書くと、

 \displaystyle
  2N_{s, i} - \sum_{s' \in A(s)}L_{s, s', i} = 0

となります。

条件3 あるマス sが数字 iのマスのとき、

  •  s A(s)のいずれか1つのマスと i'の線でつながっている。
  • 逆にそれ以外の数字の線ではつながっていない。

これは条件2とほぼ同じ感じで、
  \displaystyle
  N_{s, i} - \sum_{s' \in A(s)}L_{s, s', i} = 0
で表せます。

また、 s i'のマスなので、
 N_{s, i'} = 1 です。

最適化するもの:

「空白のマスをなるべく多くする」= 「線の入るマスを少なくする」、つまり
  N_{s_1, 1} + N_{s_1, 2} + \dots + N_{s_{12}, 1} + N_{s_{12}, 2}
が最小になるようにする。


以上の状態、条件、最適化するものをまとめてもう少しそれっぽく書くと、
以下のようになります(と思います。あまり数式に自信がない)。
ただし S_1は数字1のマスの集合( \{s_{10}, s_{12}\})、 S_2は数字2のマスの集合( \{s_5, s_{11}\})、  S_0はそれ以外のマスの集合、を表しています。

 \displaystyle
  \mbox{minimize}\; z = \sum_{s \in S, i \in I}N_{s, i}\\
  \mbox{subject to}\;\;\;\:  N_{s, i} \in \{0, 1\} \; \; \; \forall s \in S_0, \forall i \in I \\
\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\:\: N_{s, i} = \left \{
\begin{array}{l}
1 (i=1) \\
0 (i\neq 1)
\end{array}
\right. \; \; \; \forall s \in S_1 \\
\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\:\: N_{s, i} = \left \{
\begin{array}{l}
1 (i = 2) \\
0 (i \neq 2)
\end{array}
\right. \; \; \; \forall s \in S_2 \\
\displaystyle \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\:\: \sum_{i\in I} N_{s, i} \in \{0, 1\} \; \; \; \forall s \in S\\
\displaystyle \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\:\: 2N_{s, i} - \sum_{s' \in A(s)}L_{s, s', i} = 0 \; \; \; \forall s \in S_0, \forall i \in I \\
\displaystyle \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\:\: N_{s, i} - \sum_{s' \in A(s)}L_{s, s', i} = 0 \; \; \;  \forall s \in S_1 \cup S_2, \forall i \in I \\

minimizeは最適化する式、subject toは条件式、を表しています。

問題を解くと

この最適化問題を、最適化問題ソフトで解いたりすると、

 z = 12
 N_{s_1, 1} = 1
 N_{s_1, 2} = 0
 N_{s_2, 1} = 1
...

のような結果が得られます。
すると s_1には1の線が通過する、などということがわかるので、その情報を組み立てると

f:id:uedaa17:20161209071345p:plain:w150

という解が得られます!

またこの解は、最適化の条件より、問題の中で「最も線の通過するマスが少ない」(=空白マスが多い)パターンです。
つまりこの解が短絡解でないということは、この問題には短絡解がなく、したがって「これはいい問題である」ことが確認できます。
 zは線の通過するマスの数を示しているので、「 z = マスの数」かどうかで、「短絡解があるか」どうかがわかります。

また、もしそもそもこの問題がナンバーリンクとして解けない場合、最適化問題ソフトはしっかり解がないことを教えてくれます。

ちなみに

この方法による定式化では、
4×4の盤面で数字が1〜3ぐらいであれば、状態を表す変数は76、条件式の数は46はになりましたが、
10×10の盤面で数字が1〜8までとかになると、状態の変数は1906、条件式の数は838はまで膨れ上がりました
(数字マスが端にあるかなどで、数は多少増減します)。

まとめ

  • ナンバーリンクは、組合せ最適化問題的に考えると、それほど大きくない盤面の問題なら、自動的かつそれなりの時間で解けそう。でも工夫は必要そう。
  • 問題を作る人を雇った方が効率が良さそう。

終わりに

  • はてなぶろぐは数式扱うのが辛い
  • 定式化の話は、間違っている気もするので、おかしなところがあればご指摘いただけると大変ありがたいです。
  • 明日は @pocke-pocke-pocke さんによる(おそらく)トレンディーな話です!
  • カヤックUnityアドベントカレンダー も熱いです!