3

So this is my first time programming in clojure and I am running into some issues with stackoverflow with my program. This program basically tries to find all the possible solutions to the N-queens problem

http://en.wikipedia.org/wiki/Eight_queens_puzzle

When I call sol-count (finds number of solutions for a given N) on 10 or higher I get a stack overflow

(defn qextends?
  "Returns true if a queen at rank extends partial-sol."
  [partial-sol rank]
  (if (>= (count partial-sol) 1)
    (and
      (not= (first partial-sol) (- rank (count partial-sol)))
      (not= (first partial-sol) (+ rank (count partial-sol)))
      (not= (first partial-sol) rank)
      (qextends? (rest partial-sol) rank))
    true)
  )

(defn qextend-helper [n x partial-sol partial-sol-list]
  (if (<= x n)
    (if (qextends? partial-sol x)
      (qextend-helper n (inc x) partial-sol (conj partial-sol-list (conj partial-sol x)))
      (qextend-helper n (inc x) partial-sol partial-sol-list)
    )
  partial-sol-list)
  )


(defn qextend
  "Given a vector *partial-sol-list* of all partial solutions of length k,
  returns a vector of all partial solutions of length k + 1.
  "
  [n partial-sol-list]
  (if (>= (count partial-sol-list) 1)
    (vec (concat (qextend-helper n 1 (first partial-sol-list) [])
      (qextend n (rest partial-sol-list))))
    nil))

(defn sol-count-helper
  [n x partial-sol-list]
  (if (<= x (- n 1))
    (sol-count-helper n (+ 1 x) (qextend n partial-sol-list))
    (qextend n partial-sol-list))
  )

(defn sol-count
  "Returns the total number of n-queens solutions on an n x n board."
  [n]
  (count (sol-count-helper n 1 [[]]))
  )

2 Answers2

5

Completing dizzystar's answer:

Most of your recursions can be recurred: You can put recur inside if, and hence under and and or, macros which expand to if forms. For example ...

(defn qextend-helper [n x partial-sol partial-sol-list]
  (if (<= x n)
    (if (qextends? partial-sol x)
      (recur n (inc x) partial-sol (conj partial-sol-list (conj partial-sol x)))
      (recur n (inc x) partial-sol partial-sol-list))
    partial-sol-list)
  )

But the recursive call in qextend:

(vec (concat ( ...)
             (qextend n (rest partial-sol-list))))

... can't be dealt with by recur, since it is buried in function calls, to concat and vec.

You can solve this problem by returning a lazy sequence, so making the partial-sol-list argument lazy:

  • get rid of vec - return the sequence - and
  • replace concat with lazy-cat.

The resulting function is

(defn qextend [n partial-sol-list]
  (if (seq partial-sol-list)
    (lazy-cat (qextend-helper n 1 (first partial-sol-list) [])
            (qextend n (rest partial-sol-list)))
    nil))

You also have to avoid counting (and hence realizing) the lazy sequence: so useseq instead to test whether there is anything in partial-sol-list.

It works!

=> (sol-count 11)
2680
Thumbnail
  • 13,293
  • 2
  • 29
  • 37
  • Wow that worked really well. But one problem I have now is that qextend needs to return a vector and using just lazy-cat I end up with a sequence of the solutions. Is there a lazy-vec of some sorts that could make a vector? – Nirmal Patel Mar 09 '14 at 17:29
  • 1
    Why must 'qextend' return a vector of vectors? Why won't a sequence of vectors do? There are no lazy vectors, but you could pour the solution sequence into a vector, for example '(vec (sol-count-helper 8 1 [[]]))'. – Thumbnail Mar 09 '14 at 18:03
  • There will be other functions that rely on qextends that need it to be a vector. That answer works but sol-count returns the count so optimally I would like it to be vector returned inside qextends. – Nirmal Patel Mar 09 '14 at 18:48
  • "Completing dizzystar's answer" --- Thanks for that. One point that I didn't make is, in my experience, recur can often be reduced to something cleaner. Exploring Clojure takes considerable time, where the challenge is to always look for alternatives. – dizzystar Mar 10 '14 at 00:58
3

For starters, Clojure does not have TCO like other lisps. To mitigate this, Clojure offers loop and recur. In Clojure and other lisps, you generally do not have explicit returns, instead, the focus is on returning values.

I fixed up the qextend-helper to give you a start. I tested a few of your answers against the snippet that I replaced here, and they all resolve to the same solution. You still can't go past 10 even with this snippet inside of it, but if you continue to remove all the tail call recursions, you should be able to get past the stackoverflow errors:

(defn qextend-helper [n x partial-sol partial-sol-list]
  (loop [n n
         x x
         partail-sol partial-sol
         partial-sol-list partial-sol-list]
    (when (and (<= x n)
               (qextends? partail-sol x))
      (recur n (inc x) partail-sol (conj partial-sol-list (conj partial-sol x))))))

I should point out also that the above isn't great Clojure either. There are many other solutions online that are less LOC and more Clojure-y, but since you are starting down this route, I am simply attempting to encourage you to remove the errors that you are running into. In this case, removing the tail-calls is a good place to start. With that said, there is nothing that would stop you from using for or other constructs and I encourage you to look into other options after you solved this.

When using recur, resist the temptation to put recur at the end of a function to simulate TCO:

(defn my-funct [x]
      .....
      (recur x))

Finally, this indention style and putting parens on their own lines does not improve readability.:

      (qextend-helper n (inc x) partial-sol partial-sol-list)
    )
  partial-sol-list)
  )
dizzystar
  • 1,055
  • 12
  • 22
  • The adapted 'qextend-helper' is erroneous. And the 'loop' is redundant: the function is a legitimate and idiomatic target for 'recur'. – Thumbnail Mar 10 '14 at 08:49
  • Yes, a function is legitimate target of recur. The point is that recur to function should not be the first thing to reach for. As for the loop, it is only meant to get him started. I prefer to leave it up to the person to figure out where to go for improvements. I even say in the answer that it was not good Clojure. – dizzystar Mar 11 '14 at 04:40