1. 程式人生 > >Scheme來實現八皇后問題(1)

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個解。