0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AI駆動開発】パズルのソルバー開発をAIに「丸投げ」したら、SATソルバーというガチ解法で殴られた話(ナンバーリンク)

0
Last updated at Posted at 2026-02-28

はじめに:AI時代の「ものづくり」

最近のAI、進化が早すぎませんか?
毎日のように新しいモデルが出てきて、「ゲーム作ってみた」「アプリ作ってみた」という記事を見るたびに、エンジニアとしての血が騒いでいました。

「自分もAIを使って何か作りたい。どうせなら、昔から好きなパズルで。」

私は 「ナンバーリンク」 という、盤面上の同じ数字同士を線で結ぶパズルが大好きです。
かねてから「このパズルを自動で解いたり、あるいは無限に自動生成するプログラムを作りたい」と思っていました。

これまでは「アルゴリズムどうしよう、バックトラック法とかか……? 実装が面倒だ……」と腰が重かったのですが、「今のAIなら、設計から実装まで全部丸投げしてもいけるのでは?」 と思い立ち、Geminiを相棒(というか主戦力)に開発を始めてみました。

結論から言うと、自分では思いつかなかった「SATソルバー」というガチな数学的アプローチを提案され、実装まで完走できました(そして最後に、AIだけでは越えられない壁にもぶつかりました)。

AIとのペアプロ開始

1. とりあえず要望を投げる

まずは雑に、やりたいことを投げました。

: 与えられた、ナンバーリンクというペンシルパズルの問題を解くソルバープログラムを開発したいです。最適なアルゴリズムなども、あれば教えてください。

すると、AIは即座にこう返してきました。

AI: ナンバーリンクはNP完全問題として知られており、制約プログラミングやSATへの還元が、特に難易度の高い問題を解くのに有効な手段です。SATソルバー(充足可能性問題ソルバー)を用いて解くのが有効なアプローチです。Pythonの python-sat (PySAT) ライブラリを使用するのが標準的で高速です。

お、おう、なるほどなー、充足可能性問題なー! わかるわー完全に理解した。

そして、頼んでもいないのに「変数の定義」「制約条件の数式化(CNF化)」、そして「実装コード」まで一気に出力してきました。

2. SATソルバー? なにそれ?

正直、私は数学や数理モデル構築の専門家ではありません。「なんか速いらしい」くらいの知識でした。
しかしAIの説明によると、ナンバーリンクは以下のような論理パズルとして記述できるそうです。

  • 色の変数 ($C_{i,j,k}$): マス $(i, j)$ に数字 $k$ が入るか?
  • 接続の変数 ($E_{u,v}$): マスとマスが繋がっているか?
  • 制約:
    • 「隣り合うマスが繋がっているなら、色は同じでなければならない」
    • 「数字のマスは端点なので線は1本だけ」
    • 「空白マスは線が通るなら2本(入って出る)、通らないなら0本」

これらを「論理式(CNF)」に変換し、ソルバーに食わせると、答えが一瞬で出る。
なるほど、バックトラックで再帰関数をカリカリ書くより、数理モデルに落とし込むほうが賢いのか。AI、やるじゃん。

3. バグ発生、そして修正もAI任せ

最初に出力されたコードを実行してみると、何を投げても No solution foundに。どうやら何かがおかしい。

ここでも、自分でデバッグプリントを仕込むのではなくAIに丸投げします。

: 「問題が全く解けないんだけど、なんで?」

AI: 「解析しました。どうやらライブラリの変数ID管理にバグがあったので直します」

すごい。

「論理的な制約のミス」と「ライブラリの使い方のミス」を同時に修正したコードが返ってきました。人間がやったら半日は溶ける作業です。

完成したコード(抜粋)

最終的に出来上がったソルバーのコア部分(のよう)です。
実際、ソースを全文解読はできていないです(エンジニア失格か)。

