Prolog學習:數獨和八皇后問題


上一篇簡單介紹了下Prolog的一些基本概念,今天我們來利用這些基本概念解決兩個問題:數獨八皇后問題。

數獨

 數獨是一個很經典的游戲:

玩家需要根據n×n盤面上的已知數字,推理出所有剩余空格的數字,並滿足每一行、每一列、每一個粗線宮內的數字均含1-n,不重復。

當然數獨的階有很多,9×9是最常見的,我們就以它做例子。在用Prolog解決之前先想想如果我們用C#或Java來做或怎么做?無非就是數據結構加算法,我們先得用一個數據結構表示數獨,然后我們要在這個數據結構上“施加”算法進行求解。采用Prolog的第一步是相同的,我們得找一個數據結構表示數獨,毫無疑問在Prolog中我們只能選擇列表或元組,這里列表是更好的選擇,因為列表可以進行[Head|Tail]解析,后面你就知道為什么了。我們像下面這樣表示一個數獨:

[_, 6, _, 5, 9, 3, _, _, _,
 9, _, 1, _, _, _, 5, _, _,
 _, 3, _, 4, _, _, _, 9, _,
 1, _, 8, _, 2, _, _, _, 4,
 4, _, _, 3, _, 9, _, _, 1,
 2, _, _, _, 1, _, 6, _, 9,
 _, 8, _, _, _, 6, _, 2, _,
 _, _, 4, _, _, _, 8, _, 7,
 _, _, _, 7, 8, 5, _, 1, _] 

“_”代表未知的數字,需要玩家填空的地方。

接下來的步驟跟命令式語言就截然不同了,我們不是描述算法,而是要描述數獨這個游戲的規則:

  1. 給定玩家一個9×9的盤面,玩家填充完所有的空格后最終的解仍然是這個9×9的盤面;
  2. 填充完空格后,每一個空格內的數字均在1~9之內;
  3. 填充完空格后,每一行9個數字各不相同;
  4. 填充完空格后,每一列9個數字各不相同;
  5. 填充完空格后,每一個宮格內的數字各不相同。

Ok,這就是整個游戲的規則。你可能覺得第一條規則沒什么用,實際上第一條規則定義了“解”的形式,就像在C#中我們確定了方法的簽名一樣:

sudoku(Puzzle,Solution):- Solution = Puzzle.

事實上這個規則已經可以工作了:

| ?- sudoku([1,2,3,4,5,6,7,8,9,
             1,2,3,4,5,6,7,8,9, 
             1,2,3,4,5,6,7,8,9, 
             1,2,3,4,5,6,7,8,9,
             1,2,3,4,5,6,7,8,9,
             1,2,3,4,5,6,7,8,9,
             1,2,3,4,5,6,7,8,9,
             1,2,3,4,5,6,7,8,9,
             1,2,3,4,5,6,7,8,9],Solution). 

