背景
初学clojure,想着看一些算法来熟悉clojure语法及相关算法实现。
找到一个各种语言生成迷宫的网站:http://rosettacode.org/wiki/Maze_generation
在上述网站可以看到clojure的实现版,本文就是以初学者的视角解读改程序。
小试牛刀
先看一些简单的示例,以帮助我们理解迷宫生成程序。
绑定符号x++
(defn f [x]
(let [x++ (+ x 5)]
#{[x x++]}))
(println (f 1))
=> #'sinlov.clojure.base-learn/f
#{[1 6]}
=> nil
Tips: 上述程序将x++
绑定为x+5
,不同于c语言中的自增运算符。
集合过滤
(select odd? (set [1 2 3 4 5]))
=> #{1 3 5}
(select (partial odd?) (set [1 2 3 4 5]))
=> #{1 3 5}
select
语法参考文档:http://clojuredocs.org/clojure.set/select
partial
解释见下文
vec交叉合并
(interleave [0 1 2] ['a 'b 'c])
=> (0 a 1 b 2 c)
(interleave [0 1 2] ['a 'b 'c] ['b])
=> (0 a b)
(interleave [0 1 2] ['a 'b 'c] (repeat 'z))
=> (0 a z 1 b z 2 c z)
文档:http://clojuredocs.org/clojure.core/interleave
transduce
transducer是clojure里面的一种编程思想,使用transducer可以简化很多语法。
可以参考这篇文章链接,帮助理解
文档:http://clojuredocs.org/clojure.core/transduce
思路
笔者阅读了迷宫生成算法,将思路整理如下
坐标点与符号映射关系
比如迷宫的左上角┌
是如何生成的,不同大小的迷宫如何确定?
经过阅读源码发现,一个坐标点的符号与其周围4个临接点相关,如果按照坐标点表示,5个点排序顺序是一致的。
比如,上述坐标点(5,5),和其4个临界点。可以看到在该坐标系内,一个点与其临界点做成的集合排序一定是下面的顺序:
比如迷宫左上角坐标是(0, 0),该点五元组应该是
不在迷宫,不在迷宫,(0, 0), (0, 1), (1, 0)
假设不在迷宫或者该位置为空,记为0
;如果是墙记为1
那么上述五元组可以换算为11100
。
再比如迷宫右上角,五元组为
(n-1, 0), 不在迷宫, (n, 0), (n, 1), 不在迷宫
可换算为10110
。
按照如上规则可以生成如下表:
[" " " " " " " " "· " "╵ " "╴ " "┘ "
" " " " " " " " "╶─" "└─" "──" "┴─"
" " " " " " " " "╷ " "│ " "┐ " "┤ "
" " " " " " " " "┌─" "├─" "┬─" "┼─"]
程序代码
(ns maze.core
(:require [clojure.set :refer [intersection
select]]
[clojure.string :as str]))
;; 得到周围临界点
(defn neighborhood
([] (neighborhood [0 0]))
([coord] (neighborhood coord 1))
([[y x] r]
(let [y-- (- y r) y++ (+ y r)
x-- (- x r) x++ (+ x r)]
#{[y++ x] [y-- x] [y x--] [y x++]})))
;; 判断位置是否为空
(defn cell-empty? [maze coords]
(= :empty (get-in maze coords)))
;; 判断位置是否为墙
(defn wall? [maze coords]
(= :wall (get-in maze coords)))
;; 过滤迷宫中指定类型的点的集合
(defn filter-maze
([pred maze coords]
(select (partial pred maze) (set coords)))
([pred maze]
(filter-maze
pred
maze
(for [y (range (count maze))
x (range (count (nth maze y)))]
[y x]))))
;; 创建新迷宫
(defn create-empty-maze [width height]
(let [width (inc (* 2 width))
height (inc (* 2 height))]
(vec (take height
(interleave
(repeat (vec (take width (repeat :wall))))
(repeat (vec (take width (cycle [:wall :empty])))))))))
(defn next-step [possible-steps]
(rand-nth (vec possible-steps)))
;; 核心算法,深度优先递归
(defn create-random-maze [width height]
(loop [maze (create-empty-maze width height)
stack []
nonvisited (filter-maze cell-empty? maze)
visited #{}
coords (next-step nonvisited)]
(if (empty? nonvisited)
maze
(let [nonvisited-neighbors (intersection (neighborhood coords 2) nonvisited)]
(cond
(seq nonvisited-neighbors)
(let [next-coords (next-step nonvisited-neighbors)
wall-coords (map #(+ %1 (/ (- %2 %1) 2)) coords next-coords)]
(recur (assoc-in maze wall-coords :empty)
(conj stack coords)
(disj nonvisited next-coords)
(conj visited next-coords)
next-coords))
(seq stack)
(recur maze (pop stack) nonvisited visited (last stack)))))))
;; 迷宫坐标与字符映射
(def cell-code->str
[" " " " " " " " "· " "╵ " "╴ " "┘ "
" " " " " " " " "╶─" "└─" "──" "┴─"
" " " " " " " " "╷ " "│ " "┐ " "┤ "
" " " " " " " " "┌─" "├─" "┬─" "┼─"])
;; 获取迷宫坐标的类型
;; 使用5 bit表示一个点对应的字符映射
;; 例如:00111对应┘
(defn cell-code [maze coord]
(transduce
(comp
(map (partial wall? maze))
(keep-indexed (fn [idx el] (when el idx)))
(map (partial bit-shift-left 1)))
(completing bit-or)
0
(sort (cons coord (neighborhood coord)))))
(defn cell->str [maze coord]
(get cell-code->str (cell-code maze coord)))
;; 将迷宫坐标转换为字符
(defn maze->str [maze]
(->> (for [y (range (count maze))]
(for [x (range (count (nth maze y)))]
(cell->str maze [y x])))
(map str/join)
(str/join
ewline)))
;; 生成迷宫
(println (maze->str (create-random-maze 10 10)))
上述程序输出: