4.12 新的問題來了:猴子和香蕉問題
為了顯示我們的GPS真的是通用的,需要在不同的問題中檢驗。我們來看一看一個經典的AI問題,最初是由Saul Amarel在1968年提出的。想象下面的場景:一只饑餓的猴子正站在一個房間的門口,在房子的正中間房頂上,用繩子吊著一串香蕉,剛剛好高度是猴子夠不著的。在門附近有一把椅子,足夠輕便,猴子推得動,也足夠高度,猴子可以站在上面拿到香蕉。為了讓事情再復雜些,假設猴子手里拿著一個玩具球,而且規定一次只能在手里持一件東西。
在嘗試表現這樣的場景的時候,我們可以靈活選擇把什么放到當前狀態,把什么放進操作符。假設我們如此定義操作符:
(defparameter *banana-ops*
?(list
? ?(op ‘climb-on-chair
? ? ?:preconds ‘(chair-at-middle-room at-middle-room on-floor)
? ? ?:add-list ‘(at-bananas on-chair)
? ? ?:del-list ‘(at-middle-room on-floor))
? ?(op ‘push-chair-from-door-to-middle-room
? ? ?:preconds ‘(chair-at-door at-door)
? ? ?:add-list ‘(chair-at-middle-room at-middle-room)
? ? ?:del-list ‘(chair-at-door at-door))
? ?(op ‘walk-from-door-to-middle-room
? ? ?:preconds ‘(at-door on-floor)
? ? ?:add-list ‘(at-middle-room)
? ? ?:del-list ‘(at-door))
? ?(op ‘grasp-bananas
? ? ?:preconds ‘(at-bananas empty-handed)
? ? ?:add-list ‘(has-banans)
? ? ?:del-list ‘(empty-handed))
? ?(op ‘drop-ball
? ? ?:preconds ‘(has-ball)
? ? ?:add-list ‘(empty-handed)
? ? ?:del-list ‘(has-ball))
? ?(op ‘eat-banans
? ? ?:preconds ‘(has-bananas)
? ? ?:add-list ‘(empty-handed not-hungry)
? ? ?:del-list ‘(has-bananas hungry))))
使用這些運算符,可以展現的問題就是讓猴子不再饑餓,給定的初始狀態,在門口,站在地板上,拿著球,饑餓,在門口有把椅子。GPS可以找到一個這個問題的解決方案:
> (use *banana-ops*) => 6
?> (GPS '(at-door on-floor has-ball hungry chair-at-door)
'(not-hungry) )
((START)
(EXECUTING PUSH-CHAIR-FROM-DOOR-TO-MIDDLE-ROOM)
(EXECUTING CLIMB-ON-CHAIR)
(EXECUTING DROP-BALL)
(EXECUTING GRASP-BANANAS)
(EXECUTING EAT-BANANAS))
這里沒有對原來的GPS程序作出任何的改動,僅僅是使用了一個新的操作符集合。
4.13 迷宮搜索問題
現在我們來看看另一個經典問題,迷宮搜索。我們假定有一個迷宮,如下圖所示:
定義一些函數來幫助構建針對問題的操作符,比直接定義一大串操作符要容易得多。下面的代碼定義了一般的迷宮操作符集合,以及這個特定迷宮:
(defun make-maze-ops (pair)
? “Make maze ops in both directions”
? (list (make-maze-op (first pair) (second pair))
? ? (make-maze-op (second pair) (first pair))))
(defun make-maze-op (here there)
?“Make an operator to move between two places”
?(op ‘(move from ,here to ,there)
? ?:preconds ’((at ,here))
? ? ?:add-list ‘((at ,there))
? ?:del-list ’((at ,here))))
(defparameter *maze-ops*
?(mappend #’make-maze-ops
? ?‘((1 2) (2 3) (3 4) (4 9) (9 14) (9 8) (8 7) (7 12) (12 13)
? ?(12 11) (11 6) (11 16) (16 17) (17 22) (21 22) (22 23)
? ?(23 18) (23 24) (24 19) (19 20) (20 15) (15 10) (10 5) (20 25))))
現在我們可以使用操作符的列表來解決這個迷宮的一些問題。我們也可以通過給定的連接的列表來創建另一個迷宮。注意沒有證據表明迷宮的形式必須是55一層的,他僅僅是一種連接具象化的表示方式。
> (use *maze-ops*) => 48
> (gps '((at 1)) '((at 25)))
((START)
(EXECUTING (MOVE FROM 1 TO 2))
(EXECUTING (MOVE FROM 2 TO 3))
(EXECUTING (MOVE FROM 3 TO 4))
(EXECUTING (MOVE FROM 4 TO 9))
(EXECUTING (MOVE FROM 9 TO 8))
(EXECUTING (MOVE FROM 8 TO 7))
(EXECUTING (MOVE FROM 7 TO 12))
(EXECUTING (MOVE FROM 12 TO 11))
(EXECUTING (MOVE FROM 11 TO 16))
(EXECUTING (MOVE FROM 16 TO 17))
(EXECUTING (MOVE FROM 17 TO 22))
(EXECUTING (MOVE FROM 22 TO 23))
(EXECUTING (MOVE FROM 23 TO 24))
(EXECUTING (MOVE FROM 24 TO 19))
(EXECUTING (MOVE FROM 19 TO 20))
(EXECUTING (MOVE FROM 20 TO 25))
(AT 25))
迷宮問題指出了一個隱秘的小bug。我們要GPS返回的是一系列執行的操作的列表。然而,因為考慮到目標達成也可以沒有任何操作,所以,在GPS返回值中加上了(START)。這些例子包含了START和EXECUTING形式,但是也包含了一系列的(AT n)。這就是bug。如果我們回過頭看看函數GPS,就會發現他的結果是根據achieve-all返回的狀態刪除所有的原子得來的。這是一個雙關,我們所說的移除原子就是移除除了(start),(executing action)之外的形式。到現在為止,所有的這些條件都是原子,所以方法可用。迷宮問題是從形式(AT n)中引入條件,所以首先這就是個問題。基本的精神就是當一個程序員使用雙關的時候,也就是將真正發生的事情用比較通俗的話來說,就會出現一些麻煩。我們真正想要做的不是刪除原子,而是找到所有知識操作的元素。下面的代碼就是主要的意思:
(defun GPS (state goals &optional (*ops* *ops*))
?“General Problem Solver: from state, achieve goals using *ops*.”
?(find-all-if #’action-p
? ?(achieve-all (cons ‘(start) state) goals nil)))
(defun action-p (x)
?“Is x something that is (start) or (executing …)?”
?(or (equal x ‘(start)) (executing-p x)))
迷宮問題也顯示出了一個版本2的優點:就是他會返回所做的操作的表現,而不僅僅是打印他們。這樣子就可以利用結果來進行一些處理,而不是僅僅看看而已。假設我們想要一個函數,給我們一個穿越迷宮的路徑,用一系列的位置表示。我們可以將GPS作為一個子函數調用,之后再操作結果。
(defun find-path (start end)
?“Search a maze for a path from start to end.”
?(let ((results (GPS ‘((at ,start)) ’((at ,end)))))
? ?(unless (null results)
? ? ?(cons start (mapcar #’destination
? ? ? ?(remove ‘(start) results
? ? ? ? ?:test #’equal))))))
(defun destination (action)
?“Find the Y in (executing (move from X to Y))”
?(fifth (second action)))
函數find-path調用GPS來獲取results。如果是nil就沒有答案,如果不是nil就會提取results的rest部分(也就是移除start)。從每一個形式(executing (move from x to y))中挑選出目的地,y,并且記住起始點。
> (use *maze-ops*) => 48
?> (find-path 1 25) =>
(1 2 3 4 9 8 7 12 11 16 17 22 23 24 19 20 25)
> (find-path 1 1) => (1)
> (equal (find-path 1 25) (reverse (find-path 25 1))) => T