GitHubリポジトリとして公開しました:
mashinosatoshi/pencil_puzzle_solver

    def _generate_constraints(self, force_full_fill):
        for r in range(self.rows):
            for c in range(self.cols):
                # ... (色の制約などは省略) ...

                # 次数制約(ここが肝)
                if self.grid[r][c] != 0:
                    # 端点: 次数は必ず 1
                    self.add_clauses(CardEnc.equals(lits=adj_edges, bound=1, top_id=self.next_var-1))
                else:
                    # 空白マス
                    if force_full_fill:
                        # 全マス埋め問題の場合: 次数は必ず 2
                        self.add_clauses(CardEnc.equals(lits=adj_edges, bound=2, top_id=self.next_var-1))
                    else:
                        # 空白許容の場合: 次数は 0 または 2 (1は禁止、3以上も禁止)
                        
                        # (A) 次数は2以下
                        self.add_clauses(CardEnc.atmost(lits=adj_edges, bound=2, top_id=self.next_var-1))
                        
                        # (B) 次数は1ではない (入ってきたら必ず出る)
                        if len(adj_edges) > 0:
                            for i, e_target in enumerate(adj_edges):
                                others = adj_edges[:i] + adj_edges[i+1:]
                                if others:
                                    # e_target が True なら、others のどれかも True
                                    self.cnf.append([-e_target] + others)
                                else:
                                    self.cnf.append([-e_target])

実行結果

Webで見かける形式のテキストデータを食わせてみます。

入力:

. . 4 . . . 
. . . . 2 . 
. 3 . . . 3 
. 1 2 4 . . 
. . . . 1 . 
. . . . . . 

出力:

[4, 4, 4, 3, 3, 3]
[4, 3, 3, 3, 2, 3]
[4, 3, 2, 2, 2, 3]
[4, 1, 2, 4, 4, 4]
[4, 1, 1, 1, 1, 4]
[4, 4, 4, 4, 4, 4]

(※実際は数字ではなく線を可視化したほうが分かりやすいですが、ロジック上は正しく線が繋がっています!)

一瞬です。試しに10×10の一般的な難易度のものを投げてみても、すぐに答えが返ってきました。
自分でアルゴリズムを考えていたら、まだ「再帰が止まらない」と悩んでいたことでしょう(そもそもコーディングにまでたどり着けていなかった可能性すら)。

残された課題:25×25 ジャイアントパズルの壁

さて、ここまでは順風満帆でした。
「AIすごい!SATソルバー最強!もう全部これでいいじゃん!」

そう調子に乗った私は、ネットで見つけた 「25×25」の超巨大ナンバーリンク(ジャイアントパズル) をこのソルバーに投げてみました。
人間が解けば数時間はかかる代物です。AIなら数秒で終わるでしょう。

……返ってこない。

1分経過。
5分経過。
ファンが唸りを上げ、CPU使用率は100%に張り付いていますが、答えが出ません。
結局、10分待っても結果は返ってきませんでした。

なぜ解けないのか?

どうやら、盤面が大きくなると変数の数(マス数 × 色数)と制約条件(Clause)が爆発的に増えてしまい、今の「ナイーブな(単純な)SATエンコーディング」では探索空間が広すぎて太刀打ちできないようです。

AIに「作って」と丸投げしたコードは、正しくはあるものの「最適化」までは考慮されていませんでした。
ここから先は、

  • 不要な変数の削減(前処理)
  • より高度な制約の追加(ループ禁止の効率化など)
  • あるいはSATではなくZDD(Zero-suppressed Binary Decision Diagram)のような別アルゴリズムへの移行

といった、「人間のエンジニアによるチューニング」 が必要になりそうです。

おわりに

AIは「0から1」を爆速で作ってくれましたが、「1を100にする」高速化や最適化の領域には、まだ我々が手を動かす余地(楽しみ)が残されていました。

とはいえ、たった数十分で「動くソルバー」が手に入ったのは事実です。
このベースラインがあるからこそ、「じゃあどう高速化するか?」という次のステップに進めます。

次は、この「25×25の壁」を超えるべく、AIと相談しながら高速化に挑んでみたいと思います。

GitHubリポジトリ(記事作成時点コミット):
https://github.com/mashinosatoshi/pencil_puzzle_solver/tree/bd1a22114a8c71b6990bfaccedef32da5d577f8a

※この記事もGeminiを使用して作成しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?