Common Lispで数独を解くプログラム
お絵描きロジックをボールペンで解くと失敗したときに取り返しがつかない。何問かやってみて結構な頻度で取り返しがつかなくなるのに憤慨したからお絵描きロジックを解くプログラムを書こうと思った。お絵描きロジックを解くアルゴリズムをはっきりさせるのが難しそうだったからとりあえず探索するだけで解ける数独を解くプログラムを書いたってワケ。久々にLispで遊んだ。
(defparameter *nums* '(1 2 3 4 5 6 7 8 9)) (defparameter *rules* '(;; rows ((0 . 0) (0 . 1) (0 . 2) (0 . 3) (0 . 4) (0 . 5) (0 . 6) (0 . 7) (0 . 8)) ((1 . 0) (1 . 1) (1 . 2) (1 . 3) (1 . 4) (1 . 5) (1 . 6) (1 . 7) (1 . 8)) ((2 . 0) (2 . 1) (2 . 2) (2 . 3) (2 . 4) (2 . 5) (2 . 6) (2 . 7) (2 . 8)) ((3 . 0) (3 . 1) (3 . 2) (3 . 3) (3 . 4) (3 . 5) (3 . 6) (3 . 7) (3 . 8)) ((4 . 0) (4 . 1) (4 . 2) (4 . 3) (4 . 4) (4 . 5) (4 . 6) (4 . 7) (4 . 8)) ((5 . 0) (5 . 1) (5 . 2) (5 . 3) (5 . 4) (5 . 5) (5 . 6) (5 . 7) (5 . 8)) ((6 . 0) (6 . 1) (6 . 2) (6 . 3) (6 . 4) (6 . 5) (6 . 6) (6 . 7) (6 . 8)) ((7 . 0) (7 . 1) (7 . 2) (7 . 3) (7 . 4) (7 . 5) (7 . 6) (7 . 7) (7 . 8)) ((8 . 0) (8 . 1) (8 . 2) (8 . 3) (8 . 4) (8 . 5) (8 . 6) (8 . 7) (8 . 8)) ;; columns ((0 . 0) (1 . 0) (2 . 0) (3 . 0) (4 . 0) (5 . 0) (6 . 0) (7 . 0) (8 . 0)) ((0 . 1) (1 . 1) (2 . 1) (3 . 1) (4 . 1) (5 . 1) (6 . 1) (7 . 1) (8 . 1)) ((0 . 2) (1 . 2) (2 . 2) (3 . 2) (4 . 2) (5 . 2) (6 . 2) (7 . 2) (8 . 2)) ((0 . 3) (1 . 3) (2 . 3) (3 . 3) (4 . 3) (5 . 3) (6 . 3) (7 . 3) (8 . 3)) ((0 . 4) (1 . 4) (2 . 4) (3 . 4) (4 . 4) (5 . 4) (6 . 4) (7 . 4) (8 . 4)) ((0 . 5) (1 . 5) (2 . 5) (3 . 5) (4 . 5) (5 . 5) (6 . 5) (7 . 5) (8 . 5)) ((0 . 6) (1 . 6) (2 . 6) (3 . 6) (4 . 6) (5 . 6) (6 . 6) (7 . 6) (8 . 6)) ((0 . 7) (1 . 7) (2 . 7) (3 . 7) (4 . 7) (5 . 7) (6 . 7) (7 . 7) (8 . 7)) ((0 . 8) (1 . 8) (2 . 8) (3 . 8) (4 . 8) (5 . 8) (6 . 8) (7 . 8) (8 . 8)) ;; squares ((0 . 0) (0 . 1) (0 . 2) (1 . 0) (1 . 1) (1 . 2) (2 . 0) (2 . 1) (2 . 2)) ((3 . 0) (3 . 1) (3 . 2) (4 . 0) (4 . 1) (4 . 2) (5 . 0) (5 . 1) (5 . 2)) ((6 . 0) (6 . 1) (6 . 2) (7 . 0) (7 . 1) (7 . 2) (8 . 0) (8 . 1) (8 . 2)) ((0 . 3) (0 . 4) (0 . 5) (1 . 3) (1 . 4) (1 . 5) (2 . 3) (2 . 4) (2 . 5)) ((3 . 3) (3 . 4) (3 . 5) (4 . 3) (4 . 4) (4 . 5) (5 . 3) (5 . 4) (5 . 5)) ((6 . 3) (6 . 4) (6 . 5) (7 . 3) (7 . 4) (7 . 5) (8 . 3) (8 . 4) (8 . 5)) ((0 . 6) (0 . 7) (0 . 8) (1 . 6) (1 . 7) (1 . 8) (2 . 6) (2 . 7) (2 . 8)) ((3 . 6) (3 . 7) (3 . 8) (4 . 6) (4 . 7) (4 . 8) (5 . 6) (5 . 7) (5 . 8)) ((6 . 6) (6 . 7) (6 . 8) (7 . 6) (7 . 7) (7 . 8) (8 . 6) (8 . 7) (8 . 8)) )) ;; collect rules relative to pos (defun collect-rules (pos rules) (remove-if-not (lambda (ru) (member pos ru :test #'equal)) rules)) ;; parse text file containing question ;; and generate candidates table (defun parse-question (filename nums) (with-open-file (ifs filename :direction :input) (let* ((ta (make-hash-table :test #'equal)) (str (read-line ifs nil)) (row (read-from-string str t nil :start 0)) (clm (read-from-string str t nil :start 2)) (lst nil)) (loop for r below row do (let ((s (read-line ifs nil))) (loop for c below clm do (let ((val (read-from-string s t nil :start (* c 2)))) (if (= val 0) (setf (gethash (cons r c) ta) nums) (setf (gethash (cons r c) ta) (cons val nil))) (setq lst (cons (cons r c) lst)))))) (values row clm (reverse lst) ta)))) ;; check whether a value can be put ;; under a rule and condition (defun validp-each (val rule certain) (not (find (list val) (mapcar (lambda (key) (gethash key certain)) rule) :test #'equal))) ;; check whether a value can be put at pos ;; under the condition and rules relative to the value (defun validp (pos val rules certain) (let ((rule (collect-rules pos rules))) (every (lambda (v) (not (null v))) (mapcar (lambda (r) (validp-each val r certain)) rule)))) ;; return next candidates table (defun possible-list (certain nums poslist rules) (let ((tmphash (make-hash-table :test #'equal))) (mapc (lambda (pair) (let* ((pos (car pair)) (possibility (cdr pair)) (certain? (gethash pos certain))) (if (= 1 (list-length certain?)) (setf (gethash pos tmphash) certain?) (setf (gethash pos tmphash) possibility)))) (mapcar (lambda (pos) (cons pos (remove-if-not (lambda (n) (validp pos n rules certain)) nums))) poslist)) tmphash)) ;; count elements with only 1 possible value (defun count-certain (certain) (count-if (lambda (v) (= 1 (length v))) (loop for val being the hash-values in certain collect val))) ;; check consistency (defun inconsistentp (certain rules) (let ((values (loop for val being the hash-values in certain collect val))) (cond ((some #'null values) t) ((some (lambda (val) (> (list-length val) 1)) values) nil) ((ruleviolatedp certain rules) t) (t nil)))) (defun ruleviolatedp (certain rules) (some #'null (mapcar (lambda (val1) (let ((lst (mapcar (lambda (val2) (gethash val2 certain)) val1))) (= (list-length lst) (list-length (remove-duplicates lst :test #'equal))))) rules))) ;; check if solved or not (defun solvedp (certain rules) (and (not (ruleviolatedp certain rules)) (every (lambda (v) (= 1 (list-length v))) (loop for val being the hash-values in certain collect val)))) (defun print-solution (certain row clm) (loop for r below row do (progn (loop for c below clm do (format t "~S " (car (gethash (cons r c) certain)))) (format t "~%"))) (format t "~%")) (defun smallest-uncertain (certain num) (let ((cand (find-if (lambda (pair) (= (list-length (cdr pair)) num)) (loop for key being the hash-keys in certain using (hash-value val) collect (cons key val))))) (if cand cand (smallest-uncertain certain (+ 1 num))))) ;; solve sudoku (defun solve (certain nums poslist rules row clm solutions) (let ((sol1 solutions)) (cond ((inconsistentp certain rules) nil) ((solvedp certain rules) (setf sol1 (cons certain sol1)) ) (t (let ((next (possible-list certain nums poslist rules))) (if (< (count-certain certain) (count-certain next)) (setf sol1 (solve next nums poslist rules row clm sol1)) (let ((first-uncertain (smallest-uncertain certain 2))) (mapc (lambda (val) (progn (setf (gethash (car first-uncertain) next) (list val)) (setf sol1 (solve next nums poslist rules row clm sol1)))) (cdr first-uncertain))))))) sol1)) (defun run (pathname) (multiple-value-bind (r c l ta) (parse-question pathname *nums*) (time (mapc (lambda (s) (print-solution s r c)) (solve ta *nums* l *rules* r c nil)))))
sudoku-sample.txt 9 9 0 0 5 3 0 0 0 0 0 8 0 0 0 0 0 0 2 0 0 7 0 0 1 0 5 0 0 4 0 0 0 0 5 3 0 0 0 1 0 0 7 0 0 0 6 0 0 3 2 0 0 0 8 0 0 6 0 5 0 0 0 0 9 0 0 4 0 0 0 0 3 0 0 0 0 0 0 9 7 0 0
こんなファイルを用意して
CL_USER> (run "sudoku-sample.txt")
ってやると
1 4 5 3 2 7 6 9 8 8 3 9 6 5 4 1 2 7 6 7 2 9 1 8 5 4 3 4 9 6 1 8 5 3 7 2 2 1 8 4 7 3 9 5 6 7 5 3 2 9 6 4 8 1 3 6 7 5 4 2 8 1 9 9 8 4 7 6 1 2 3 5 5 2 1 8 3 9 7 6 4 Evaluation took: 14.176 seconds of real time 14.289376 seconds of total run time (14.124370 user, 0.165006 system) [ Run times consist of 0.356 seconds GC time, and 13.934 seconds non-GC time. ] 100.80% CPU 39,602,840,884 processor cycles 1,647,842,864 bytes consed
こんな感じに結果が出る。
答えが一つと決まってるならいいんだけど複数のあり得る答えがある場合もあるから結局全探索する事になって難しい問題だと時間がかかる。高速化を図ろうかな。