Skip to content

Commit c25d7d3

Browse files
Merge pull request #42 from harold/tree-seq-dfs
`[clojure/tree-seq/depth-first-search]` First draft
2 parents 40b767a + 2c4b26d commit c25d7d3

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

src/clojure/tree_seq/2q5.png

71.3 KB
Loading

src/clojure/tree_seq/5q2.png

86.7 KB
Loading
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
^{:kindly/hide-code true
2+
:clay {:title "Depth-first search in Clojure (`tree-seq`)"
3+
:quarto {:type :post
4+
:author [:harold]
5+
:date "2025-08-11"
6+
:description "Step-by-step development of a depth-first search, using `tree-seq`, to solve a classic puzzle."
7+
:image "5q2.png"
8+
:category :clojure
9+
:tags [:tree-seq :puzzle]
10+
:keywords [:tree-seq :puzzle]}}}
11+
(ns clojure.tree-seq.depth-first-search)
12+
13+
;; ![eight queens on a chessboard](5q2.png)
14+
15+
;; A [classic puzzle](https://en.wikipedia.org/wiki/Eight_queens_puzzle) involves placing eight queens on a chessboard so that no two are attacking each other.
16+
17+
;; Today, we search out such arrangements, in Clojure.
18+
19+
;; ---
20+
21+
;; 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.
22+
23+
;; For example, the vector `[0 2]` would be a board with two queens, one in the corner and another a knight's move away.
24+
25+
(def board [0 2])
26+
27+
;; We can visualize boards by converting these vectors into so-called [FEN strings](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation), which can be converted into images by a web service provided by the caring strangers at [chessboardimage.com](https://chessboardimage.com/).
28+
29+
;; First, we obtain the elements of the FEN string as a sequence.
30+
31+
(for [i board] (str i "q" (- 7 i)))
32+
33+
;; FEN strings do not allow zeros (I do not make the rules).
34+
35+
(for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
36+
37+
38+
;; Each rank is delimited with a slash.
39+
40+
(->> (for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
41+
(clojure.string/join "/"))
42+
43+
;; That goes straight into the chessboardimage.com URL
44+
45+
(->> (for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
46+
(clojure.string/join "/")
47+
(format "https://chessboardimage.com/%s.png"))
48+
49+
;; ![two queens on a chessboard](2q5.png)
50+
51+
;; That is the body of a function that converts a board into an image
52+
53+
(defn board->image
54+
[board]
55+
(->> (for [i board] (.replace (str i "q" (- 7 i)) "0" ""))
56+
(clojure.string/join "/")
57+
(format "https://chessboardimage.com/%s.png")))
58+
59+
;; ---
60+
61+
;; 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.
62+
63+
;; To find the squares under attack, we begin by computing the board's ranks.
64+
65+
(map-indexed vector board)
66+
67+
;; 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`).
68+
69+
(for [m [-1 0 1]
70+
[rank i] (map-indexed vector board)]
71+
(+ i (* m (- (count board) rank))))
72+
73+
;; To compute the candidate squares, we take the set of valid indexes and remove those under attack.
74+
75+
(->> (for [m [-1 0 1]
76+
[rank i] (map-indexed vector board)]
77+
(+ i (* m (- (count board) rank))))
78+
(apply disj (set (range 8))))
79+
80+
;; From those we produce a sequence of child boards.
81+
82+
(->> (for [m [-1 0 1]
83+
[rank i] (map-indexed vector board)]
84+
(+ i (* m (- (count board) rank))))
85+
(apply disj (set (range 8)))
86+
(map #(conj board %)))
87+
88+
;; That is the body of a function that takes a board, and produces child boards in the tree of candidate solutions.
89+
90+
(defn board->children
91+
[board]
92+
(->> (for [m [-1 0 1]
93+
[rank i] (map-indexed vector board)]
94+
(+ i (* m (- (count board) rank))))
95+
(apply disj (set (range 8)))
96+
(map #(conj board %))))
97+
98+
;; ---
99+
100+
;; 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.
101+
102+
;; In our case, we keep adding queens as long as a board has fewer than eight queens.
103+
104+
^{:kindly/hide-code true} (def ... nil)
105+
106+
(def boards (tree-seq #(< (count %) 8) ... ...))
107+
108+
;; The second argument to `tree-seq` is a function that given a node, produces a sequence of children.
109+
110+
;; We just wrote that function (`board->children`).
111+
112+
(def boards (tree-seq #(< (count %) 8) board->children ...))
113+
114+
;; The third argument to `tree-seq` is the root of the tree, an empty board `[]` will do.
115+
116+
(def boards (tree-seq #(< (count %) 8) board->children []))
117+
118+
;; The solutions to the puzzle are those boards with 8 queens on them.
119+
120+
(def solutions (filter #(= (count %) 8) boards))
121+
122+
;; Of which, there are this many...
123+
124+
(count solutions)
125+
126+
;; The forty-second such solution
127+
128+
(nth solutions 42)
129+
130+
;; As an image
131+
132+
(board->image (nth solutions 42))
133+
134+
;; ![eight queens on a chessboard](5q2.png)
135+
136+
;; 🙇

0 commit comments

Comments
 (0)