0

I'm trying to implement heuristic search strategy A* to the puzzle "8-puzzle" in Lisp.

To run my search I use the command: (run-best '(0 1 2 3 4 5 6 B 7) '(0 1 2 3 4 5 6 7 B))

Where the first state is the start goal and the second is the end goal.

However, I end up with my program running for a long time. Eventually, I assume it will stack-overflow. *Edit: It does not run out of memory however it took 30 minutes, much longer then my Breadth first search.

Search algorithm code:

;;; This is one of the example programs from the textbook:
;;;
;;; Artificial Intelligence: 
;;; Structures and strategies for complex problem solving
;;;
;;; by George F. Luger and William A. Stubblefield
;;;
;;; Corrections by Christopher E. Davis (chris2d@cs.unm.edu)
;;; insert-by-weight will add new child states to an ordered list of 
;;; states-to-try.  
(defun insert-by-weight (children sorted-list)
  (cond ((null children) sorted-list)
        (t (insert (car children) 
           (insert-by-weight (cdr children) sorted-list)))))

(defun insert (item sorted-list)
  (cond ((null sorted-list) (list item))
        ((< (get-weight item) (get-weight (car sorted-list)))
         (cons item sorted-list))
        (t (cons (car sorted-list) (insert item (cdr sorted-list))))))


;;; run-best is a simple top-level "calling" function to run best-first-search

(defun run-best (start goal)
  (declare (special *goal*)
           (special *open*)
           (special *closed*))
  (setq *goal* goal)
  (setq *open* (list (build-record start nil 0 (heuristic start))))
  (setq *closed* nil)
  (best-first))

;;; These functions handle the creation and access of (state parent) 
;;; pairs.

(defun build-record (state parent depth weight) 
  (list state parent depth weight))

(defun get-state (state-tuple) (nth 0 state-tuple))

(defun get-parent (state-tuple) (nth 1 state-tuple))

(defun get-depth (state-tuple) (nth 2 state-tuple))

(defun get-weight (state-tuple) (nth 3 state-tuple))

(defun retrieve-by-state (state list)
  (cond ((null list) nil)
        ((equal state (get-state (car list))) (car list))
        (t (retrieve-by-state state (cdr list)))))


;; best-first defines the actual best-first search algorithm
;;; it uses "global" open and closed lists.

(defun best-first ()
  (declare (special *goal*)
           (special *open*)
           (special *closed*)
           (special *moves*))
  (print "open =") (print *open*)
  (print "closed =") (print *closed*)
  (cond ((null *open*) nil)
        (t (let ((state (car *open*)))
             (setq *closed* (cons state *closed*))
             (cond ((equal (get-state state) *goal*) (reverse (build-solution *goal*)))
                   (t (setq *open* 
                            (insert-by-weight 
                                    (generate-descendants (get-state state)
                                                          (1+ (get-depth state))
                                                          *moves*)
                                    (cdr *open*)))
                      (best-first)))))))


;;; generate-descendants produces all the descendants of a state

(defun generate-descendants (state depth moves)
  (declare (special *closed*)
           (special *open*))
  (cond ((null moves) nil)
        (t (let ((child (funcall (car moves) state))
                 (rest (generate-descendants state depth (cdr moves))))
             (cond ((null child) rest)
                   ((retrieve-by-state child rest) rest)
                   ((retrieve-by-state child *open*) rest)
                   ((retrieve-by-state child *closed*) rest)
                   (t (cons (build-record child state depth 
                                          (+ depth (heuristic child))) 
                            rest)))))))


(defun build-solution (state)
  (declare (special *closed*))
  (cond ((null state) nil)
        (t (cons state (build-solution 
                        (get-parent 
                         (retrieve-by-state state *closed*)))))))

Heuristic function for 8puzzle:

