我们先了解一下什么是数独
数独(shù dú)是源自18世纪瑞士的一种数学游戏。它是一种运用纸、笔进行演算的逻辑游戏。数独有多种类型,我们仅以其中一种类型作为本文实例。
玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。
方格
水平方向有九横行,垂直方向有九纵列的矩形,画分八十一个小正方形,称为九宫格(Grid),如图一所示,是数独(Sudoku)的作用范围。
行:水平方向的每一横行有九格,每一横行称为行(Row)
列:垂直方向的每一纵列有九格,每一纵列称为列(Column)
宫:三行与三列相交之处有九格,每一单元称为小九宫(Box、Block),简称宫,如图四所示(在杀手数独中,宫往往用单词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对每个宫内的元素做限定
行14、15、16、17、18、19为’宫’描述谓词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
来源:oschina
链接:https://my.oschina.net/u/3936811/blog/3179553