数独是一种经典的智力游戏。数独可以转化为精确覆盖问题,从而使用精确覆盖问题的通用解法:舞蹈链(DLX)来解决。本文将介绍一种通过通过回溯法与剪枝来解决数独的方法。经OJ测验,其效率与DLX相差无几,甚至比DLX略快。
9*9的数独是最常见的数独,因此用它举例
通常我们会主动关心那些周围填好的数比较多的格子,看能否推理出其中应该填什么。比如对于上图中第五行第一列的元素,只有填入4才是合法的。
当某个数已经填了8次,我们总能快速推理出剩下的一个在哪里。当填了6或7次的时候也总能找到一些线索。比如上图中可以推理出第四行第二列、第五行第三列,必定有一个位置填入3
为了叙述方便,我们做如下定义
结合人工解决数独的经验,我使用了以下策略进行剪枝:
朴素的上述过程,枚举操作集并统计选择数的复杂度为 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不同的是,本方法将操作集按选择数这一指标桶排序。DLX中应当也可用类似的方法,对列进行排序。
规模 | 耗时(秒) |
---|---|
1×1 | 0.001 |
4×4 | 0.001 |
9×9 | 0.002 |
16×16 | 0.017 |
25×25 | 0.064 |
36×36 | 19.5 |
#include
#include
#include
int maxv,e;
//Copyright 1800013060
bool victory;
char ans[36][37];
struct option
{
const bool isGrid;
int level;
option *l,*r;
option(bool b):isGrid(b){}
void remove();
void insert();
};
option* opRank[37];
void option::remove()
{
if(l)
l->r=r;
else opRank[level]=r;
if(r) r->l=l;
}
void option::insert()
{
l=NULL;
r=opRank[level];
if(opRank[level])
opRank[level]->l=this;
opRank[level]=this;
}
struct cell;
struct area;
struct areaValue;
struct areaValue:public option
{
area* src;
areaValue():option(false){}
void init(area*s)
{
src=s;
level=maxv;
insert();
}
void inc()
{
remove();
++level;
insert();
}
void dec()
{
remove();
--level;
insert();
}
};
struct area
{
cell* member[36];
areaValue val[36];
void allow(int v);
void limit(int v);
void init()
{
for(int i=0;i<maxv;i++)
val[i].init(this);
}
}row[36],col[36],sqr[36];
struct cell:public option
{
int limit[36];
area *row;
area *col;
area *sqr;
cell():option(true){}
void init()
{
memset(limit,0,sizeof(limit));
level=maxv;
insert();
}
void add(int v)
{
if(!limit[v])
{
remove();
--level;
insert();
row->val[v].dec();
col->val[v].dec();
sqr->val[v].dec();
}
++limit[v];
}
void minus(int v)
{
--limit[v];
if(!limit[v])
{
remove();
++level;
insert();
row->val[v].inc();
col->val[v].inc();
sqr->val[v].inc();
}
}
void fill(int v)
{
ans[row-(::row)][col-(::col)]=v+'A';
for(int i=0;i<maxv;i++)
add(i);
row->limit(v);
col->limit(v);
sqr->limit(v);
remove();
}
void unfill(int v)
{
insert();
ans[row-(::row)][col-(::col)]='-';
row->allow(v);
col->allow(v);
sqr->allow(v);
for(int i=0;i<maxv;i++)
minus(i);
}
}g[36][36];
void area::limit(int v)
{
for(int i=0;i<maxv;i++)
member[i]->add(v);
val[v].remove();
}
void area::allow(int v)
{
val[v].insert();
for(int i=0;i<maxv;i++)
member[i]->minus(v);
}
void init()
{
victory=false;
for(int i=0;i<maxv;i++)
for(int j=0;j<maxv;j++)
{
g[i][j].init();
g[i][j].row=row+i;
row[i].member[j]=&g[i][j];
g[i][j].col=col+j;
col[j].member[i]=&g[i][j];
g[i][j].sqr=sqr+(i/e)*e+j/e;
sqr[(i/e)*e+j/e].member[i%e*e+j%e]=&g[i][j];
}
for(int i=0;i<maxv;i++)
{
row[i].init();
col[i].init();
sqr[i].init();
}
}
void dfs(int dep)
{
if(dep==maxv*maxv)
{
victory=true;
for(int i=0;i<maxv;i++)
puts(ans[i]);
putchar('\n');
return;
}
if(opRank[0])
return;
for(int i=1;i<=maxv;i++)
{
if(opRank[i])
{
switch(opRank[i]->isGrid)
{
cell *cp;
areaValue *vp;
case true:
cp=(cell*)opRank[i];
for(int j=0;j<maxv;j++)
if(!cp->limit[j])
{
cp->fill(j);
dfs(dep+1);
if(victory) return;
cp->unfill(j);
}
break;
case false:
vp=(areaValue*)opRank[i];
for(int j=0;j<maxv;j++)
{
cp=vp->src->member[j];
if(!cp->limit[vp-(vp->src->val)])
{
cp->fill(vp-(vp->src->val));
dfs(dep+1);
if(victory) return;
cp->unfill(vp-(vp->src->val));
}
}
break;
}
return;
}
}
}
int main()
{
bool flag=false;
while(flag!=true)
{
scanf("%d",&e);
maxv=e*e;
init();
int cnt=0;
for(int i=0;i<maxv;i++)
{
if(scanf("%s",ans[i])==EOF)
{
flag=true;
break;
}
for(int j=0;j<maxv;j++)
if(ans[i][j]!='-')
{
g[i][j].fill(ans[i][j]-'A');
cnt++;
}
}
if(flag) break;
int st=clock();
dfs(cnt);
printf("%.3f\n",1.0*(clock()-st)/CLOCKS_PER_SEC);
}
return 0;
}