(defun hole (grid)
  "Return integer index into GRID at which the 'hole' is located."
  (position '0 grid))

(defun col (pair)
  (car pair))

(defun row (pair)
  (cdr pair))

(defun coords (index1)
  "Transform INDEX, an integer index into the list, into an (X . Y)
coordinate pair for a 3x3 grid."
  (cons (second (multiple-value-list (floor index1 3)))
    (floor index1 3)))

(defun index1 (coords)
  "Transform COORDS, an (X . Y) coordinate pair for a 3x3 grid, into
an integer index."
  (+ (col coords)
     (* 3 (row coords))))

(defun swap (a b list)
  "Return a new list equivalent to LIST but with the items at indexes
A and B swapped."
  (let ((new (copy-seq list)))
    (setf (nth a new)
      (nth b list))
    (setf (nth b new)
      (nth a list))
    new))

(defun right1 (grid)
  "Move the 'hole' on the 3x3 GRID one space to the right.  If there
is no space to the right, return NIL."
  (let ((hole (coords (hole grid))))
    (if (= 2 (col hole))
    nil
    (swap (index1 hole)
          (index1 (cons (1+ (col hole)) (row hole)))
          grid))))

(defun left1 (grid)
  "Move the 'hole' on the 3x3 GRID one space to the left.  If there
is no space to the left, return NIL."
  (let ((hole (coords (hole grid))))
    (if (zerop (col hole))
    nil
    (swap (index1 hole)
          (index1 (cons (1- (col hole)) (row hole)))
          grid))))

(defun up (grid)
  "Move the 'hole' on the 3x3 GRID one space up.  If there is no space
up, return NIL."
  (let ((hole (coords (hole grid))))
    (if (zerop (row hole))
    nil
    (swap (index1 (cons (col hole) (1- (row hole))))
          (index1 hole)
          grid))))

(defun down (grid)
  "Move the 'hole' on the 3x3 GRID one space down.  If there is no
space down, return NIL."
  (let ((hole (coords (hole grid))))
    (if (= 2 (row hole))
    nil
    (swap (index1 (cons (col hole) (1+ (row hole))))
          (index1 hole)
          grid))))

;Moves
(setq *moves*
  '(right1 left1 up down))

;heuristics for puzzle8
 (defun heuristic (state)
  (declare (special *goal*))
  (heuristic-eval state *goal*))

 (defun heuristic-eval (state goal)
  (cond ((null state) 0)
        ((equal (car state) (car goal)) 
        (heuristic-eval (cdr state) (cdr goal)))
        (t (1+ (heuristic-eval (cdr state) (cdr goal))))))
Asia x3
  • 606
  • 2
  • 16
  • 37
  • My search actually finished! (After 30mins..) Any idea on how to correct this? – Asia x3 Oct 19 '15 at 05:13
  • 4
    Side note: use `defstruct` or `defclass` instead of lists for the state – coredump Oct 19 '15 at 06:54
  • 3
    I would remove the various recursion and use normal iteration instead. – Rainer Joswig Oct 19 '15 at 08:51
  • 3
    also see MERGE and FIND... – Rainer Joswig Oct 19 '15 at 08:52
  • @coredump I researched on defstruct and defclass, and I'm still very lost on what I need to do.. Do you mind explaining a bit more? Sorry :( – Asia x3 Oct 19 '15 at 08:57
  • 2
    `(defstruct srecord state parent depth weight)` – Rainer Joswig Oct 19 '15 at 09:00
  • 2
    @Asiax3 With the code provided by Rainer Joswig, accessors are already generated for you. You don't need to write all the `get-*` functions. Also, you can make new states with `(make-srecord :state s ...)`. And setf functions like `(setf (srecord-parent state) p)` are defined too, though you don't seem to mutate slots. This will simplify your code. But the good point of your code was that you provided a functional interface which is better than writing `(nth ...)` everywhere you use a particular slot. Not everyone does this. – coredump Oct 19 '15 at 09:19
  • You do not need to manually translate the coordinates. Lisp supports multidimensional arrays directly. – Svante Oct 19 '15 at 13:33
  • @RainerJoswig Thank you coredump & Rainer for the help! I been trying to fix this issue for a while now and I'm having a really hard time figuring out how to use your code Rainer Joswig to fix my program. Sorry for all the trouble :( – Asia x3 Oct 19 '15 at 13:37
  • Your insert function is already pretty much in the standard as [**merge**](http://www.lispworks.com/documentation/HyperSpec/Body/f_merge.htm). E.g., **(insert item list)** is pretty much **(merge (list item) list :key 'get-weight)**. – Joshua Taylor Oct 19 '15 at 14:02
  • Parts of the code are actually from here: https://www.cs.unm.edu/~luger/ai-final/code/LISP.best.html – Rainer Joswig Oct 19 '15 at 14:06
  • Oh yes, that's the code I'm working off of! – Asia x3 Oct 19 '15 at 14:14
  • @Asiax3 You really should have mentioned that. Posting code that you didn't write, but letting it appear as your own, is something that you should not do. There could be copyright and licensing issues. For instance, did you obtain permission from the original author before posting the code here? – Joshua Taylor Oct 19 '15 at 15:02
  • @JoshuaTaylor I apologize, I do have the code cited in my editor. Post edited :) – Asia x3 Oct 19 '15 at 18:45

2 Answers2

2

Problems in the code:

  • recursion. write loops to avoid stack overflows

  • possibly long open and closed lists. The open and closed lists can be quite long. One operation is to check if there is a record with a certain state on the lists. I would use a hash-table to record the states and then use the table to check whether a state exists.

My version of the code

No solution:

CL-USER 220 > (time (run-best '(0 1 2 3 4 5 6 7 8)
                              '(0 2 1 3 4 5 6 7 8)
                              '(right1 left1 up down)))
Timing the evaluation of (RUN-BEST (QUOTE (0 1 2 3 4 5 6 7 8))
                                   (QUOTE (0 2 1 3 4 5 6 7 8))
                                   (QUOTE (RIGHT1 LEFT1 UP DOWN)))

User time    =  0:01:05.620
System time  =        0.220
Elapsed time =  0:01:05.749
Allocation   = 115386560 bytes
22397 Page faults
NO-SOLUTION

Solution:

CL-USER 223 > (time (pprint (run-best '(2 1 5 3 4 6 0 8 7)
                                      '(0 1 2 3 4 5 6 7 8)
                                      '(right1 left1 up down))))
Timing the evaluation of (PPRINT (RUN-BEST (QUOTE (2 1 5 3 4 6 0 8 7))
                                           (QUOTE (0 1 2 3 4 5 6 7 8))
                                           (QUOTE (RIGHT1 LEFT1 UP DOWN))))

((2 1 5 3 4 6 0 8 7)
 (2 1 5 0 4 6 3 8 7)
 (2 1 5 4 0 6 3 8 7)
 (2 0 5 4 1 6 3 8 7)
 (0 2 5 4 1 6 3 8 7)
 (4 2 5 0 1 6 3 8 7)
 (4 2 5 1 0 6 3 8 7)
 (4 2 5 1 6 0 3 8 7)
 (4 2 5 1 6 7 3 8 0)
 (4 2 5 1 6 7 3 0 8)
 (4 2 5 1 0 7 3 6 8)
 (4 2 5 1 7 0 3 6 8)
 (4 2 0 1 7 5 3 6 8)
 (4 0 2 1 7 5 3 6 8)
 (0 4 2 1 7 5 3 6 8)
 (1 4 2 0 7 5 3 6 8)
 (1 4 2 3 7 5 0 6 8)
 (1 4 2 3 7 5 6 0 8)
 (1 4 2 3 0 5 6 7 8)
 (1 0 2 3 4 5 6 7 8)
 (0 1 2 3 4 5 6 7 8))
User time    =        0.115
System time  =        0.001
Elapsed time =        0.103
Allocation   = 2439744 bytes
194 Page faults
Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
  • You won't believe this... I was using incorrect start and goal states..! I was using something like (0 1 2 3 4 5 6 7 B), representing B as the blank space as opposed to just having 0. Right after seeing this comment. Now I successfully implemented defstruct as well and everything is working beautiful! :D – Asia x3 Oct 20 '15 at 03:32
0

Try the memoize utility. You can find a related question here (How do I memoize a recursive function in Lisp?). Memoize keeps track of the calls made to any memoized function and immediately returns any known (previously calculated) results to avoid recalculating them. The results in the case of a recursive function like yours are spectacular.

Community
  • 1
  • 1
Leo
  • 1,869
  • 1
  • 13
  • 19
  • Hi Leo, I tried to utilize the memorize utility but I must be memorizing the wrong function. I tried to use it on functions "herusitic-eval" and "best-first". – Asia x3 Oct 19 '15 at 20:56