今回はパズル「フリップ・イット」の解法プログラムを Common Lisp で作ってみましょう。
「フリップ・イット (Flip It)」は芦ヶ原伸之氏が考案されたパズルで、すべての駒を裏返しにするのが目的です。今回はリバーシの駒を使うことにしましょう。次の図を見てください。
0 1 2 3 4 5 0 1 2 3 4 5
┌─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┐
│ │●│●│●│●│●│ │●│○│○│○│○│ │
└─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┘
│ │
┌─────────┘ └─────┐
↓ ↓
┌─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┐
│●│○│○│○│○│ │ │●│○│ │●│●│○│
└─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┘
5の駒が0へ跳んだ場合 2の駒が5へ跳んだ場合
図 : フリップ・イットのルール
フリップ・イットのルールは簡単です。ある駒は他の駒を跳び越して空き場所へ移動することができます。空き場所の隣にある駒は、跳び越す駒がないので移動できません。このとき、跳び越された駒は裏返しにされますが、跳んだ駒はそのままです。図では 5 の位置にある駒が 0 へ跳び、それから 2 の駒が 5 へ跳んだ場合を示しています。このあと 0 -> 2, 5 -> 0 と跳ぶと、すべての駒を白にすることができます。それでは問題です。
┌─┬─┬─┬─┬─┬─┐
(A) │●│●│ │●│●│●│
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┬─┐
(B) │●│ │○│●│●│●│●│
└─┴─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┬─┬─┐
(C) │●│ │○│○│○│●│●│●│
└─┴─┴─┴─┴─┴─┴─┴─┘
問題 : フリップ・イット
参考文献 [1] の問題は 4 つの駒を使っているので、ここでは駒の個数を増やしてみました。すべての駒を白にする最短手順を求めてください。
それではプログラムを作りましょう。アルゴリズムは単純な反復深化を使います。盤面は Lisp らしくリストで表しましょう。要素はシンボルで、nil が黒、t が白、space が空き場所を表します。
最初に、駒を動かして新しい盤面を生成する関数 move-piece を作ります。次のリストを見てください。
リスト : 駒の移動
(defun move-piece (n board piece start end)
(cond ((null board) nil)
((or (= n start) (= n end))
(cons (if (eq (car board) 'space) piece 'space)
(move-piece (1+ n) (cdr board) piece start end)))
((< start n end)
(cons (not (car board))
(move-piece (1+ n) (cdr board) piece start end)))
(t (cons (car board)
(move-piece (1+ n) (cdr board) piece start end)))))
引数 n が盤面の位置、board が盤面、piece が移動する駒の種類、start と end (start < end) が移動する駒の位置と空き場所の位置です。move-piece は board をコピーするとともに、start と end の間の駒を裏返しにして、start と end の位置にある piece と space を入れ替えます。駒の裏返しは not を使えば簡単です。
次は、反復深化で最短手順を探索する関数 solve-id を作ります。
リスト : 反復深化による探索
(defun solve-id (n limit board space history)
(if (= limit n)
(when (zerop (count nil board))
(print-answer board history)
(throw 'find-answer t))
(dotimes (x (length board))
(when (and (not (eql (cdar history) x))
(or (< x (1- space)) (< (1+ space) x)))
;; 移動可能
(solve-id (1+ n)
limit
(move-piece 0 board (nth x board) (min x space) (max x space))
x
(cons (cons x space) history))))))
引数 n が手数、limit が反復深化の上限値、board が盤面、space が空き場所の位置、history が移動手順を表すリストです。history の要素はドット対 (動かす駒の位置 . 空き場所の位置) です。
手数 n が上限値 limit になったならば、駒がすべて白になったかチェックします。関数 count で nil の個数を数え、その値が 0 であれば黒の駒はありません。関数 print-answer で手順を表示してから、throw で大域脱出して探索を終了します。
フリップ・イットは、同じ駒を続けて動かすと元の状態に戻ってしまいます。そこで、動かす駒の位置 x が 1 手前の空き場所の位置 (cdar history) と同じ場合は、その駒を動かさないようにします。history の初期値は nil なので、比較には eql を使っています。このチェックがないと実行時間がとても遅くなります。ご注意くださいませ。
それから、フリップ・イットのルールでは、空き場所の隣の駒は動かすことができません。この条件を (or (< x (1- space)) (< (1+ space) x)) でチェックしています。ルールを「空き場所の隣の駒を動かしてもよい」ことに変更する場合は、(or ... ) の部分を (/= x space) に修正してください。
最後に、solve-id を呼び出す関数 flip-it-solver と手順を表示する関数 print-answer を作ります。
リスト : 「フリップ・イット」解法プログラム
;;; 盤面を表示
(defun print-board (board)
(let ((code '((nil . "●") (t . "○") (space . "_"))))
(dolist (piece board (terpri))
(format t "~A " (cdr (assoc piece code))))))
;;; 手順を表示
(defun print-answer (board history)
(let ((s (caar history))
(p (cdar history)))
(if history
(print-answer (move-piece 0 board (nth p board) (min p s) (max p s))
(cdr history)))
(print-board board)))
;;; フリップ・イットを解く
(defun flip-it-solver (start)
(catch 'find-answer
(dotimes (limit 20)
(format t "***** ~D 手を探索 *****~%" (1+ limit))
(solve-id 0 (1+ limit) start (position 'space start) nil))))
flip-it-solver は、反復深化の上限値 limit を 1 手ずつ増やして solve-id を呼び出すだけです。print-answer は history から盤面を再現して手順を表示します。引数 board が現在の盤面で history が移動手順です。再帰呼び出しで最初の状態に戻してから print-board で盤面を表示します。盤面 board を 1 手前に戻すとき、history の先頭要素を (s . p) とすると、board の p の位置に駒があり、s の位置が空き場所であることに注意してください。
これでプログラムは完成です。詳細はプログラムリスト1をお読みください。
それでは、「フリップ・イット」の解答を示します。図では空き場所を _ で表しています。
(A) (B) (C)
0: ● ● _ ● ● ● ● _ ○ ● ● ● ● ● _ ○ ○ ○ ● ● ●
1: _ ○ ● ● ● ● ● ● ● _ ● ● ● ● ○ ● _ ○ ● ● ●
2: ● ● ○ _ ● ● _ ○ ○ ● ● ● ● _ ● ○ ● ○ ● ● ●
3: ● ● ○ ● ○ _ ○ ● _ ● ● ● ● ○ ○ _ ● ○ ● ● ●
4: ● ● _ ○ ● ○ ○ ● ● ○ _ ● ● ○ ○ ○ ○ _ ● ● ●
5: _ ○ ● ○ ● ○ ○ ● ● ○ ● ○ _ ○ ○ ○ ○ ● ○ ○ _
6: ○ ● ○ _ ● ○ ○ ● ● _ ○ ● ○ _ ● ● ● ○ ● ● ○
7: ○ _ ● ● ● ○ ○ ● ● ● ● _ ○ ○ ○ ○ ○ _ ● ● ○
8: ○ ○ ○ ○ ○ _ _ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ _
図 : フリップ・イットの解答
(A), (B), (C) ともに最短手数は 8 手になりました。実は、これが最長手数の局面となります。ちなみに、駒の個数が 4 つの場合だと、最長手数は 10 手と長くなります。また、最後の白石の位置を限定すると、手数が長くなる場合もあります。たとえば、(A) の問題でゴールを "_ ○ ○ ○ ○ ○" とすると、最短手数は 9 手になります。興味のある方は、いろいろと試してみてください。
次は、最長手数の局面を幅優先探索で求めてみましょう。最初に、キューの大きさを決めるため石の置き方が何通りあるか数えます。これは空き場所の配置から考えた方が簡単です。盤面の大きさを N とすると、空き場所の配置は N 通りあります。残りは黒石か白石のどちらかなので、石の配置は 2 N - 1 通りあります。したがって、全体では N * 2 N - 1 通りになります。
実際に計算してみると、N = 6 で 192 通り、N = 7 で 448 通り、N = 8 で 1024 通りになります。大きな数ではないので、同一局面のチェックは線形探索でいいでしょう。プログラムは次のようになります。
リスト : 「フリップ・イット」最長手数の探索
;;; 解の表示
(defun print-answer-max (n move-table state-table)
(let ((max (aref move-table n)))
(format t "最長手数 ~D 手~%" max)
(loop
(print (aref state-table n))
(decf n)
(if (/= max (aref move-table n)) (return)))))
;;; 最長手数の探索
(defun solve-max (board-size)
(let* ((max-state (* (expt 2 (1- board-size)) board-size))
(state-table (make-array max-state)) ; 盤面
(space-table (make-array max-state)) ; 空き場所の位置
(move-table (make-array max-state)) ; 手数
(rear 0)
(front 0)
board new-board space)
;; キューの初期化
(dotimes (x board-size)
(setf board (make-list board-size :initial-element t)
(nth x board) 'space
(aref state-table rear) board
(aref space-table rear) x
(aref move-table rear) 0)
(incf rear))
;; 探索
(do ()
((>= front rear))
(setq board (aref state-table front)
space (aref space-table front))
(dotimes (x board-size)
(when (or (< x (1- space)) (< (1+ space) x))
;; 移動可能
(setq new-board (move-piece 0 board (nth x board) (min x space) (max x space)))
(unless (find new-board state-table :test #'equal)
;; キューに登録
(setf (aref state-table rear) new-board
(aref space-table rear) x
(aref move-table rear) (1+ (aref move-table front)))
(incf rear))))
(incf front))
;; 解の表示
(format t "状態数 ~D 個~%" rear)
(print-answer-max (1- rear) move-table state-table)))
関数 solve-max には盤面のサイズを渡します。最初に、キューの大きさを計算して変数 max-state にセットします。キューはベクタを使って定義します。state-table が盤面、space-table が空き場所の位置、move-table が移動手数を格納します。それぞれ make-array でベクタを生成して変数にセットします。
次に、キューを初期化します。ゴールの条件である「すべての石が白の盤面」を生成してキューにセットすれば OK です。make-list で要素が t のリストを生成して変数 board にセットし、空き場所の位置 x の要素を space に書き換えます。そして、盤面 board を state-table に、空き場所の位置 x を space-table に、手数 0 を move-table にセットします。キューを管理する変数 rear をインクリメントすることもお忘れなく。
あとは単純な幅優先探索です。同一局面のチェックには関数 find を使っています。比較するデータはリストなので、キーワード :test には #'equal を指定します。あとは特に難しいところはないでしょう。詳細はプログラムリスト2をお読みくださいませ。
実行結果は次のようになりました。
(solve-max 5) (solve-max 6)
状態数 80 個 状態数 192 個
最長手数 10 手 最長手数 8 手
● ● ○ _ ● ● _ ○ ● ● ●
● ● _ ● ● ● ● _ ● ● ●
● _ ○ ● ● ● ● ● ○ _ ●
nil ● ● ● _ ● ●
nil
(solve-max 7) (solve-max 8)
状態数 448 個 状態数 1024 個
最長手数 8 手 最長手数 8 手
● ● ● ● ○ _ ● ● ● _ ● ● ● ● ●
● ● ● ● _ ● ● ● _ ○ ● ● ● ● ●
● ● _ ● ● ● ● ● _ ○ ○ ○ ● ● ●
● _ ○ ● ● ● ● ● ● ● ● ● ○ _ ●
nil ● ● ● ● ● _ ● ●
● ● ● ○ ○ ○ _ ●
nil
図 : フリップ・イットの最長手数
フリップ・イットの場合、盤面を大きくしたからといって、最長手数が長くなるとは限らないようです。興味のある方は、より大きな盤面で試してみてください。
ところで、フリップ・イットのルールでは空き場所の隣の駒を動かすことはできません。ルールを「空き場所の隣の駒を動かしてもよい」ことに変更して最長手数の局面を求めてみたところ、結果は次のようになりました。
(solve-max 5) (solve-max 6) (solve-max 7)
状態数 80 個 状態数 192 個 状態数 448 個
最長手数 6 手 最長手数 6 手 最長手数 7 手
● ● _ ● ● ● ● _ ● ● ● ● ● ● _ ● ● ●
nil ● ● ○ _ ● ● ● ● ● ○ _ ● ●
● _ ○ ● ● ● ● ● _ ○ ● ● ●
● ● ● _ ● ● nil
● ● ● ○ _ ●
● ● _ ○ ● ●
nil
図 : フリップ・イットの最長手数(別ルール)
(solve-max 8) の結果は、局面が多数あるため省略しましたが、最長手数は 7 手になりました。どうやら、このルールの方が簡単に解くことができるようです。
;;;
;;; flipit.lisp : フリップ・イット 反復深化による解法
;;;
;;; Copyright (C) 2003-2023 Makoto Hiroi
;;;
;;;
;;; 盤面を表示
;;;
(defun print-board (board)
(let ((code '((nil . "●") (t . "○") (space . "_"))))
(dolist (piece board (terpri))
(format t "~A " (cdr (assoc piece code))))))
;;;
;;; 駒を動かす
;;;
(defun move-piece (n board piece start end)
(cond ((null board) nil)
((or (= n start) (= n end))
(cons (if (eq (car board) 'space) piece 'space)
(move-piece (1+ n) (cdr board) piece start end)))
((< start n end)
(cons (not (car board))
(move-piece (1+ n) (cdr board) piece start end)))
(t (cons (car board)
(move-piece (1+ n) (cdr board) piece start end)))))
;;;
;;; 手順を表示
;;;
(defun print-answer (board history)
(let ((s (caar history))
(p (cdar history)))
(if history
(print-answer (move-piece 0 board (nth p board) (min p s) (max p s))
(cdr history)))
(print-board board)))
;;;
;;; 反復深化による探索
;;;
(defun solve-id (n limit board space history)
(if (= limit n)
(when (zerop (count nil board))
(print-answer board history)
(throw 'find-answer t))
(dotimes (x (length board))
(when (and (not (eql (cdar history) x))
(or (< x (1- space)) (< (1+ space) x)))
;; 移動可能
(solve-id (1+ n)
limit
(move-piece 0 board (nth x board) (min x space) (max x space))
x
(cons (cons x space) history))))))
;;;
;;; フリップ・イットを解く
;;;
(defun flip-it-solver (start)
(catch 'find-answer
(dotimes (limit 20)
(format t "***** ~D 手を探索 *****~%" (1+ limit))
(solve-id 0 (1+ limit) start (position 'space start) nil))))
;;;
;;; flipmax.lisp : フリップ・イット 最長手数の探索
;;;
;;; Copyright (C) 2003-2023 Makoto Hiroi
;;;
;;;
;;; 駒を動かす
;;;
(defun move-piece (n board piece start end)
(cond ((null board) nil)
((or (= n start) (= n end))
(cons (if (eq (car board) 'space) piece 'space)
(move-piece (1+ n) (cdr board) piece start end)))
((< start n end)
(cons (not (car board))
(move-piece (1+ n) (cdr board) piece start end)))
(t (cons (car board)
(move-piece (1+ n) (cdr board) piece start end)))))
;;;
;;; 盤面を表示
;;;
(defun print-board (board)
(let ((code '((nil . "●") (t . "○") (space . "_"))))
(dolist (piece board (terpri))
(format t "~A " (cdr (assoc piece code))))))
;;;
;;; 解の表示
;;;
(defun print-answer-max (n move-table state-table)
(let ((max (aref move-table n)))
(format t "最長手数 ~D 手~%" max)
(loop
(print-board (aref state-table n))
(decf n)
(if (/= max (aref move-table n)) (return)))))
;;;
;;; 最長手数の探索
;;;
(defun solve-max (board-size)
(let* ((max-state (* (expt 2 (1- board-size)) board-size))
(state-table (make-array max-state)) ; 盤面
(space-table (make-array max-state)) ; 空き場所の位置
(move-table (make-array max-state)) ; 手数
(rear 0)
(front 0)
board new-board space)
;; キューの初期化
(dotimes (x board-size)
(setf board (make-list board-size :initial-element t)
(nth x board) 'space
(aref state-table rear) board
(aref space-table rear) x
(aref move-table rear) 0)
(incf rear))
;; 探索
(do ()
((>= front rear))
(setq board (aref state-table front)
space (aref space-table front))
(dotimes (x board-size)
(when (or (< x (1- space)) (< (1+ space) x))
;; 移動可能
(setq new-board (move-piece 0 board (nth x board) (min x space) (max x space)))
(unless (find new-board state-table :test #'equal)
;; キューに登録
(setf (aref state-table rear) new-board
(aref space-table rear) x
(aref move-table rear) (1+ (aref move-table front)))
(incf rear))))
(incf front))
;; 解の表示
(format t "状態数 ~D 個~%" rear)
(print-answer-max (1- rear) move-table state-table)))
今回はパズル「変形魔方陣」の解法プログラムを Common Lisp で作ってみましょう。
それでは問題です。
┌─┬─┬─┐ 式
│A│B│C│ A + B + E + D = N
├─┼─┼─┤ B + C + F + E = N
│D│E│F│ D + E + H + G = N
├─┼─┼─┤ E + F + I + H = N
│G│H│I│ A + C + I + G = N
└─┴─┴─┘
図 : 変形魔方陣
上図の A から I の場所に 1 から 9 までの数字をひとつずつ配置します。図では、A - B - E - D のように 2 * 2 マスの正方形が 4 つ、大きな正方形 (A - C - I - G) がひとつあります。魔方陣は縦横斜めの合計が等しくなるように数字を配置しますが、今回は上図の式で表すように、正方形の頂点の合計が等しくなるような配置を見つけてください。
出典は 『Cマガ電脳クラブ第 92 回 変形魔方陣』, C MAGAZINE 1998 年 8 月号, ソフトバンク です。Cマガ電脳クラブの問題は、2 * 2 マスの数字の合計が等しくなることが条件でしたが、今回は大きな正方形も条件に加えてみました。
プログラムを作る場合、対称解のチェックは面倒だと思われる方もいるでしょう。ところが、下図のように四隅の大小関係を利用すると簡単です。
┌─┬─┬─┐ │A│B│C│ ├─┼─┼─┤ A < C < G │D│E│F│ ├─┼─┼─┤ A < I │G│H│I│ └─┴─┴─┘ 図 : 対称解のチェック
魔方陣の場合、回転解が 4 種類あって、鏡像解が 2 種類あります。四隅の大小関係をチェックすることで、これらの対称解を排除することができます。また、早い段階で枝刈りを行うため、盤面の番号と試行順序を工夫します。
┌─┬─┬─┐
│0│4│1│ (1) B[0] + B[1] + B[2] + B[3] = N
├─┼─┼─┤ (2) B[0] + B[4] + B[5] + B[6] = N
│5│6│7│ (3) B[1] + B[4] + B[6] + B[7] = N
├─┼─┼─┤ (4) B[2] + B[5] + B[6] + B[8] = N
│2│8│3│ (4) B[3] + B[6] + B[7] + B[8] = N
└─┴─┴─┘
図 : 盤面の番号と試行順序
盤面を 1 次元配列 B で表すことにします。試行順序を上図のように定義し、配列の添字と対応させます。そうすると、最初に四隅 (0, 1, 2, 3) の数字が選択されますね。ここで対称解のチェックが行われるので、枝刈りの効率は良くなります。また、数字の合計値 N も決めることができるので、あとは 2 * 2 マスの正方形が完成したら、合計値が N と等しいかチェックしていくだけです。
プログラムは次のようになります。配列を使っているので Lisp らしくありませんが、ほかのプログラミング言語に移植するのは簡単でしょう。興味のある方は挑戦してみてください。
リスト : 解法プログラム
;;; 盤面
(defvar *board* (make-array 9))
;;; 解の表示
(defun print-answer ()
(format t "~D ~D ~D~%~D ~D ~D~%~D ~D ~D~%~%"
(aref *board* 0) (aref *board* 4) (aref *board* 1)
(aref *board* 5) (aref *board* 6) (aref *board* 7)
(aref *board* 2) (aref *board* 8) (aref *board* 3)))
;;; 4 つの数字を足し算する
(defun add-number (a b c d)
(+ (aref *board* a) (aref *board* b) (aref *board* c) (aref *board* d)))
;;; 枝刈り
(defun checkp (n value)
(or (and (= n 1) (> (aref *board* 0) (aref *board* 1)))
(and (= n 2) (> (aref *board* 1) (aref *board* 2)))
(and (= n 3) (> (aref *board* 0) (aref *board* 3)))
(and (= n 6) (/= value (add-number 0 4 5 6)))
(and (= n 7) (/= value (add-number 1 4 6 7)))))
;;; 解法
(defun solve (&optional (n 0) (numbers '(1 2 3 4 5 6 7 8 9)) value)
(dolist (x numbers)
(setf (aref *board* n) x)
;; 枝刈り
(unless (checkp n value)
;; value のセット
(if (= n 3)
(setq value (add-number 0 1 2 3)))
;; 解けたか
(cond ((= n 8)
(if (= value (add-number 2 5 6 8) (add-number 3 6 7 8))
(print-answer)))
(t (solve (1+ n) (remove x numbers) value))))))
次は 8 個の数字を使う魔方陣です。それでは問題です。
┌─┬─┬─┐
│A│B│C│ 式
├─┼─┼─┤ A + B + C = N
│D│ │E│ A + D + F = N
├─┼─┼─┤ C + E + H = N
│F│G│H│ F + G + H = N
└─┴─┴─┘
図 : 変形魔方陣
上図の A から H の場所に 1 から 8 までの数字をひとつずつ配置します。4 辺の合計が等しくなるような配置を見つけてください。なお、合計の値 (N) は 12, 13, 14, 15 の 4 通りの場合があります。
プログラムの作成は簡単です。問題1のプログラムを改造するだけです。特に難しいところはないので、説明は省略いたします。詳細はプログラムリストをお読みくださいませ。
ところで、問題2は数字 { 1, 2, 3, 4, 5, 6, 7, 8 } を使いましたが、数字を奇数 { 1, 3, 5, 7, 9, 11, 13, 15 } にする、または、数字を偶数 { 2, 4, 6, 8, 10, 12, 14, 16 } にするとどうなるでしょうか。実は、問題2の答えがわかると簡単に解くことができます。答えを見る前に、ちょっと考えてみてくださいね。
最後は素数を使った変形魔方陣です。
┌─┬─┬─┐
│A│B│C│ 式
├─┼─┼─┤ A + B + C = N
│D│ │E│ A + D + F = N
├─┼─┼─┤ C + E + H = N
│F│G│H│ F + G + H = N
└─┴─┴─┘
図 : 素数の変形魔方陣
上図の A から H の場所に素数 { 3, 5, 7, 11, 13, 17, 19, 23 } をひとつずつ配置します。4 辺の合計が等しくなるような配置を見つけてください。なお、合計の値 (N) も素数になります。
;;;
;;; magic2.lisp : 変形魔方陣の解法プログラム
;;;
;;; Copyright (C) 2005-2023 Makoto Hiroi
;;;
;;; 盤面
;;;
;;; 041
;;; 5 6 0 < 1 < 2, 0 < 3
;;; 273
;;;
(defvar *board* (make-array 8))
;;; 数字を足し算する
(defun add-number (n1 n2 n3)
(+ (aref *board* n1) (aref *board* n2) (aref *board* n3)))
;;; 解の表示
(defun print-answer (rest-num)
(format t "Rest ~A, Sum ~D~%" rest-num (add-number 0 4 1))
(format t "~2D ~2D ~2D~%~2D ~2D~%~2D ~2D ~2D~%~%"
(aref *board* 0) (aref *board* 4) (aref *board* 1)
(aref *board* 5) (aref *board* 6)
(aref *board* 2) (aref *board* 7) (aref *board* 3)))
;;; 枝刈り
(defun checkp (n value)
(or (and (= n 1) (> (aref *board* 0) (aref *board* 1)))
(and (= n 2) (> (aref *board* 1) (aref *board* 2)))
(and (= n 3) (> (aref *board* 0) (aref *board* 3)))
(and (= n 5) (/= value (add-number 0 5 2)))
(and (= n 6) (/= value (add-number 1 6 3)))))
;;; 解法
(defun solve (&optional (n 0) (numbers '(1 2 3 4 5 6 7 8)) value)
(dolist (x numbers)
(setf (aref *board* n) x)
;; 枝刈り
(unless (checkp n value)
;; value のセット
(if (= n 4)
(setq value (add-number 0 1 4)))
;; 解けたか
(cond ((= n 7)
(if (= value (add-number 2 7 3))
(print-answer (remove x numbers))))
(t (solve (1+ n) (remove x numbers) value))))))
対称解(回転解と鏡像解)を除くと、解は下図の 1 通りしかありません。
┌─┬─┬─┐ │1│8│3│ ├─┼─┼─┤ │6│5│4│ ├─┼─┼─┤ │7│2│9│ └─┴─┴─┘
解の個数は対称解(回転解と鏡像解)を除いた場合です。
┌─┬─┬─┐ │1│8│3│ ├─┼─┼─┤ │5│ │7│ ├─┼─┼─┤ │6│4│2│ └─┴─┴─┘
┌─┬─┬─┐ ┌─┬─┬─┐ │1│7│5│ │1│8│4│ ├─┼─┼─┤ ├─┼─┼─┤ │4│ │6│ │7│ │3│ ├─┼─┼─┤ ├─┼─┼─┤ │8│3│2│ │5│2│6│ └─┴─┴─┘ └─┴─┴─┘
┌─┬─┬─┐ ┌─┬─┬─┐ │1│6│7│ │3│7│4│ ├─┼─┼─┤ ├─┼─┼─┤ │5│ │3│ │6│ │2│ ├─┼─┼─┤ ├─┼─┼─┤ │8│2│4│ │5│1│8│ └─┴─┴─┘ └─┴─┴─┘
┌─┬─┬─┐ │3│5│7│ ├─┼─┼─┤ │4│ │2│ ├─┼─┼─┤ │8│1│6│ └─┴─┴─┘
偶数 { 2, 4, 6, 8, 10, 12, 14, 16 } の場合は、問題2の数字を 2 倍することで求めることができます。奇数 { 1, 3, 5, 7, 9, 11, 13, 15 } の場合は、数字を 2 倍して -1 することで求めることができます。下図に一例を示します。
┌─┬─┬─┐ ┌─┬─┬─┐ ┌─┬─┬─┐ │1│8│3│ │2│16│6│ │1│15│5│ ├─┼─┼─┤ ├─┼─┼─┤ ├─┼─┼─┤ │5│ │7│ │10│ │14│ │9│ │13│ ├─┼─┼─┤ ├─┼─┼─┤ ├─┼─┼─┤ │6│4│2│ │12│8│4│ │11│7│3│ └─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘ 問題2の解 (A)偶数の場合 (B)奇数の場合
┌─┬─┬─┐ 合計値 N = 31(素数) │3│23│5│ ├─┼─┼─┤ │17│ │19│ ├─┼─┼─┤ │11│13│7│ └─┴─┴─┘