用Prolog解决数独问题

房东的猫 提交于 2020-02-29 18:45:02

我们先了解一下什么是数独

数独(shù dú)是源自18世纪瑞士的一种数学游戏。它是一种运用纸、笔进行演算的逻辑游戏。数独有多种类型,我们仅以其中一种类型作为本文实例。

玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。

方格

水平方向有九横行,垂直方向有九纵列的矩形,画分八十一个小正方形,称为九宫格(Grid),如图一所示,是数独(Sudoku)的作用范围。

行:水平方向的每一横行有九格,每一横行称为行(Row)

列:垂直方向的每一纵列有九格,每一纵列称为列(Column

宫:三行与三列相交之处有九格,每一单元称为小九宫(BoxBlock),简称,如图四所示(在杀手数独中,宫往往用单词Nonet表示)

上述行、列、宫、单元格统称为单元(Unit);而行、列、宫统称为区域(Region)。

区块

由三个连续宫组成大行列(Chute),分大行(Floor)及大列(Tower)。

第一大行:由第一宫、第二宫、第三宫组成。

第二大行:由第四宫、第五宫、第六宫组成。

第三大行:由第七宫、第八宫、第九宫组成。

第一大列:由第一宫、第四宫、第七宫组成。

第二大列:由第二宫、第五宫、第八宫组成。

第三大列:由第三宫、第六宫、第九宫组成。

格位编号

坐标有多种标示法,有横行 A~I,纵列 1~9(如中国),也有横行 1~9,纵列 A~I(如日本),这两种标示容易混淆,故最被广泛使用的是横行R1~R9,纵列C1~C9的标示法。

提示数:在九宫格的格位填上一些数字,做为填数判断的线索Hint),称为提示数(Clue

通常人工解决数独的思路

1、依解题填制的过程可区分为直观法与候选数法。

直观法就是不做任何记号,直接从数独的盘势观察线索,推论答案的方法。

2、候选数法就是排除行列宫格位已出现的数字,将剩余可填数字填入空格,做为解题线索的参考,可填数字称为候选数(Candidates,或称备选数)

直观法和候选数法只是填制时候是否有注记的区别,依照个人习惯而定,并非鉴定题目难度或技巧难度的标准,无论是难题或是简单题都可上述方法填制,一般程序解题以候选数法较多。

数独的Prolog程序

有效的数独板核心程序可以简明地表示如下:

1 : sudoku(Rows) :-

2 :       length(Rows, 9),

3 :       maplist(same_length(Rows), Rows),

4 :       append(Rows, Vs), %Rows列表里元素也是表

5 :       Vs ins 1..9, %Vs的取值范围是0~9之间

6 :       maplist(all_distinct, Rows), %每行的元素都不重复

7 :       transpose(Rows, Columns),

8 :       maplist(all_distinct, Columns), %每列的元素都不重复

9 :       Rows = [As,Bs,Cs,Ds,Es,Fs,Gs,Hs,Is],

10:       blocks(As, Bs, Cs),

11:       blocks(Ds, Es, Fs),

12:       blocks(Gs, Hs, Is).

13:

14:       blocks([], [], []).

15:       blocks([N1,N2,N3|Ns1], 

16:        [N4,N5,N6|Ns2],

17:    [N7,N8,N9|Ns3]) :-

18:      all_distinct([N1,N2,N3,N4,N5,N6,N7,N8,N9]),%元素都不重复

19: blocks(Ns1, Ns2, Ns3).

注意:程序中每行的行首是为了在后面方便描述而另外编辑的行号,不是程序的一部分

像所有纯Prolog程序一样,该谓词可以在所有方向上使用。

先让我们运行程序做一下测试:

例如,使用如下代码生成有效的数独板:

运行程序如下:

R1: ?- sudoku(Rows), %启动数独 

R2:   maplist(label, Rows),  %开始产生数独板

R3:   maplist(portray_clause, Rows). %格式化输出结

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

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

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

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

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

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

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

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

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

Rows = [[1, 2, 3, 4, 5, 6, 7, 8|...], [4, 5, 6, 7, 8, 9, 1|...] | ... ] .

行的部分实例化然后将其变成完成状态,这就是我们通常所理解的数独难题。

接下来开始来分析一下Prolog是怎么做到的

行号2: length(Rows,9) 

功能:是将变量设为9个元素的列表

测试:

?- write('Rows = '),write(Rows),nl,length(Rows,9),write('Rows = '),write(Rows).

Rows = _83784

Rows = [_84522,_84528,_84534,_84540,_84546,_84552,_84558,_84564,_84570]

Rows = [_84522, _84528, _84534, _84540, _84546, _84552, _84558, _84564, _84570].

Rows = _83784

%分配前的

Rows = [_84522,_84528,_84534,_84540,_84546,_84552,_84558,_84564,_84570]

%分配后在Rows的列表里有9个变量,变量的表述是以下划线’_’开始的标识,这是Prolog对变量的缺省约定

Rows = [_84522, _84528, _84534, _84540, _84546, _84552, _84558, _84564, _84570].   %系统默认的变量的结果

行号3: maplist(same_length(Rows), Rows)

功能: 将变量设为含有9个列表,每个列表有9个元素的列表

测试: 为了测试和观察方便,我们测试一个3*3的列表测试

?- length(Rows, 3),maplist(same_length(Rows), Rows),write('MyRows = '),write(Rows),nl,nl.

MyRows = [[_82040,_82046,_82052],[_82058,_82064,_82070],[_82076,_82082,_82088]]

Rows = [[_82040, _82046, _82052], [_82058, _82064, _82070], [_82076, _82082, _82088]].

为了观察方便我们先手工格式化一下MyRows=后面的数据如下

[[_82040,_82046,_82052],

 [_82058,_82064,_82070],

 [_82076,_82082,_82088]]

行号4: append(Rows, Vs),

功能: Rows列表里的列表元素合取到Vs表中的元素

测试

?- length(Rows, 3),maplist(same_length(Rows), Rows),append(Rows, Vs).

Rows = [[_81812, _81818, _81824], [_81830, _81836, _81842], [_81848, _81854, _81860]],

Vs = [_81812, _81818, _81824, _81830, _81836, _81842, _81848, _81854, _81860].

行号5: Vs ins 1..9,

功能: Vs列表中元素的取值范围设置为0~9之间。

行号6: maplist(all_distinct, Rows)

功能: 限制每行的元素都不重复

行号7: transpose(Rows, Columns),

功能: 行,列变换 (列表中的每个子列表中的元素作为一个限定单元,也就是无重复元素,因为列也是需要限定的范围,所以需要此行)。

测试:

?- length(Rows, 3),maplist(same_length(Rows), Rows),transpose(Rows, Columns).

Rows = [[_82506, _82512, _82518], [_82524, _82530, _82536], [_82542, _82548, _82554]],

Columns = [[_82506, _82524, _82542], [_82512, _82530, _82548], [_82518, _82536, _82554]].

整理行:

[[_82506, _82512, _82518],

 [_82524, _82530, _82536],

 [_82542, _82548, _82554]],

整理列:

[[_82506, _82524, _82542],

 [_82512, _82530, _82548],

 [_82518, _82536, _82554]]

行号8: maplist(all_distinct, Columns)

功能: 每行()的元素都不重复

行号9: Rows = [As,Bs,Cs,Ds,Es,Fs,Gs,Hs,Is],

功能: Rows列表中的子表与As,Bs... 元素合取。

10:       blocks(As, Bs, Cs),

11:       blocks(Ds, Es, Fs),

12:       blocks(Gs, Hs, Is).

功能: 分别调用blocks对每个宫内的元素做限定

141516171819描述谓词blocks

14: blocks([], [], []).

15: blocks([N1,N2,N3|Ns1], 

16:    [N4,N5,N6|Ns2],

17:    [N7,N8,N9|Ns3]) :-

18:      all_distinct([N1,N2,N3,N4,N5,N6,N7,N8,N9]),%元素都不重复

19: blocks(Ns1, Ns2, Ns3).

行号18: 限定内数据不能重复。

运行行号R1:

?- sudoku(Rows),

功能:启动数独    

运行行号R2:

maplist(label, Rows)

功能: 开始产生数独板

测试:

?- length(Rows, 3),maplist(same_length(Rows), Rows),append(Rows, Vs),Vs ins 1..9,all_distinct(Vs),maplist(label, Rows).

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

Vs = [1, 2, 3, 4, 5, 6, 7, 8, 9] ;  %下一个解

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

Vs = [1, 2, 3, 4, 5, 6, 7, 9, 8] ;  %下一个解

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

Vs = [1, 2, 3, 4, 5, 6, 8, 7, 9] ;  %下一个解

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

Vs = [1, 2, 3, 4, 5, 6, 8, 9, 7] ;  %下一个解

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

Vs = [1, 2, 3, 4, 5, 6, 9, 7, 8] ;  %下一个解

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

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

运行行号R3:

maplist(portray_clause, Rows). %格式化输出结果

测试:

?- length(Rows, 3),maplist(same_length(Rows), Rows),append(Rows, Vs),Vs ins 1..9,all_distinct(Vs),maplist(label, Rows),maplist(portray_clause, Rows).

[1, 2, 3].

[4, 5, 6].

[7, 8, 9].

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

Vs = [1, 2, 3, 4, 5, 6, 7, 8, 9] ;  %下一个解

[1, 2, 3].

[4, 5, 6].

[7, 9, 8].

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

Vs = [1, 2, 3, 4, 5, 6, 7, 9, 8] ;  %下一个解

[1, 2, 3].

[4, 5, 6].

[8, 7, 9].

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

Vs = [1, 2, 3, 4, 5, 6, 8, 7, 9] .  %到此为止

[[8,1,_,_,_,3,2,9,_],

 [_,6,7,_,_,_,_,_,_],

 [9,_,_,5,_,_,_,_,6],

 [_,_,_,4,_,8,_,_,_],

 [6,_,4,_,_,_,8,_,9]

 [_,_,_,2,_,9,_,_,_],

 [7,_,_,_,_,1,_,_,8],

 [_,_,_,_,_,_,3,7,_],

 [_,5,3,8,_,_,_,4,2]

?- sudoku(Rows), %启动数独 

   maplist(label, Rows),  %开始产生数独板

   maplist(portray_clause, Rows). %格式化输出结果

这段运行程序可以将数独的解一网打尽,只需要每个解后输入’;’即可得到更多解,输入’.’’Enter’结束运行。如果需要数独得到指定问题的解该如何做呢?

可以把问题写入程序如:

problem(1, [[_,_,_,_,_,_,_,_,_],

            [_,_,_,_,_,3,_,8,5],

            [_,_,1,_,2,_,_,_,_],

            [_,_,_,5,_,7,_,_,_],

            [_,_,4,_,_,_,1,_,_],

            [_,9,_,_,_,_,_,_,_],

            [5,_,_,_,_,_,_,7,3],

            [_,_,2,_,1,_,_,_,_],

            [_,_,_,_,4,_,_,_,9]]).

 

problem(2, [[8,1,_,_,_,3,2,9,_],

            [_,6,7,_,_,_,_,_,_],

            [9,_,_,5,_,_,_,_,6],

            [_,_,_,4,_,8,_,_,_],

            [6,_,4,_,_,_,8,_,9],

            [_,_,_,2,_,9,_,_,_],

            [7,_,_,_,_,1,_,_,8],

            [_,_,_,_,_,_,3,7,_],

            [_,5,3,8,_,_,_,4,2]]).

problem(3, [[_,2,_,_,_,_,_,1,_],

            [5,_,6,_,_,_,3,_,9],

            [_,8,_,5,_,2,_,6,_],

            [_,_,5,_,7,_,1,_,_],

            [_,_,_,2,_,8,_,_,_],

            [_,_,4,_,1,_,8,_,_],

            [_,5,_,8,_,7,_,3,_],

            [7,_,2,_,_,_,4,_,5],

            [_,4,_,_,_,_,_,7,_]]).

运行程序:

?- problem(3,Rows),sudoku(Rows),maplist(label, Rows),maplist(portray_clause, Rows).

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

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

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

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

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

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

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

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

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

Rows = [[4, 2, 3, 7, 6, 9, 5, 1|...], [5, 7, 6, 4, 8, 1, 3|...], [9, 8, 1, 5, 3, 2|...], [8, 3, 5, 6, 7|...], [1, 9, 7, 2|...], [2, 6, 4|...], [6, 5|...], [7|...], [...|...]] ;

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

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

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

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

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

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

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

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

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

Rows = [[4, 2, 3, 7, 6, 9, 5, 1|...], [5, 7, 6, 4, 8, 1, 3|...], [9, 8, 1, 5, 3, 2|...], [8, 6, 5, 3, 7|...], [1, 9, 7, 2|...], [2, 3, 4|...], [6, 5|...], [7|...], [...|...]] .

Prolog语言解决数独问题只用了区区19行代码,而用其它过程式语言如C,100行程序肯定完成不了,这就是Prolog的魅力之一。

转子本人微信公众号:https://mp.weixin.qq.com/s/Q2pP4UhteDtrSR4IVMtIhA

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!