3.9 宣教師と人食い人種問題

面倒になってきたのでコーディング部分だけまとめることにした。掃除機問題のバリエーションはいまいちなので見送り。


次は順に考えるのが面倒くさいことで有名な宣教師問題である。色々な資料を見ると細かいバリエーションがあるのがわかるが、今回はもっとも単純なタイプを考える。

問題

M人の宣教師(missionary)とC人の食人種(cannibal)が川の同じ岸にいる。その岸にはボートがあるが、最大2人までしか乗ることができない。ところで、宣教師より食人種が多くいるとき、食人種は宣教師を食べてしまう。N=3、M=3のとき、全員無事に向こう岸に渡るにはどのようにすればよいだろうか。

状態空間

このケースでは宣教師同士、食人種同士の区別がなく、人数の出入りもない。そのため、状態を記述するには、こちら岸のボートの有無、宣教師、食人種の人数を考えれば良い。向こう岸にいる人数は引き算で求められる。


それぞれの場所の人数は、宣教師、食人種それぞれ0〜合計人数だから、取りうる状態数は2MC。条件を加味すれば、状態空間はさらに狭くなる。それを求める関数とその結果は次の通り。

(defun all-state (m c &aux l)
  (nset-difference
   (dotimes (x (1+ m) l)
     (dotimes (y (1+ c))
       (when (and (or (= x 0) (>= x y))
                  (or (= x m) (>= (- m x) (- c y))))
         (setq l (cons (list t x y) (cons (list nil x y) l))))))
   (list (list t 0 0) (list nil m c)) :test 'equal))

(format t "~{|~A~%~}" (all-state 3 3))
|(t 3 3)
|(t 3 2)
|(nil 3 2)
|(t 3 1)
|(nil 3 1)
|(t 3 0)
|(nil 3 0)
|(t 2 2)
|(nil 2 2)
|(t 1 1)
|(nil 1 1)
|(t 0 3)
|(nil 0 3)
|(t 0 2)
|(nil 0 2)
|(t 0 1)
|(nil 0 1)
|(nil 0 0)

状態空間はこれだけだ。実際には初期状態から達成できない状態もあるだろうから、もう少し減るかも知れない。同じ状態を繰り返すことは無意味なので、解の手順はこの合計数以下になる。

解法

状態の遷移はボートに乗れる組合せが(1 0)(0 1)(1 1)(2 0)(0 2)だけなので、これらに対して次の状態が正当かどうかを確認すればよい。また、続けて同じ人数で動かすとすぐ元の状態に戻るので、状態の繰り返しは避けるようにする。探索方針としては、最短手を尽くし、解がなければ手数を増やして再探索する反復深化探索を行う。ということで、作ってみたコードを以下に置く。

(defun next-step (w v)
  (let ((f (if (car w) #'- #'+)))
    (cons (not (car w)) (mapcar (lambda (x y) (funcall f x y)) (cdr w) v))))

(defun mc-problem (m c)
  (let* ((states (all-state m c))
         (mover '((1 0)(0 1)(1 1)(2 0)(0 2)))
         (mnum (length mover))
         (sarg (list (list t m c)))
         (marg (list 0))
         (depth 1)
         (max 0)
         (goal '(nil 0 0)))
    (or
     (while (>= (+ max 3) depth)
       (let ((cur (next-step (car sarg) (elt mover (car marg)))))
         (if (equal goal cur) (return (cons cur sarg)))
         (if (and (< (length marg) depth)
                  (not (find cur sarg :test 'equal))
                  (find cur states :test 'equal))
             (progn (push cur sarg) (push 0 marg)
               (setq max (max max (length marg))) (do-events))
           (while (<= mnum (incf (car marg)))
             (setq sarg (cdr sarg)
                   marg (cdr marg))
             (unless (car marg)
               (incf depth 2)
               (setq sarg (list (list t m c))
                     marg (list 0))
               (return))))))
     (list nil 'impossible))))

(format t "~{~A ~A~%~}" (reverse (mc-problem 3 3)))
|(t 3 3) (nil 2 2)
|(t 3 2) (nil 3 0)
|(t 3 1) (nil 1 1)
|(t 2 2) (nil 0 2)
|(t 0 3) (nil 0 1)
|(t 1 1) (nil 0 0)