Scheme來實現八皇后問題(1)
版權申明:本文為博主窗戶(Colin Cai)原創,歡迎轉帖。如要轉貼,必須註明原文網址 http://www.cnblogs.com/Colin-Cai/p/9768105.html 作者:窗戶 QQ/微信:6679072 E-mail:[email protected]
看到有人寫八皇后,那我就也寫寫這個吧。
八皇后問題
這個問題大家應該都不陌生,很多計算機教程都以八皇后為例題。
上面是一個國際象棋棋盤,總共8X8個格子。
皇后是國際象棋裡殺力最強的子,它可以吃掉同一條橫線、豎線上其他棋子,也可以吃掉所在的兩條斜線上的其他棋子(當然在角上只有一條斜線)。
能否在棋盤上放更多的皇后,讓彼此之間不能互相吃到?基於很顯然一行或者一列最多隻有一個皇后,那麼這個8X8的棋盤是否可以放8個皇后?
解的表示
8個皇后的表示可以用座標,那麼就是8個座標的集合,其中行、列都是範圍1~8的數字。
考慮到每一行都只有一個,我們完全可以用讓8個皇后按照行座標進行從小到大排序,那麼必然8個皇后的行座標分別是1、2、3、4、5、6、7、8,於是這都是無用的資訊。又因為只有8列,而且任意兩個皇后都不能同列,從而每一列也有且只有一個,從而剛才排序之後的8個皇后的縱座標序列是1、2、3、4、5、6、7、8的一個排列。於是每一種可行的解對應著1、2、3、4、5、6、7、8的一個排列。
考慮更一般的情況,n皇后問題:nXn的棋盤上放n個皇后,要求彼此之間不互相吃。那麼它的每一個解對應著1~n的一個排列。
解法框架
一種做法就是先找到1~n的所有排列,然後篩選符合條件的結果。
那麼利用filter運算元最終程式碼很容易給出:
(define (queen n) (filter valid? (P n) ) )
這裡的(P n)是所有的1~n排列的集合,這裡排列當然用list來表示,集合也用list來表示。
集合的每個元素是沒有序的關係,所以邏輯上表示集合的list我們應該忽略其各個元素的序的差別。
比如(P 2)表示的是'((1 2) (2 1)),或者是'((2 1) (1 2)),無論哪種實現,都是可行的。
valid?是個謂詞函式(返回bool值的函式),它的作用是對於某個具體排列,判斷其表示的n個皇后有沒有互相吃的情況:
如果有兩個皇后互相吃,那麼這個排列不可以作為最後的解,應當返回假,Scheme裡也就是#f;
如果不存在兩個皇后互相吃,那麼這個排列可以作為最後的皆,從而應當返回真,Scheme裡也就是#t。
filter運算元就是使用valid?這樣的謂詞函式來過濾後面的集合,
比如(filter even? '(1 2 3 4 5 6 7 8 9 10))就是抓取其中為偶數的元素組成的集合,那麼當然返回'(2 4 6 8 10)。
filter這麼常用的運算元似乎並未出現在r5rs中,很奇怪,我在這裡就給出一個實現如下:
(define (filter boolf set) (cond ((null? set) '()) ((boolf (car set)) (cons (car set) (filter boolf (cdr set)))) (else (filter boolf (cdr set))) ) )
接下去就是P函式和valid?函式的實現。
全排列
第一個問題就是要解決1~n的所有排列,可能會有人考慮將所有的排列用字典排序依次輸出。
不過這一般是迭代的思想,而對於一種Lisp,我們第一反應一般是遞迴。
假設我們已經有1~n-1的全排列了,那麼我們怎麼得到1~n的全排列呢?
我們可以取1~n-1的一個排列,不妨用字母標註
a1 a2 ... an-1
我們希望找個位置插入n,得到新的1~n的排列。
這個插入點一共有n個,分別為:
a1之前
a1和a2之間
a2和a3之間
...
an-2和an-1之間
an-1之後
從而可以得到n個1~n的排列。
而對1~n-1的所有排列都這麼做,則構成了1~n的所有排列,且不存在重複。
比如1~2的所有排列組成的集合為
((1 2) (2 1))
現在我們要用它生成1~3的全排列
對於(1 2),有3個插入點,插入3,得到三個排列
(3 1 2) (1 3 2) (1 2 3)
對於(2 1),有3個插入點,插入3,得到三個排列
(3 2 1) (2 3 1) (2 1 3)
以上6個排列組成的集合就是我們所需要的結果。
首先,當然要建立一個往列表某個位置插值的函式list-insert,帶三個引數,將列表lst的位置pos插入v。而對於位置的解釋是,列表頭之前的位置稱為0,然後依次增加。比如(1 2 3)的位置1插入4,得到列表(1 4 2 3)。這個很容易用遞迴設計出來,如下:
(define (list-insert lst pos v) (if (zero? pos) (cons v lst) (cons (car lst) (list-insert (cdr lst) (- pos 1) v)) ) )
上述(list-insert '(1 2 3) 1 4),運算返回'(1 4 2 3)
按照上面的遞迴思想,我們使用map運算元先寫一點測試測試,我們希望從1~2的全排列推到1~3的全排列
(map
(lambda (x) (map (lambda (m) (list-insert x m 3)) '(0 1 2))) ;對於每個排列,給出0、1、2三個位置插入3
'((1 2)(2 1))
)
結果為
'(((3 1 2) (1 3 2) (1 2 3)) ((3 2 1) (2 3 1) (2 1 3)))
這很像我們所要的,但似乎又不是,因為我們需要應該是'((3 1 2) (1 3 2) (1 2 3) (3 2 1) (2 3 1) (2 1 3))
實際上,(apply append '(((3 1 2) (1 3 2) (1 2 3)) ((3 2 1) (2 3 1) (2 1 3))))就是我們需要的結果了。
而apply是把最後一個引數(這個引數一定要是i列表)展開。
於是上述就成了(append '((3 1 2) (1 3 2) (1 2 3)) '((3 2 1) (2 3 1) (2 1 3)) ),當然就是我們需要的結果了。
而只有1個元1的全排列集合就是'((1)),這是遞迴的邊界,
結合上述,全排列的函式定義應該如下:
(define (P n) (if (= n 1) '((1)) (apply append (map (lambda (x) (map (lambda (m) (list-insert x m n)) (range 0 n))) (P (- n 1)) ) ) ) )
判斷合法
目前只剩下valid?函式的實現了。實際上,在我們開始採用用1~n排序來作為最後的解的時候,已經把棋盤中同行同列的情況給排除了。於是,valid?函式實際上是要判斷是否有兩個棋子在同一個斜線上。
比如'(1 3 6 4 2 5 8 7)表示如圖的八個皇后,皇后的位置被打了紅圈
其中存在著皇后互吃,
在資料上看,'(1 3 6 4 2 5 8 7),其中
1和4相差3,距離也為3(1在列表的第0個位置,4在列表的第3個位置,所以距離為3);
3和8相差5,距離也為5;
8和7相差1,距離也為1。
對應著上面三對互吃的皇后。
我們這裡可以用迭代來完成,這有點類似於過程式語言的迴圈了。
從左到右先距離為1的,看看有沒有值也相差1的,如果有,那麼valid?返回假,也就是#f
然後從左到右再掃距離為2的....
...
最後當距離到n的時候,直接返回真,也就是#f(因為最左邊和最右邊距離達到,也就是n-1,此時代表所有可能都已掃過)
(define (_valid? x left-pos distance) (cond ;當距離以及達到列表長度了,掃完了,返回真 ((= distance (length x)) #t) ;如果發現差值等於距離,這一對皇后互吃,返回假 ((= distance (abs (- (list-ref x left-pos) (list-ref x (+ left-pos distance))))) #f) ;如果這個距離還沒掃完,那麼往後推一個掃 ((< (+ left-pos distance) (- (length x) 1)) (_valid? x (+ left-pos 1) distance)) ;否則,這個距離的已經掃完,距離加1,從最左邊開始掃 (else (_valid? x 0 (+ distance 1))) ) )
用它實現valid?,初始的時候,從left-pos為0,distance為1的一對皇后開始掃起
(define (valid? x) (_valid? x 0 1) )
執行
我們就拿8個皇后來測試一下,計算(queen 8)
得到
((4 7 3 8 2 5 1 6) (3 6 4 2 8 5 7 1) (3 5 2 8 6 4 7 1) (6 3 7 2 4 8 1 5) (3 6 8 2 4 1 7 5) (3 7 2 8 6 4 1 5) (3 5 2 8 1 7 4 6) (6 3 7 2 8 5 1 4) (3 6 2 7 5 1 8 4) (3 6 2 5 8 1 7 4) (7 3 8 2 5 1 6 4) (3 7 2 8 5 1 4 6) (3 6 2 7 1 4 8 5) (4 2 7 3 6 8 5 1) (4 2 7 3 6 8 1 5) (5 2 4 6 8 3 1 7) (5 2 4 7 3 8 6 1) (2 4 6 8 3 1 7 5) (5 7 2 6 3 1 8 4) (5 7 2 6 3 1 4 8) (8 2 5 3 1 7 4 6) (2 7 3 6 8 5 1 4) (7 2 6 3 1 4 8 5) (2 6 8 3 1 4 7 5) (4 7 5 2 6 1 3 8) (6 4 2 8 5 7 1 3) (4 2 5 8 6 1 3 7) (4 2 7 5 1 8 6 3) (7 4 2 5 8 1 3 6) (4 2 8 5 7 1 3 6) (4 6 8 2 7 1 3 5) (7 4 2 8 6 1 3 5) (4 2 8 6 1 3 5 7) (5 7 2 4 8 1 3 6) (2 5 7 4 1 8 6 3) (6 8 2 4 1 7 5 3) (7 2 4 1 8 5 3 6) (8 2 4 1 7 5 3 6) (5 2 6 1 7 4 8 3) (5 2 8 1 4 7 3 6) (2 7 5 8 1 4 6 3) (6 2 7 1 4 8 5 3) (2 6 1 7 4 8 3 5) (2 5 7 1 3 8 6 4) (6 2 7 1 3 5 8 4) (2 8 6 1 3 5 7 4) (4 7 5 3 1 6 8 2) (4 8 5 3 1 7 2 6) (4 6 8 3 1 7 5 2) (5 3 8 4 7 1 6 2) (3 5 8 4 1 7 2 6) (3 6 4 1 8 5 7 2) (6 3 7 4 1 8 2 5) (3 8 4 7 1 6 2 5) (6 3 5 7 1 4 2 8) (6 3 5 8 1 4 2 7) (3 5 7 1 4 2 8 6) (3 6 8 1 4 7 5 2) (6 3 1 8 4 2 7 5) (7 5 3 1 6 8 2 4) (5 3 1 6 8 2 4 7) (5 3 1 7 2 8 6 4) (6 3 1 7 5 8 2 4) (6 3 1 8 5 2 4 7) (3 6 8 1 5 7 2 4) (7 3 1 6 8 5 2 4) (3 1 7 5 8 2 4 6) (8 3 1 6 2 5 7 4) (5 7 4 1 3 8 6 2) (5 8 4 1 3 6 2 7) (4 1 5 8 6 3 7 2) (6 4 7 1 3 5 2 8) (8 4 1 3 6 2 7 5) (4 8 1 3 6 2 7 5) (5 7 1 3 8 6 4 2) (1 6 8 3 7 4 2 5) (7 1 3 8 6 4 2 5) (5 1 8 6 3 7 2 4) (1 5 8 6 3 7 2 4) (5 8 4 1 7 2 6 3) (6 4 1 5 8 2 7 3) (4 6 1 5 2 8 3 7) (4 7 1 8 5 2 6 3) (4 8 1 5 7 2 6 3) (4 1 5 8 2 7 3 6) (6 4 7 1 8 2 5 3) (5 1 4 6 8 2 7 3) (5 7 1 4 2 8 6 3) (5 1 8 4 2 7 3 6) (1 7 4 6 8 2 5 3) (1 7 5 8 2 4 6 3) (6 1 5 2 8 3 7 4))
一共92個解。