Depth-first search in Clojure (tree-seq
)
tree-seq
, to solve a classic puzzle.
tree-seq, puzzle
A classic puzzle involves placing eight queens on a chessboard so that no two are attacking each other.
Today, we search out such arrangements, in Clojure.
Since no solution has two queens on the same rank, a nice way to represent the board with data is as a vector of numbers, each element of the vector a column index for the queen on that rank.
For example, the vector [0 2]
would be a board with two queens, one in the corner and another a knight’s move away.
def board [0 2]) (
We can visualize boards by converting these vectors into so-called FEN strings, which can be converted into images by a web service provided by the caring strangers at chessboardimage.com.
First, we obtain the elements of the FEN string as a sequence.
for [i board] (str i "q" (- 7 i))) (
"0q7" "2q5") (
FEN strings do not allow zeros (I do not make the rules).
for [i board] (.replace (str i "q" (- 7 i)) "0" "")) (
"q7" "2q5") (
Each rank is delimited with a slash.
->> (for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
("/")) (clojure.string/join
"q7/2q5"
That goes straight into the chessboardimage.com URL
->> (for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
("/")
(clojure.string/join format "https://chessboardimage.com/%s.png")) (
"https://chessboardimage.com/q7/2q5.png"
That is the body of a function that converts a board into an image
defn board->image
(
[board]->> (for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
("/")
(clojure.string/join format "https://chessboardimage.com/%s.png"))) (
To solve the puzzle, we build a tree of candidate solution boards, the children of each node being boards with a new queen added on the next rank to each square not under attack.
To find the squares under attack, we begin by computing the board’s ranks.
vector board) (map-indexed
0 0] [1 2]) ([
Each queen attacks up to three squares on the next rank, so for each slope m
in -1, 0, 1 and each queen’s rank and index, we produce three indexes under attack (y=mx+b
).
for [m [-1 0 1]
(vector board)]
[rank i] (map-indexed + i (* m (- (count board) rank)))) (
2 1 0 2 2 3) (-
To compute the candidate squares, we take the set of valid indexes and remove those under attack.
->> (for [m [-1 0 1]
(vector board)]
[rank i] (map-indexed + i (* m (- (count board) rank))))
(apply disj (set (range 8)))) (
7 4 6 5} #{
From those we produce a sequence of child boards.
->> (for [m [-1 0 1]
(vector board)]
[rank i] (map-indexed + i (* m (- (count board) rank))))
(apply disj (set (range 8)))
(map #(conj board %))) (
0 2 7] [0 2 4] [0 2 6] [0 2 5]) ([
That is the body of a function that takes a board, and produces child boards in the tree of candidate solutions.
defn board->children
(
[board]->> (for [m [-1 0 1]
(vector board)]
[rank i] (map-indexed + i (* m (- (count board) rank))))
(apply disj (set (range 8)))
(map #(conj board %)))) (
We can enumerate all candidate boards with Clojure’s tree-seq
; a function of three arguments, the first is a predicate that is true for nodes with children.
In our case, we keep adding queens as long as a board has fewer than eight queens.
def boards (tree-seq #(< (count %) 8) ... ...)) (
The second argument to tree-seq
is a function that given a node, produces a sequence of children.
We just wrote that function (board->children
).
def boards (tree-seq #(< (count %) 8) board->children ...)) (
The third argument to tree-seq
is the root of the tree, an empty board []
will do.
def boards (tree-seq #(< (count %) 8) board->children [])) (
The solutions to the puzzle are those boards with 8 queens on them.
def solutions (filter #(= (count %) 8) boards)) (
Of which, there are this many…
count solutions) (
92
The forty-second such solution
nth solutions 42) (
3 0 4 7 1 6 2 5] [
As an image
nth solutions 42)) (board->image (
"https://chessboardimage.com/3q4/q7/4q3/7q/1q6/6q1/2q5/5q2.png"
🙇