hiho-102周 搜索五·数独
2016-06-19 14:49
190 查看
描述
在上一次学会了跳舞链的搜索方法之后,小Ho觉得这个算法真是棒极了。小Ho:小Hi,还有没有什么有意思的题目可以用跳舞链解决的啊?
小Hi:我想想,首先可以确定的是给出01矩阵的精确覆盖问题都可以解决。
小Ho:嗯,我觉得如果一个问题能够通过模型转换成01矩阵,应该都可以用跳舞链解决吧。
小Hi:是的,那么小Ho,你觉得数独问题能用跳舞链解决么?
小Ho:数独是啥?
小Hi:小Ho,你居然不知道数独?
小Ho:我有听过,但是没有实际去做过。
小Hi:这样啊,那我就简单给你讲一讲。数独问题就是给定一个9x9的矩阵。然后将1~9填入当中,比如下面这个:
其中有些格子已经填好了,有些格子则需要你填进去。
对于填好后的格子,需要满足3个条件:
每一个数字在每一行只能出现1次 每一个数字在每一列只能出现1次 每一个数字在每一个九宫区域内只能出现1次,上图中每一个粗线包围的区域就是一个九宫。
小Ho:规则看上去还是蛮简单的嘛。
小Hi:对啊。那么你觉得这个问题可以用跳舞链解决么?
小Ho:嗯,我想一想啊。
数独提示
小Ho:不行不行,我觉得搞不定啊!
小Hi:什么搞不定?你先说说你的想法?
小Ho:我是这样想的,要用跳舞链来解决数独问题,那就要求把数独问题转化为01的矩阵。我们之前遇到的问题是,一个格子只存在覆盖或是不覆盖的情况。而数独这个问题里面,一个位置是从数字当中选择一个。这样我就没有办法去覆盖一个解了。
小Hi:你说的没错。
小Ho:那应该怎么办啊?
小Hi:小Ho,其实你陷入了一个很大的误区。
小Ho:什么?
小Hi:对于精确覆盖问题的01矩阵,其实它的含义是这样:
列:一个原问题的约束条件
行:一个方案所满足的约束条件
在我们之前的问题中,一个问题的约束条件就是每一列是否被覆盖到,因此约束条件的数量也就和列的数量相同了。
而对于数独来说,其约束条件并不是列的数量。
根据数独游戏的规则,我们要满足的约束条件有四个:
每一个数字在每一行只能出现1次。
由于行和数字的互相匹配,因此一共会产生9x9,81个约束条件:
1. 第1行存在数字1 2. 第1行存在数字2 3. 第1行存在数字3 ... 9. 第1行存在数字9 10. 第2行存在数字1 11. 第2行存在数字2 ... 18. 第2行存在数字9 19. 第3行存在数字1 ... 80. 第9行存在数字8 81. 第9行存在数字9 对于第i行第j列填写数字k时,其对应的序号为(i-1)*9+k
每一个数字在每一列只能出现1次
由于列和数字的互相匹配,因此一共会产生9x9,81个约束条件:
82. 第1列存在数字1 83. 第1列存在数字2 84. 第1列存在数字3 ... 90. 第1列存在数字9 91. 第2列存在数字1 92. 第2列存在数字2 ... 99. 第2列存在数字9 100. 第3列存在数字1 ... 161. 第9列存在数字8 162. 第9列存在数字9 对于第i行第j列填写数字k时,其对应的序号为81+(j-1)*9+k
每一个数字在每一个九宫只能出现1次
由于九宫和数字的互相匹配,因此一共会产生9x9,81个约束条件:
163. 第1个九宫存在数字1 164. 第1个九宫存在数字2 165. 第1个九宫存在数字3 ... 171. 第1个九宫存在数字9 172. 第2个九宫存在数字1 173. 第2个九宫存在数字2 ... 180. 第2个九宫存在数字9 181. 第3个九宫存在数字1 ... 242. 第9个九宫存在数字8 243. 第9个九宫存在数字9 对于第i行第j列填写数字k时,位于第t个九宫,其对应的序号为162+(t-1)*9+k
每一个格子只能填一个数字
由于一共有81个格子,所以也是81个约束条件:
244. 第1行第1列填写了数字 245. 第1行第2列填写了数字 246. 第1行第3列填写了数字 ... 252. 第1行第9列填写了数字 253. 第2行第1列填写了数字 254. 第2行第2列填写了数字 ... 261. 第2行第9列填写了数字 262. 第3行第1列填写了数字 ... 323. 第9行第8列填写了数字 324. 第9行第9列填写了数字 对于第i行第j列填写数字k时,其对应的序号为243+(i-1)*9+j
总共有324个约束条件,也就是说对于数独游戏来说,对应的01矩阵有324列。
对于我们填写好的答案,一定会满足这全部324个约束条件。
那么再来看看方案的数量,这里有两种情况:
board[i][j] = 0:即第i行第j列为空格,因此我们可以将1-9中的数字填入,会产生9个不同的方案。
board[i][j] ≠ 0:即第i行第j列已经填了数字,所以这个地方只有1种方案。
那么小Ho,数独问题最多会有多少个方案呢?
小Ho:每一个格子可能填1-9这9个数字,一共有81个格子,所以总共是729个方案。
小Hi:对,由此可以得到数独问题的01矩阵有729行。
最后一步,就是来生成方案所覆盖的约束条件:
假设有方案在第i行第j列填写了数字k,其中第i行第j列是属于第t个九宫的格子。则其对应的约束条件为:
满足了第i行存在数字k。
满足了第j列存在数字k。
满足了第t个九宫存在数字k。
满足了第i行第j列填写有数字。
由此也可以看出,每一个方案恰好对应了4个约束条件。满足324个约束条件一共需要81个方案,也正好对应了数独游戏一共有81个格子的规则。
至此,我们也就构造出了生成数独游戏01矩阵的方法,其伪代码为:
set(i, j, k):
id = (i - 1) * 9 + j // 表示第i行第j列格子的编号
pid = (id - 1) * 9 + k // 表示该格子填写k所对应的方案编号
// 约束条件1 - 对应第1~81列
// 第(i-1)*9+k列表示第i行存在数字k
matrix[ pid ][(i - 1) * 9 + k] = 1
// 约束条件2 - 对应第82~162列 // 第81+(j-1)*9+k列表示第j列存在数字k matrix[ pid ][81 + (j - 1) * 9 + k] = 1 // 约束条件3 - 对应第163~243列 // 第162+(t-1)*9+k列表示第t个九宫存在数字k t = ((i - 1) / 3 * 3 + (j - 1) / 3) + 1 matrix[ pid ][162 + (t - 1) * 9 + k] = 1 // 约束条件4 - 对应第244~324列 // 第243+id列表示第i行第j列填写有数字 matrix[ pid ][243 + id] = 1
create(board):
matrix = [] // 设置01矩阵为729*324的矩阵,初始化为全0
For i = 1 .. 9
For j = 1 .. 9
If (board[i][j] == 0) Then
For k = 1 .. 9 // 枚举可能填写的9个数字
set(i, j, k)
End For
Else
set(i, j, board[i][j])
End If
End For
End For
之后的工作只需要将这个矩阵用跳舞链跑一次就可以了!最后根据答案栈中的行数还原出对应的位置和数字,就得到了数独的解。
小Ho:好,让我试试!
小Hi:小Ho,你先别着急。对于数独问题来说,直接使用跳舞链可能会导致超时,因此还有一个可以用到的优化。
小Ho:什么优化啊?
小Hi:这个优化需要改变我们的搜索顺序。每次都选择元素最少的那一列进行搜索,而不是每次都搜素head节点右边的节点。
额外增加一个cnt数组来记录每一列除了头结点外的节点个数。同时需要修改原来的跳舞链函数:
build函数中纵向增加节点的部分:
For j = 1 .. m
pre = columnHead[j]
For i = 1 .. n
If (board[i][j] == 1) Then
cnt[j] = cnt[j] + 1 // 计数器增加
p = node[ id[i][j] ]
p.down = pre.down
p.up = pre
pre.down.up = p
pre.down = p
pre = p
End If
End For
End For
remove函数中需要修改对应列的计数:
While (p2 != p)
// 获取该列下的每一个节点p2
p3 = p2.right
While (p3 != p2)
// 获取节点p2所在行的其他节点p3
p3.down.up = p3.up
p3.up.down = p3.down
cnt[ p3.y ] = cnt[ p3.y ] - 1 // 计数器减少
p3 = p3.right
End While
p2 = p2.down
End While
resume函数中需要修改对应列的计数:
While (p2 != p)
// 获取该列下的每一个节点p2
p3 = p2.right
While (p3 != p2)
// 获取节点p2所在行的其他节点p3
p3.down.up = p3
p3.up.down = p3
cnt[ p3.y ] = cnt[ p3.y ] + 1 // 计数器增加
p3 = p3.right
End While
p2 = p2.down
End While
dance函数中需要去查找cnt最小的列:
dance(depth):
If (head.right == head) Then
// 若head的右边就是head自己,则已经找到解
Return True;
End If
p = findMinCnt(head) // 从head右边的所有节点中找到cnt最小的
p2 = p.down
If (p2 == p) Then
// 当前列没有节点,则当前列一定不会被覆盖
Return false
End If
... ans[(p2.x - 1) / 81 + 1][((p2.x - 1) / 9) % 9 + 1] = (p2.x - 1) % 9 + 1 ...
小Ho:我知道了!我这加上这个优化。
相关文章推荐
- Android 进程间通信
- 构造方法__construct()与析构方法__destruct()
- MyEclipse添加Maven插件
- 翻转课堂心得
- 字符串当引号遇到美元
- QT自绘标题和边框
- 6-1认识CSS样式
- 《HTML5移动开发指南》——笔记4(离线应用)
- Jimoshi_用easyui做Struts2+Hibenate4学生实体类(Student)的增删改
- 广度优先搜索-抓住那头牛(算法基础 第8周)
- 特殊的引用”$this“的使用
- winform chart 控件之滚动条的放大与缩小功能
- 流程控制的使用CASE 语句用法
- 2016.6.19——C++杂记
- mysql 字符集设置
- 课程总结
- android你可能要用到的自定义View分享
- 离线保存浏览器网页
- Spring缓存注解@Cache,@CachePut , @CacheEvict,@CacheConfig使用
- Android新特性之CardView的简单使用