1

I'm new to Lisp and I'm trying to solve an 8-puzzle using simple dfs (depth-first search). But I am getting a program stack overflow. My code:

(setq used (list))

(defun is_used (state lst) 
  (cond
    ((null lst)   nil)
    ((equalp (car lst) state)   t) 
    (t   (is_used state (cdr lst)))))

(defun move (lst direction)
  (let* ( (zero (find_zero lst)) 
          (row  (floor zero 3)) 
          (col  (mod zero 3)) 
          (res  (copy-list lst)))
     (cond
        ((eq direction 'L) 
           (if (> col 0) 
               (rotatef (elt res zero) (elt res (- zero 1)))))
        ((eq direction 'R) 
           (if (< col 2) 
               (rotatef (elt res zero) (elt res (+ zero 1)))))
        ((eq direction 'U) 
           (if (> row 0) 
               (rotatef (elt res zero) (elt res (- zero 3)))))
        ((eq direction 'D) 
           (if (< row 2) 
               (rotatef (elt res zero) (elt res (+ zero 3))))))
     (if (equalp res lst) 
         (return-from move nil))
     (return-from move res))
  nil)

(defun dfs (cur d prev)
  ; (write (length used))
  ; (terpri)
  (push cur used)
  (let* ((ways '(L R U D)))
    (loop for dir in ways
          do (if (move cur dir)
                 (if (not (is_used (move cur dir) used))
                     (dfs (move cur dir) (+ d 1) cur))))))

state here is a list of 9 atoms.

With uncommented (write (length used)) it prints 723 - number of items in used before the stack overflow occurs.

Now, before solving 8-puzzle, I just want to iterate over all possible states (there are exactly 9! / 2 = 181440 possible states). Sure, it may take some time, but how can I avoid the stack overflow here?

2
  • What is the definition of move? Commented Apr 12, 2018 at 17:42
  • @Sylwester I've added move function to the given code Commented Apr 12, 2018 at 17:53

2 Answers 2

2

This is a typical problem explained in some AI programming books. If you need to search a large / unbounded amount of states, you should not use recursion. Recursion in CL is limited by the stack depth. Some implementations can optimize tail recursion - but then you need architecture your code, so that it is tail recursive.

Typically a data structure for that will be called an agenda. It keeps the states still to explore. If you look at a state, you push all states to explore from there onto the agenda. Make sure the agenda is in some way sorted (this might determine if it is depths or breadths first). Then take the next state from the agenda and examine it. If the goal is reached, then you are done. If the agenda is empty before the goal is found, then there is no solution. Otherwise loop...

Sign up to request clarification or add additional context in comments.

Comments

0

Your code, simplified, is

(setq *used* (list))

(defun move (position direction)
  (let* ( (zero (position 0 position)) 
          (row  (floor zero 3)) 
          (col  (mod   zero 3)) 
          (command (find direction `((L ,(> col 0) ,(- zero 1))
                                     (R ,(< col 2) ,(+ zero 1))
                                     (U ,(> row 0) ,(- zero 3))
                                     (D ,(< row 2) ,(+ zero 3)))
                         :key #'car)))
     (if (cadr command)
        (let ((res (copy-list position)))
           (rotatef (elt res zero) (elt res (caddr command)))
           res))))

(defun dfs-rec (cur_pos depth prev_pos)
  (write (length *used*)) (write '_) (write depth) (write '--)
  ; (terpri)
  (push cur_pos *used*)
  (let* ((dirs '(L R U D)))
    (loop for dir in dirs
          do (let ((new_pos (move cur_pos dir)))
               (if (and new_pos
                        (not (member new_pos *used* :test #'equalp)))
                 (dfs-rec new_pos (+ depth 1) cur_pos))))))

(print (dfs-rec '(0 1 2 3 4 5 6 7 8) 0 '()))

Instead of processing the four moves one by one while relying on recursion to keep track of what-to-do-next on each level, just push all the resulting positions at once to a to-do list, then pop and continue with the top one; repeating while the to-do list is not empty (i.e. there is something to do, literally):

(defun new-positions (position)
  (let* ( (zero (position 0 position)) 
          (row  (floor zero 3)) 
          (col  (mod   zero 3)) )
    (mapcan
         #'(lambda (command)
             (if (cadr command)
               (let ((res (copy-list position)))
                  (rotatef (elt res zero) (elt res (caddr command)))
                  (list res)))) 
         `((L ,(> col 0) ,(- zero 1))
           (R ,(< col 2) ,(+ zero 1))
           (U ,(> row 0) ,(- zero 3))
           (D ,(< row 2) ,(+ zero 3))) ))) 

; non-recursive dfs function skeleton
(defun dfs (start-pos &aux to-do curr new)
   (setf to-do (list start-pos))
   (loop while to-do
      do (progn (setf curr (pop to-do))
                (setf new (new-positions curr))
                (setf to-do (nconc new to-do)))))

This way there's no more info to keep track of, with recursion -- it's all in the to-do list.

This means the generated positions will be processed in the LIFO order, i.e. the to-do list will be used as a stack, achieving the depth-first search strategy.

If you'd instead append all the new positions on each step at the end of the to-do list, it'd mean it being used as a queue, in a FIFO order, achieving the breadth-first search.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.