Solution = [1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9......

當然這只是第一步,這個規則對於輸入的數獨形式沒有任何限制,事實上可以是任意的列表,Prolog都返回yes:

| ?- sudoku([1,2,3],Solution).

Solution = [1,2,3]

yes

我們需要規定下數獨的形式:

sudoku(Puzzle,Solution):-
    Solution = Puzzle,
    Puzzle = [S11,S12,S13,S14,S15,S16,S17,S18,S19,
              S21,S22,S23,S24,S25,S26,S27,S28,S29,
              S31,S32,S33,S34,S35,S36,S37,S38,S39,
              S41,S42,S43,S44,S45,S46,S47,S48,S49,
              S51,S52,S53,S54,S55,S56,S57,S58,S59,
              S61,S62,S63,S64,S65,S66,S67,S68,S69,
              S71,S72,S73,S74,S75,S76,S77,S78,S79,
              S81,S82,S83,S84,S85,S86,S87,S88,S89,
              S91,S92,S93,S94,S95,S96,S97,S98,S99].
| ?- sudoku([1,2,3],Solution).

no

我們接着看第二條規則:“填充完空格后,每一個空格內的數字均在1~9之內” 。上一篇文章中我們介紹了Prolog中有一個內置謂詞叫fd_domain,這時候就可以派上用場了:

sudoku(Puzzle,Solution):-
    Solution = Puzzle,
    Puzzle = [S11,S12,S13,S14,S15,S16,S17,S18,S19,
              S21,S22,S23,S24,S25,S26,S27,S28,S29,
              S31,S32,S33,S34,S35,S36,S37,S38,S39,
              S41,S42,S43,S44,S45,S46,S47,S48,S49,
              S51,S52,S53,S54,S55,S56,S57,S58,S59,
              S61,S62,S63,S64,S65,S66,S67,S68,S69,
              S71,S72,S73,S74,S75,S76,S77,S78,S79,
              S81,S82,S83,S84,S85,S86,S87,S88,S89,
              S91,S92,S93,S94,S95,S96,S97,S98,S99],
    fd_domain(Puzzle,1,9).

好了現在我們只能輸入9×9並且每個每個位置上只能是1~9之間的數的列表了。

好了,現在到整個游戲的關鍵規則,事實上2,3,4這三個規則才決定了數獨的難度,1,2只不過是基礎,我們來統一考慮這三個問題。這里其實比想象的簡單多了。我們首先要做的就是需要定義出來宮格

Row1 = [S11,S12,S13,S14,S15,S16,S17,S18,S19],
Row2 = [S21,S22,S23,S24,S25,S26,S27,S28,S29],
Row3 = [S31,S32,S33,S34,S35,S36,S37,S38,S39],
Row4 = [S41,S42,S43,S44,S45,S46,S47,S48,S49],
Row5 = [S51,S52,S53,S54,S55,S56,S57,S58,S59],
Row6 = [S61,S62,S63,S64,S65,S66,S67,S68,S69],
Row7 = [S71,S72,S73,S74,S75,S76,S77,S78,S79],
Row8 = [S81,S82,S83,S84,S85,S86,S87,S88,S89],
Row9 = [S91,S92,S93,S94,S95,S96,S97,S98,S99],
    
Col1 = [S11,S21,S31,S41,S51,S61,S71,S81,S91],
Col2 = [S12,S22,S32,S42,S52,S62,S72,S82,S92],
Col3 = [S13,S23,S33,S43,S53,S63,S73,S83,S93],
Col4 = [S14,S24,S34,S44,S54,S64,S74,S84,S94],
Col5 = [S15,S25,S35,S45,S55,S65,S75,S85,S95],
Col6 = [S16,S26,S36,S46,S56,S66,S76,S86,S96],
Col7 = [S17,S27,S37,S47,S57,S67,S77,S87,S97],
Col8 = [S18,S28,S38,S48,S58,S68,S78,S88,S98],
Col9 = [S19,S29,S39,S49,S59,S69,S79,S89,S99],
    
Square1 = [S11,S12,S13,S21,S22,S23,S31,S32,S33],
Square2 = [S14,S15,S16,S24,S25,S26,S34,S35,S36],
Square3 = [S17,S18,S19,S27,S28,S29,S37,S38,S39],
Square4 = [S41,S42,S43,S51,S52,S53,S61,S62,S63],
Square5 = [S44,S45,S46,S54,S55,S56,S64,S65,S66],
Square6 = [S47,S48,S49,S57,S58,S59,S67,S68,S69],
Square7 = [S71,S72,S73,S81,S82,S83,S91,S92,S93],
Square8 = [S74,S75,S76,S84,S85,S86,S94,S95,S96],
Square9 = [S77,S78,S79,S87,S88,S89,S97,S98,S99],

上一篇文章中我還提到一個謂詞叫fd_all_different:檢查列表中是否有重復元素,接下來我們只要證明每一列,每一行,每一個宮格列表內沒有重復元素就可以了:

fd_all_different(Row1),
fd_all_different(Row2),
……
fd_all_different(Col1),
fd_all_different(Col2),
……
fd_all_different(Square1),
fd_all_different(Square2),
……

其實到此這個解數獨的程序已經結束了,不過最后這幾行代碼太土了,我們可以采用用遞歸“優化”下,像下面這樣:

valid([]).
valid([Head|Tail]):-
    fd_all_different(Head),
    valid(Tail).
valid([Row1,Row2,Row3,Row4,Row5,Row6,Row7,Row8,Row9,
          Col1,Col2,Col3,Col4,Col5,Col6,Col7,Col8,Col9,
          Square1,Square2,Square3,Square4,Square5,Square6,Square7,Square8,Square9]).

不管你信不信,我們已經搞定了,最終完整的代碼如下:

valid([]).
valid([Head|Tail]):-
    fd_all_different(Head),
    valid(Tail).
sudoku(Puzzle,Solution):-
    Solution = Puzzle,
    Puzzle = [S11,S12,S13,S14,S15,S16,S17,S18,S19,
          S21,S22,S23,S24,S25,S26,S27,S28,S29,
          S31,S32,S33,S34,S35,S36,S37,S38,S39,
          S41,S42,S43,S44,S45,S46,S47,S48,S49,
          S51,S52,S53,S54,S55,S56,S57,S58,S59,
          S61,S62,S63,S64,S65,S66,S67,S68,S69,
          S71,S72,S73,S74,S75,S76,S77,S78,S79,
          S81,S82,S83,S84,S85,S86,S87,S88,S89,
          S91,S92,S93,S94,S95,S96,S97,S98,S99],
    fd_domain(Puzzle,1,9),
    
    Row1 = [S11,S12,S13,S14,S15,S16,S17,S18,S19],
    Row2 = [S21,S22,S23,S24,S25,S26,S27,S28,S29],
    Row3 = [S31,S32,S33,S34,S35,S36,S37,S38,S39],
    Row4 = [S41,S42,S43,S44,S45,S46,S47,S48,S49],
    Row5 = [S51,S52,S53,S54,S55,S56,S57,S58,S59],
    Row6 = [S61,S62,S63,S64,S65,S66,S67,S68,S69],
    Row7 = [S71,S72,S73,S74,S75,S76,S77,S78,S79],
    Row8 = [S81,S82,S83,S84,S85,S86,S87,S88,S89],
    Row9 = [S91,S92,S93,S94,S95,S96,S97,S98,S99],
    
    Col1 = [S11,S21,S31,S41,S51,S61,S71,S81,S91],
    Col2 = [S12,S22,S32,S42,S52,S62,S72,S82,S92],
    Col3 = [S13,S23,S33,S43,S53,S63,S73,S83,S93],
    Col4 = [S14,S24,S34,S44,S54,S64,S74,S84,S94],
    Col5 = [S15,S25,S35,S45,S55,S65,S75,S85,S95],
    Col6 = [S16,S26,S36,S46,S56,S66,S76,S86,S96],
    Col7 = [S17,S27,S37,S47,S57,S67,S77,S87,S97],
    Col8 = [S18,S28,S38,S48,S58,S68,S78,S88,S98],
    Col9 = [S19,S29,S39,S49,S59,S69,S79,S89,S99],
    
    Square1 = [S11,S12,S13,S21,S22,S23,S31,S32,S33],
    Square2 = [S14,S15,S16,S24,S25,S26,S34,S35,S36],
    Square3 = [S17,S18,S19,S27,S28,S29,S37,S38,S39],
    Square4 = [S41,S42,S43,S51,S52,S53,S61,S62,S63],
    Square5 = [S44,S45,S46,S54,S55,S56,S64,S65,S66],
    Square6 = [S47,S48,S49,S57,S58,S59,S67,S68,S69],
    Square7 = [S71,S72,S73,S81,S82,S83,S91,S92,S93],
    Square8 = [S74,S75,S76,S84,S85,S86,S94,S95,S96],
    Square9 = [S77,S78,S79,S87,S88,S89,S97,S98,S99],
    
    valid(Row1,Row2,Row3,Row4,Row5,Row6,Row7,Row8,Row9,
          Col1,Col2,Col3,Col4,Col5,Col6,Col7,Col8,Col9,
          Square1,Square2,Square3,Square4,Square5,Square6,Square7,Square8,Square9).

反正我信了,我們來試試吧,就以上面我從百度上找到的那個圖為例:

| ?- sudoku([_, 6, _, 5, 9, 3, _, _, _,
 9, _, 1, _, _, _, 5, _, _,
 _, 3, _, 4, _, _, _, 9, _,
 1, _, 8, _, 2, _, _, _, 4,
 4, _, _, 3, _, 9, _, _, 1,
 2, _, _, _, 1, _, 6, _, 9,
 _, 8, _, _, _, 6, _, 2, _,
 _, _, 4, _, _, _, 8, _, 7,
 _, _, _, 7, 8, 5, _, 1, _],Solution).

Solution = [7,6,2,5,9,3,1,4,8,9,4,1,2,7,8,5,3,6,8,3,5,4,6,1,7,9,2,1,9,8,6,2,7,3,5,4,4,7,6,3,5,9,2,8,1,2,5,3,8,1,4,6,7,9,3,8,7,1,4,6,9,2,5,5,1,4,9,3,2,8,6,7,6,2,9,7,8,5,4,1,3]

美化后的結果是這樣的:

[7,6,2,5,9,3,1,4,8,
 9,4,1,2,7,8,5,3,6,
 8,3,5,4,6,1,7,9,2,
 1,9,8,6,2,7,3,5,4,
 4,7,6,3,5,9,2,8,1,
 2,5,3,8,1,4,6,7,9,
 3,8,7,1,4,6,9,2,5,
 5,1,4,9,3,2,8,6,7,
 6,2,9,7,8,5,4,1,3]

Perfect!

 

八皇后問題

Ok,有了數獨問題作為鋪墊,下面看八皇后問題應該就應該沒那么難了,請保持用Prolog思考問題的方式,解決后你會發現Prolog真是這方面的“專家”,Let's Go!

八皇后問題也是一個非常經典的問題:

八皇后問題是一個以國際象棋為背景的問題:如何能夠在 8×8 的國際象棋棋盤上放置八個皇后,使得任何一個皇后都無法直接吃掉其他的皇后?為了達到此目的,任兩個皇后都不能處於同一條橫行、縱行或斜線上。

老套路我們先描述游戲規則。

  1. 每個皇后有一個行號和列號,行號和列號的取值范圍在1~8之間;
  2. 一個棋盤上有八個皇后;
  3. 任意兩個皇后不可以共享一行;
  4. 任意兩個皇后不可以共享一列;
  5. 任意兩個皇后不可以在同一個對角線上(左下角->右上角);
  6. 任意兩個皇后不可以在同一個對角線上(右下角->左上角)。

在了解規則之后我們梳理一下這個問題,對照上面這個圖:

我們給棋盤上每一個位置設定一個坐標(x,y),八個皇后的坐標分別為(x1,y1),(x2,y2)……我們以回溯的角度看問題,假設如圖已經得到了最后解,那么這8個坐標滿足:x1,x2……各不相同,y1,y2……個不相同,找出(x1,y1),(x2,y2)……中屬於對角線1上的和對角線2上的位置,它們坐標應該個不相同。

(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),(1,8)
(2,1),(2,2),(2,3),(2,4),(2,5),(2,6),(2,7),(2,8)
(3,1),(3,2),(3,3),(3,4),(3,5),(3,6),(3,7),(3,8)
(4,1),(4,2),(4,3),(4,4),(4,5),(4,6),(4,7),(4,8)
(5,1),(5,2),(5,3),(5,4),(5,5),(5,6),(5,7),(5,8)
(6,1),(6,2),(6,3),(6,4),(6,5),(6,6),(6,7),(6,8)
(7,1),(7,2),(7,3),(7,4),(7,5),(7,6),(7,7),(7,8)
(8,1),(8,2),(8,3),(8,4),(8,5),(8,6),(8,7),(8,8)

所以整個問題的難點在於給定類似下面這樣一個列表,我們需要找出其中的所有的行號,列號,和在對角線上的坐標:

[(1,1),(1,5),(2,5),(2,2),(8,8),(4,4),(4,5),(5,4)]

找出行號和列號稍微簡單點,這里直接給出答案,大家也可以自己思考下:

rows([],[]).
rows([(Row,_)|QueensTail],[Row|RowsTail]):-
    rows(QueensTail,RowsTail).
cols([],[]).
cols([(_,Col)|QueensTail],[Col|ColsTail]):-
	cols(QueensTail,ColsTail).

把上面的列表代進去簡單驗證下:

| ?- rows([(1,1),(1,5),(2,5),(2,2),(8,8),(4,4),(4,5),(5,4)],Rows).

Rows = [1,1,2,2,8,4,4,5]

yes
| ?- cols([(1,1),(1,5),(2,5),(2,2),(8,8),(4,4),(4,5),(5,4)],Cols).

Cols = [1,5,5,2,8,4,5,4]

yes

關鍵是如何驗證對角線上的元素,而且兩條對角線是不一樣的,提醒下因為我們最后會還是會利用fd_all_different這個謂詞。

 

好吧,我們回過頭觀察下上面那個棋盤的坐標圖(注意我標紅的地方),有沒有發現什么規則呢?

  • 左上角到右下角的對角線上的元素:所有坐標的橫坐標-縱坐標都相同,等於0;
  • 左下角到右上角的對角線上的元素:所有坐標的橫坐標+縱坐標都相同,等於9;

OK,我們可以定義下面這樣兩個謂詞diags1和diags2:

diags1([],[]).
diags1([(Row,Col)|QueensTail],[Diagonal|DiagonalsTail]):-
    Diagonal is Col - Row,
    diags1(QueensTail,DiagonalsTail).
diags2([],[]).
diags2([(Row,Col)|QueensTail],[Diagonal|DiagonalsTail]):-
    Diagonal is Col + Row,
    diags2(QueensTail,DiagonalsTail).

我們可以簡單驗證下:

| ?- diags1([(2,2),(8,8)],Diags1).

Diags1 = [0,0]

yes

 | ?- diags2([(4,5),(5,4)],Diags2).

 
         

 Diags2 = [9,9]

 
         

 yes

如果坐標在對角線上,那么抓取到的列表元素都是相等的。

好了,到目前為止我們已經完成了最難的部分,剩下的都是一些驗證性工作。我們最終的“程序入口”應該是這樣的:

eight_queens([(X1,Y1),(X2,Y2),(X3,Y3),(X4,Y4),(X5,Y5),(X6,Y6),(X7,Y7),(X8,Y8)])

我們還需要一些驗證性工作:

1.給定列表里的皇后是不是合法的,即橫縱坐標都在1~8之內,這用到了我上一篇中提到的member謂詞:

valid_queen((Row,Col)):-
    Range = [1,2,3,4,5,6,7,8],
    member(Row,Range),member(Col,Range).

2.驗證給定的列表是不是八個皇后,這里用到一個length謂詞,顧名思義:

length(Board,8).

3.需要遞歸的驗證給定的列表中的每個元素是不是“皇后”:

valid_board([]).
valid_board([Head|Tail]):- valid_queen(Head),valid_board(Tail).

Ok,下面就是八皇后問題的答案的完整代碼:

valid_queen((Row,Col)):-
    Range = [1,2,3,4,5,6,7,8],
    member(Row,Range),member(Col,Range).

valid_board([]).
valid_board([Head|Tail]):- valid_queen(Head),valid_board(Tail).

rows([],[]).
rows([(Row,_)|QueensTail],[Row|RowsTail]):-
    rows(QueensTail,RowsTail).

cols([],[]).
cols([(_,Col)|QueensTail],[Col|ColsTail]):-
    cols(QueensTail,ColsTail).
    
diags1([],[]).
diags1([(Row,Col)|QueensTail],[Diagonal|DiagonalsTail]):-
    Diagonal is Col - Row,
    diags1(QueensTail,DiagonalsTail).
    
diags2([],[]).
diags2([(Row,Col)|QueensTail],[Diagonal|DiagonalsTail]):-
    Diagonal is Col + Row,
    diags2(QueensTail,DiagonalsTail).

eight_queens(Board) :-
    length(Board,8),
    valid_board(Board),
    
    rows(Board,Rows),
    cols(Board,Cols),
    diags1(Board,Diags1),
    diags2(Board,Diags2),
    
    fd_all_different(Rows),
    fd_all_different(Cols),
    fd_all_different(Diags1),
    fd_all_different(Diags2).   

沒錯,答案已經出來,但事實上上面這個程序運行的非常慢,我在我i7的筆記本上的GNU Prolog中執行下面這個問題,半天沒有響應:

| ?- eight_queens([(X1,Y1),(X2,Y2),(X3,Y3),(X4,Y4),(X5,Y5),(X6,Y6),(X7,Y7),(X8,Y8)]).

其實我們可以對這個問題進行一個簡化。我們可以肯定棋盤上八行每行肯定有一個皇后,又因為互不能在一行,因此我們假設八皇后的坐標分別為:(1,A),(2,B),(3,C),(4,D),(5,E),(6,F),(7,G),(8,H)。那么我們可以對上面的代碼進行優化,去掉所有對行的操作,優化后代碼如下:

valid_queen((Row,Col)):- member(Col,[1,2,3,4,5,6,7,8]).

valid_board([]).
valid_board([Head|Tail]):- valid_queen(Head),valid_board(Tail).

cols([],[]).
cols([(_,Col)|QueensTail],[Col|ColsTail]):-
    cols(QueensTail,ColsTail).
    
diags1([],[]).
diags1([(Row,Col)|QueensTail],[Diagonal|DiagonalsTail]):-
    Diagonal is Col - Row,
    diags1(QueensTail,DiagonalsTail).
    
diags2([],[]).
diags2([(Row,Col)|QueensTail],[Diagonal|DiagonalsTail]):-
    Diagonal is Col + Row,
    diags2(QueensTail,DiagonalsTail).

eight_queens(Board) :-
    Board = [(1,_),(2,_),(3,_),(4,_),(5,_),(6,_),(7,_),(8,_)],
    length(Board,8),
    valid_board(Board),
    
    cols(Board,Cols),
    diags1(Board,Diags1),
    diags2(Board,Diags2),
    
    fd_all_different(Cols),
    fd_all_different(Diags1),
    fd_all_different(Diags2).
    

然后這樣問問題:

eight_queens([(1,Y1),(2,Y2),(3,Y3),(4,Y4),(5,Y5),(6,Y6),(7,Y7),(8,Y8)]).
| ?- eight_queens([(1,Y1),(2,Y2),(3,Y3),(4,Y4),(5,Y5),(6,Y6),(7,Y7),(8,Y8)]).

Y1 = 1
Y2 = 5
Y3 = 8
Y4 = 6
Y5 = 3
Y6 = 7
Y7 = 2
Y8 = 4 ? a

Y1 = 1
Y2 = 6

Y1 = 2
Y2 = 7
Y3 = 3
Y4 = 6
Y5 = 8
Y7 = 6
Y8 = 3

Y1 = 2
Y2 = 8
Y3 = 6
Y4 = 1
Y5 = 3
Y6 = 5
Y7 = 7
Y8 = 4

Y1 = 3
Y2 = 1
Y3 = 7
Y4 = 5
Y5 = 8
Y6 = 2
Y7 = 4
Y8 = 6
……
Y1 = 8
Y2 = 3
Y3 = 1
Y4 = 6
Y5 = 2
Y6 = 5
Y7 = 7
Y8 = 4

Y1 = 8
Y2 = 4
Y3 = 1
Y4 = 3
Y5 = 6
Y6 = 2
Y7 = 7
Y8 = 5

(81860 ms) no

?后跟a可以一次性詢問所有答案,可以看到還是相當的慢,這也算是聲明式語言的一個劣勢吧。

 

好了,今天介紹的兩個問題就到此結束了。問題本身並不是重點,重點是我們思考問題的方式。 

最后提供:源代碼下載,希望大家可以喜歡Prolog這門小巧簡單,功能強大的語言。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM