一种高效的剪枝解数独策略

数独是一种经典的智力游戏。数独可以转化为精确覆盖问题,从而使用精确覆盖问题的通用解法:舞蹈链(DLX)来解决。本文将介绍一种通过通过回溯法与剪枝来解决数独的方法。经OJ测验,其效率与DLX相差无几,甚至比DLX略快。

人工解数独常用的方法

9*9的数独是最常见的数独,因此用它举例

看每个格子能填入什么数

一个例子
通常我们会主动关心那些周围填好的数比较多的格子,看能否推理出其中应该填什么。比如对于上图中第五行第一列的元素,只有填入4才是合法的。

看一个数能填在哪些地方

一个例子
当某个数已经填了8次,我们总能快速推理出剩下的一个在哪里。当填了6或7次的时候也总能找到一些线索。比如上图中可以推理出第四行第二列、第五行第三列,必定有一个位置填入3

剪枝

概念定义

为了叙述方便,我们做如下定义

  1. 格子:能填入一个数字的最小单元
  2. n:问题的规模,指数独表的边长
  3. 区域:n个不能填入相同数字的格子,包括方块、列、行三种
  4. 方块:形状为 ( n ) ∗ ( n ) \sqrt(n)*\sqrt(n) ( n)( n)的区域
  5. 列:从上到下连续n个格子组成的区域
  6. 行:从左到右连续n个格子组成的区域
  7. 操作:在某个格子中填入某个数字
  8. 合法操作:指不违背所处区域性质的操作
  9. 互斥:两个操作,当做了一个操作后,另一个操作不是合法操作
  10. 操作集:一些从规则上可以直接看出两两互斥,但是其中必须执行一个的操作的集合
  11. 完成:一个操作集中有某个操作被执行
  12. 选择数:一个操作集中合法操作的个数

剪枝策略

结合人工解决数独的经验,我使用了以下策略进行剪枝:

  1. 因为一个格子要填一个数,所以在每个格子中填1~16的操作,构成操作集。枚举所有这样的未完成操作集
  2. 因为一个区域每个数要填一次,所以在区域中不同位置填同一值的操作,构成操作集。枚举所有这样的未完成操作集
  3. 对上述步骤枚举得到的操作集按选择数排序
  4. 若选择数最少的为0,说明存在无法完成的操作集,之前执行的操作有误,回溯。否则进入步骤5
  5. 选取选择数最少的未完成操作集S。当取法不唯一时任取一个即可
  6. 执行S中的一个合法操作,并递归到下一层重复上述步骤。若没有找到解,撤销该操作。重复此步骤直到找到解或S中合法操作全都被尝试过

复杂度分析

朴素的上述过程,枚举操作集并统计选择数的复杂度为 O ( n 3 ) O(n^3) O(n3),排序的复杂度为 O ( n 2 ) O(n^2) O(n2)。寻找S的复杂度为 O ( n ) O(n) O(n)。枚举S中合法操作并执行的复杂度为 O ( n ) O(n) O(n)(不考虑下一层递归所花的时间)。以上四个步骤没有嵌套关系,故每次调用递归回溯函数的时间复杂度为它们相加的结果 O ( n 3 ) O(n^3) O(n3)。事实上,因为S的选择数是所有未完成操作集中最小的, S中合法操作的个数,即需要执行操作的次数,基本上是 O ( 1 ) O(1) O(1)的。

优化

显然,我们应该将枚举操作集,排序操作集的复杂度转移到执行操作上,以此降低单次递归的时间复杂度。也就是说,我们要维护一个支持执行操作,撤销操作和查询选择数最小的操作集的数据结构

每次执行和撤销操作,选择数发生变化的操作集的数量是O(n)的。如果有(n+1)个桶,分别表示选择数为0~n的操作集,那么我们在执行操作的时候,只需要把受到影响的操作集从一个桶里拿出来,放到另外一个桶里,然后将完成的操作集从桶中拿出,不放回。撤销操作的步骤与执行相反。而查询操作集的时候,只需从小到大依次查看每个桶里是否有操作集。我们可以通过维护(n+1)个操作集双向链表来实现这(n+1)个桶。这样,每次移动操作集都是O(1)的,每次执行或撤销操作的复杂度为O(n),查询的复杂度也是O(n)。

于是,仅剩的问题是,如何高效地枚举受到影响的操作集?一个直观的思路是:每执行一个操作(格子=g,填入值=v),g中填入任何数的操作,以及g所在区域中所有格子填入v的操作,都会变成非法。那么当一个操作变为非法的时候,就将该操作所属的4个操作集(一个第一类和三个第二类)的选择数都减1。当撤销时再将这些操作变回合法,把操作集的选择数都加1就行了。

然而这个想法是错误的。一个操作是非法,可能是多个因素的限制。比如下图中,左上角的位置不能填入3。我们撤销它右面的3,它仍然不能填3。我们撤销它里面的5,它还是不能填3。
在这里插入图片描述

让我们设想,每个操作(格子=g,填入值=v)之上都可以加若干个“盖子”,每个盖子表示g所属的某个区域中填入了v,或者g中已经填入了数。显然,一个操作合法当且仅当它没有被盖住。当盖子从少变多或者从多变少,该操作所属的4个操作集均不受影响。当盖子从无到有或从有到无,则该操作合法性发生改变,会影响其所属的4个操作集。每次执行操作会为O(n)个操作加上盖子,加盖子是O(1)的,故执行操作在O(n)时间内就可以完成对所有被影响的操作集的移动。撤销操作会拿掉O(n)个操作的盖子,复杂度同理。

以上,我们成功地维护了一个支持以O(n)复杂度执行操作,撤销操作和查询选择数最小的操作集的数据结构。这使得每次递归的复杂度为O(n)。

一些讨论

与DLX的联系

本方法在维护的数据结构方面,与DLX有极大的相似之处。每个操作集实际上对应精确覆盖问题中的一个列。每次执行操作,实际上就是DLX中移除若干列的过程。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值