正文

数独(sudoku)的生成与破解(发表时间: 2007-1-25 21:06:00)

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/rickone/22806.html 复制链接

分享到:

标签:sudoku 数独 

数独(sudoku)的生成与破解
最近在网上比较流行的智力游戏。笔者本人也玩过,可以下个模拟游戏试试,简单的还可以,太难就无从下手了。虽然偶脑子不好使,但偶是计算机科班出身,怕你不成,老规矩,编程破解。

首先,偶是第一次做数独的程序,可能程序不强,以后有时间再改进改进。望高手评析。

还是把数独游戏的规则说一说吧,或许你是刚刚听说这个名字的朋友。数独(sudoku),起源于瑞士,于1970 年代由美国的一家数学逻辑游戏杂志首先发表,当时名为Number Place。及后在日本大力推广下得以发扬光大,于1984 年取名“数独”,即“独立的数字”的省略,在一个9x9的方格中,有81个小方格组成,然后又分9个大块,每块由3x3的方格组成,就是中国的九宫图,大九宫里面再套9个小九宫,共九九八十一个小格子,游戏开始前会有一些格子上写好了数,你需要在剩下的格子里填数,真到把所有格子填满,并且要求,任何一行或一列或者一个小九宫中没有相同的数字,当然你只能用1-9之间的9个数字。如下图就是一个数独。希望我解释清楚了,如果你还不清楚,用google搜索一下相关资料,点这里http://www.google.com/search?q=sudoku%20%E6%95%B0%E7%8B%AC&hl=zh-CN&lr=&nxpt=10.1471893728569764719035

好啦,言归正传。简单来说,我的破解法非常简单,就是超级无敌强硬大搜索,呵呵,别拿你想数独的那套来让计算机想,它比你可笨得多了,它只会照程序做事,做得快而已,快就是它的本事,其它的一概不会。

核心算法:深度优先搜索(其它形式的搜索也可以)

数据结构:如果用递归的形式写深搜,定义在函数dfs里的所有变量都可以看成是这里的数据结构,因为它们自动地被系统压入栈内,所以,省了,你唯一要做的就是一个二维数组,存放当前数独的状态。

当然有了这些,偶还不敢动手做,如果你做过马遍历的程序,大概会有点怕,那才8x8,这里是9x9,不来点‘启发式’谁敢动手写程序,有可能一个数独来几千几万个解,一个解要搜80层上下(估计),懂得树这个数据结构的人就会明白,80层是什么概念,1-9有9个数字,就是9叉树,至少是9^80量级的代价,什么?计算机反正算得快?也不行,再快的计算机遇到指数复杂度的程序也得变回傻子!谢天谢地,棋局尺寸是固定不变的,我们需要做的就是,剪枝。

偶的启发式思想来源于偶想数独的思路,数独之所以难是因为可行情况太多,把这种不确定性降低就会使它变得简单。某个格子上可以填的数字的个数就称为它的不确定度吧,首先,如果一开始空格比较少,那空格上的不确定度就小,数独也就相对容易,同时,随着填上去数字增多,剩余空格上的不确定度也会降低,如果某个格子上的不确定度降到1,那这个格子可以先确定下来,如果降到了0,哦,非常遗憾,在前面的填数中一定是填错了,剪枝也发生在这里,你不得不回退。

详细地说,如果把每个空格做为分枝,那要优先选择哪个分枝进行搜索呢?对每个空格计算它的权值,也就是它的不确定度,然后从中选出最小的一个进行搜索,同时放弃其它空格在这一层上的搜索!也就是说对剩余的空格交给子层处理。这样在某一个结点上的处理就包括两步:1,选择最佳空格,2,遍历这个空格的所有可行值,填入空格,递归。

OK,看程序:

#ifndef SUDOKU_RICK_0701_
#define SUDOKU_RICK_0701_
class CSudoku
{
 int map[9][9];
 int solves;
 int check(int,int,int*);
 void dfs();
public:
 CSudoku(int n=40);// 随机生成数独,n越大越难
 CSudoku(int *data);// 人工指定数独
 virtual ~CSudoku();
 void display();// 输出数独
 void resolve();// 解数独
};
#endif

#include "sudoku.h"
#include "stdio.h"
#include "stdlib.h"
#include "time.h"

CSudoku::CSudoku(int n)
{
 int i,j,k,m,mark[10],temp,blanks=0;
 srand(time(0));
 for(i=0;i<9;++i)
 {
  for(j=0;j<9;++j)
   map[i][j]=j+1;
  for(j=0;j<9;++j)
  {
   int a=rand()%9;
   int b=rand()%9;
   temp=map[i][a];
   map[i][a]=map[i][b];
   map[i][b]=temp;
  }
 }
 for(i=0;i<9;++i)
 {
  for(j=1;j<=9;++j)
   mark[j]=0;
  for(j=0;j<9;++j)
  {
   if(mark[map[j][i]])
   {
    for(k=8;k>=0;--k)
    {
     if(mark[map[j][k]]==0)
     {
      temp=map[j][i];
      map[j][i]=map[j][k];
      map[j][k]=temp;
      break;
     }
    }
   }
   else
   {
    mark[map[j][i]]=1;
   }
  }
 }
 for(i=0;i<9;++i)
 {
  for(j=1;j<=9;++j)
   mark[j]=0;
  for(j=8;j>=0;--j)
  {
   if(mark[map[j][i]])
   {
    map[j][i]=0;
    blanks++;
   }
   else
    mark[map[j][i]]=1;
  }
 }
 for(i=0;i<9;i+=3)
 {
  for(j=0;j<9;j+=3)
  {
   for(k=1;k<=9;++k)
    mark[k]=0;
   for(k=0;k<3;++k)
   {
    for(m=0;m<3;++m)
    {
     if(map[i+k][j+m]==0)
      continue;
     if(mark[map[i+k][j+m]])
     {
      map[i+k][j+m]=0;
      blanks++;
     }
     else
      mark[map[i+k][j+m]]=1;
    }
   }
  }
 }
 while(n>blanks)
 {
  m=rand()%81;
  i=m/9;
  j=m%9;
  if(map[i][j]>0)
  {
   map[i][j]=0;
   blanks++;
  }
 }
 printf("(randomized sudoku created with %d blanks.)\n",blanks);
}
CSudoku::CSudoku(int *data)
{
 int *pm=(int*)map;
 for(int i=0;i<81;++i)
  pm[i]=data[i];
}
CSudoku::~CSudoku()
{
 return;
}
void CSudoku::display()
{
 for(int i=0;i<9;++i)
 {
  for(int j=0;j<9;++j)
  {
   if(map[i][j]>0)
    printf("< %d >  ",map[i][j]);
   else
    printf("[   ]  ");
  }
  printf("\n");
 }
}
void CSudoku::resolve()
{
 solves=0;
 dfs();
 if(solves==0)
  printf("(sorry,this sudoku is a bad one.)\n");
}
int CSudoku::check(int y,int x,int *mark)
{
 int i,j,is,js,count=0;
 for(i=1;i<=9;++i)
  mark[i]=0;
 for(i=0;i<9;++i)
  mark[map[y][i]]=1;
 for(i=0;i<9;++i)
  mark[map[i][x]]=1;
 is=y/3*3;
 js=x/3*3;
 for(i=0;i<3;++i)
 {
  for(j=0;j<3;++j)
   mark[map[is+i][js+j]]=1;
 }
 for(i=1;i<=9;++i)
  if(mark[i]==0)
   count++;
 return count;
}
void CSudoku::dfs()
{
 int i,j,im=-1,jm,min=10;
 int mark[10];
 for(i=0;i<9;++i)
 {
  for(j=0;j<9;++j)
  {
   if(map[i][j])
    continue;
   int c=check(i,j,mark);
   if(c==0)
    return;
   if(c<min)
   {
    im=i;
    jm=j;
    min=c;
   }
  }
 }
 if(im==-1)
 {
  printf("No. %d:\n",++solves);
  display();
  return;
 }
 check(im,jm,mark);
 for(i=1;i<=9;++i)
 {
  if(mark[i]==0)
  {
   map[im][jm]=i;
   dfs();
  }
 }
 map[im][jm]=0;
}

#include <iostream>
#include "sudoku.h"
using namespace std;
int main()
{
 int data1[]=
 {4,9,0,0,0,6,0,2,7,
  5,0,0,0,1,0,0,0,4,
  6,0,0,0,0,8,0,0,3,
  1,0,4,0,0,0,0,0,0,
  0,6,0,0,0,0,0,5,0,
  0,0,0,0,0,0,2,0,8,
  7,0,0,2,0,0,0,0,5,
  8,0,0,0,9,0,0,0,1,
  3,4,0,5,0,0,0,6,2
 };
 int data2[]=
 {7,4,0,0,8,0,0,1,6,
  9,0,0,0,3,5,0,0,4,
  0,0,0,7,0,0,0,0,0,
  0,7,0,0,0,9,5,0,0,
  6,1,0,0,5,0,0,8,7,
  0,0,2,6,0,0,0,4,0,
  0,0,0,0,0,4,0,0,0,
  3,0,0,5,6,0,0,0,2,
  5,6,0,0,1,0,0,3,9
 };
 CSudoku s1(data1);
 s1.display();
 s1.resolve();
 CSudoku s2(data2);
 s2.display();
 s2.resolve();
 return 0;
}

代码里有很大部分实际上是在‘生成数独’,然后结果并不是我所料的那样,我用随机填充的方法,生成的大都是没有解的数独,如果空格太多,解是有,也太多。所以数独的生成似乎成了个难题。

数独的生成,最好的情况是,先得到一个完整的数独,然后根据难度需要,随机地挖一些空格出来,过程是相反的,所以可以保证有解。所以关键就是如何得到完整的数独,也要有一定的随机化。用上面的程序可以试验出来,如果我挖的空格越大,就越容易出解(相对于无解的情况),那偶就可以从一些有很多空格的数独出发,用解数独的程序,先解一个数独,那不就得到了完整的数独!也就是先只设定少数一些位置上的数字,然后用解数独程序得到完整数独,然后再挖一些空格出来,这样就得到一个绝对有解的数独,that's right!

偶的生成方法采用很简单的方法,因为可以通过上面的程序验证,当有72个空格时,可以很快得到一个数独解,偶就在一开始在每一行的随机位置上填上1-9的数字,这些初始数字就叫他们种子吧,不同的种子可以得到不同的数独,9行,每行9个位置,那有9的9次方,大概3亿多个不同情况,那我至少可以得到3亿多个不同的完整数独,再随机去掉不同数目的空格,那就可以生成相当数量的数独了!

改进了的数独生成程序及完整的数独代码:(比原来更短了)

#ifndef SUDOKU_RICK_0701_
#define SUDOKU_RICK_0701_
class CSudoku
{
 int map[9][9];
 int smod;
 int solves;
 int check(int,int,int*);
 void dfs();
public:
 enum{ANY=0,ALL=1};
 CSudoku(int n=40);// 随机生成数独,n越大越难
 CSudoku(int *data);// 人工指定数独
 virtual ~CSudoku();
 void display();// 显示数独
 int resolve(int mod=ALL);// 解数独
};
#endif

#include "sudoku.h"
#include "stdio.h"
#include "stdlib.h"
#include "time.h"

CSudoku::CSudoku(int n)
{
 int i,j;
 srand(time(0));
 do
 {
  for(i=0;i<9;++i)
  {
   for(j=0;j<9;++j)
    map[i][j]=0;
   j=rand()%9;
   map[i][j]=i+1;
  }
 }
 while(!resolve(ANY));

 // 挖窟窿
 for(int k=0;k<n;)
 {
  i=rand()%81;
  j=i%9;
  i=i/9;
  if(map[i][j]>0)
  {
   map[i][j]=0;
   ++k;
  }

 }
 //printf("(randomized sudoku created with %d blanks.)\n",blanks);
}
CSudoku::CSudoku(int *data)
{
 int *pm=(int*)map;
 for(int i=0;i<81;++i)
  pm[i]=data[i];
}
CSudoku::~CSudoku()
{
 return;
}
void CSudoku::display()
{
 for(int i=0;i<9;++i)
 {
  for(int j=0;j<9;++j)
  {
   if(map[i][j]>0)
    printf("< %d >  ",map[i][j]);
   else
    printf("[   ]  ");
  }
  printf("\n");
 }
}
int CSudoku::resolve(int mod)
{
 smod=mod;
 if(mod==ALL)
 {
  solves=0;
  dfs();
  return solves;
 }
 else if(mod==ANY)
 {
  try
  {
   dfs();
   return 0;
  }
  catch(int)
  {
   return 1;
  }
 }
 return 0;
}
int CSudoku::check(int y,int x,int *mark)
{
 int i,j,is,js,count=0;
 for(i=1;i<=9;++i)
  mark[i]=0;
 for(i=0;i<9;++i)
  mark[map[y][i]]=1;
 for(i=0;i<9;++i)
  mark[map[i][x]]=1;
 is=y/3*3;
 js=x/3*3;
 for(i=0;i<3;++i)
 {
  for(j=0;j<3;++j)
   mark[map[is+i][js+j]]=1;
 }
 for(i=1;i<=9;++i)
  if(mark[i]==0)
   count++;
 return count;
}
void CSudoku::dfs()
{
 int i,j,im=-1,jm,min=10;
 int mark[10];
 for(i=0;i<9;++i)
 {
  for(j=0;j<9;++j)
  {
   if(map[i][j])
    continue;
   int c=check(i,j,mark);
   if(c==0)
    return;
   if(c<min)
   {
    im=i;
    jm=j;
    min=c;
   }
  }
 }
 if(im==-1)
 {
  if(smod==ALL)
  {
   printf("No. %d:\n",++solves);
   display();
   return;
  }
  else if(smod==ANY)
  {
   throw(1);
  }
 }
 check(im,jm,mark);
 for(i=1;i<=9;++i)
 {
  if(mark[i]==0)
  {
   map[im][jm]=i;
   dfs();
  }
 }
 map[im][jm]=0;
}

#include <iostream>
#include "sudoku.h"
using namespace std;
int main()
{
 int data1[]=
 {4,9,0,0,0,6,0,2,7,
  5,0,0,0,1,0,0,0,4,
  6,0,0,0,0,8,0,0,3,
  1,0,4,0,0,0,0,0,0,
  0,6,0,0,0,0,0,5,0,
  0,0,0,0,0,0,2,0,8,
  7,0,0,2,0,0,0,0,5,
  8,0,0,0,9,0,0,0,1,
  3,4,0,5,0,0,0,6,2
 };
 int data2[]=
 {7,4,0,0,8,0,0,1,6,
  9,0,0,0,3,5,0,0,4,
  0,0,0,7,0,0,0,0,0,
  0,7,0,0,0,9,5,0,0,
  6,1,0,0,5,0,0,8,7,
  0,0,2,6,0,0,0,4,0,
  0,0,0,0,0,4,0,0,0,
  3,0,0,5,6,0,0,0,2,
  5,6,0,0,1,0,0,3,9
 };
 int blanks;
 cout<<"随机生成一个数独,输入空格数";
 cin>>blanks;
 CSudoku s(blanks);
 s.display();
 cout<<"开始解数独:"<<endl;
 s.resolve();
 return 0;
}

测试运行结果:


随机生成一个数独,输入空格数40
[   ]  < 7 >  < 8 >  [   ]  [   ]  [   ]  [   ]  [   ]  < 6 >
< 6 >  < 3 >  [   ]  < 7 >  [   ]  < 2 >  < 8 >  [   ]  < 5 >
< 2 >  [   ]  [   ]  < 8 >  [   ]  [   ]  [   ]  [   ]  < 9 >
< 3 >  < 9 >  [   ]  < 1 >  [   ]  [   ]  < 2 >  < 5 >  [   ]
[   ]  [   ]  [   ]  < 2 >  < 5 >  < 4 >  < 6 >  [   ]  < 3 >
< 4 >  [   ]  [   ]  [   ]  [   ]  [   ]  < 7 >  < 1 >  [   ]
[   ]  [   ]  [   ]  < 4 >  < 7 >  [   ]  [   ]  < 3 >  < 1 >
< 9 >  < 4 >  [   ]  < 6 >  < 2 >  < 1 >  < 5 >  < 8 >  [   ]
[   ]  [   ]  < 7 >  < 9 >  < 3 >  [   ]  [   ]  < 6 >  < 2 >
开始解数独:
No. 1:
< 1 >  < 7 >  < 8 >  < 5 >  < 4 >  < 9 >  < 3 >  < 2 >  < 6 >
< 6 >  < 3 >  < 9 >  < 7 >  < 1 >  < 2 >  < 8 >  < 4 >  < 5 >
< 2 >  < 5 >  < 4 >  < 8 >  < 6 >  < 3 >  < 1 >  < 7 >  < 9 >
< 3 >  < 9 >  < 6 >  < 1 >  < 8 >  < 7 >  < 2 >  < 5 >  < 4 >
< 7 >  < 8 >  < 1 >  < 2 >  < 5 >  < 4 >  < 6 >  < 9 >  < 3 >
< 4 >  < 2 >  < 5 >  < 3 >  < 9 >  < 6 >  < 7 >  < 1 >  < 8 >
< 5 >  < 6 >  < 2 >  < 4 >  < 7 >  < 8 >  < 9 >  < 3 >  < 1 >
< 9 >  < 4 >  < 3 >  < 6 >  < 2 >  < 1 >  < 5 >  < 8 >  < 7 >
< 8 >  < 1 >  < 7 >  < 9 >  < 3 >  < 5 >  < 4 >  < 6 >  < 2 >
No. 2:
< 1 >  < 7 >  < 8 >  < 5 >  < 4 >  < 9 >  < 3 >  < 2 >  < 6 >
< 6 >  < 3 >  < 9 >  < 7 >  < 1 >  < 2 >  < 8 >  < 4 >  < 5 >
< 2 >  < 5 >  < 4 >  < 8 >  < 6 >  < 3 >  < 1 >  < 7 >  < 9 >
< 3 >  < 9 >  < 6 >  < 1 >  < 8 >  < 7 >  < 2 >  < 5 >  < 4 >
< 7 >  < 8 >  < 1 >  < 2 >  < 5 >  < 4 >  < 6 >  < 9 >  < 3 >
< 4 >  < 2 >  < 5 >  < 3 >  < 9 >  < 6 >  < 7 >  < 1 >  < 8 >
< 8 >  < 6 >  < 2 >  < 4 >  < 7 >  < 5 >  < 9 >  < 3 >  < 1 >
< 9 >  < 4 >  < 3 >  < 6 >  < 2 >  < 1 >  < 5 >  < 8 >  < 7 >
< 5 >  < 1 >  < 7 >  < 9 >  < 3 >  < 8 >  < 4 >  < 6 >  < 2 >
Press any key to continue

rickone 2007/01/25

阅读(24722) | 评论(24) | 复制链接


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

评论人: 匿名 发布时间: 2011-8-24 10:25:00
写的十分清楚,简单易懂!
评论人: 匿名 发布时间: 2011-8-24 9:59:00
佩服呀!
评论人: 匿名 发布时间: 2011-3-28 13:51:00
用启发式搜索算法搜索明显会更好
评论人: tellme 发布时间: 2011-1-18 12:24:00
请问算法思路是什么呢?能否修改代码,以得到唯一的数独解……?谢谢,在下学生,急切希望得到指点,请发邮箱!谢谢!自己调试了很多,始终得不到满意结果,请作者帮助!
评论人: 匿名 发布时间: 2010-10-19 12:53:00
通常的做法是先准备一张9x9的空表, 随机填入数字, 当然要遵守数独规则, 填满整张表后,随机删除一个数字(如果是要生成对称数独题的话就删除对称的两个数字), 再试着用数独技巧甚至穷举法去解题. 如果可以求解, 则数字删除有效, 否则恢复上一步删除的数字, 再随机选择另外的数字删除直到无法再删除为止.题目的难度就取决于删除数字时用来解题的数独技巧的难度. 如果要了解更多关于数独方面的知识或是解题技巧可以到这个网站去: http://www.51isd.com/chs/sudoku
评论人: 匿名 发布时间: 2010-10-17 12:54:00
难道计算机系的只会此等sx解法么,看来我都可以转行去搞计算机了
评论人: ……………… 发布时间: 2009-8-17 11:37:00
可不可以加点注释…………我VB的,实在看不懂C
评论人: 小破孩 发布时间: 2009-6-19 9:39:00
很复杂,时间和空间的复杂度很大,不是很好
评论人: 匿名 发布时间: 2008-5-3 22:01:00
zhegedongxitaiguiyile 
评论人: 匿名 发布时间: 2008-5-2 15:12:00
感觉有问题,不过还是很强的,支持
评论人: 匿名 发布时间: 2008-2-16 17:08:00
佩服啊
评论人: xiaomi 发布时间: 2008-2-16 10:59:00
写个唯一解的程序吧
评论人: alpc12 发布时间: 2008-2-16 9:23:00
1.对难度的衡量不能直接用空格数.
2.随机填数然后找满解可能无解.
3.该方法可能产生大量不唯一解,大大降低了难度.
评论人: zwxtxr 发布时间: 2007-7-21 10:39:00
数独 的 生成 算法 是怎样的啊??  
评论人: 匿名 发布时间: 2007-6-14 11:49:00
本人也用C编写过数独的计算器 觉得也没什么算法 就是有点类似一个一个试直到得出答案 或者 检查到题本身有错就停止 其实计算机的运算速度是非常快的 只需要零点几秒到几秒钟就OK! 
评论人: 匿名 发布时间: 2007-4-18 17:38:00
真正数读的生成,一般只有一个解,能否生成一个唯一解的呢?
评论人: 3656 发布时间: 2007-3-20 13:31:00
在内存和时间需求都苛刻的地方,不能用复杂的结构和循环,把树,栈,递归什么都望了吧,动态malloc都别想!连二维数组都不用,一维,一次遍例,一次统计。
评论人: 123 发布时间: 2007-3-20 13:22:00
我们把这个做成手机游戏了,用最少的循环办最多的事
评论人: rick 发布时间: 2007-3-19 22:16:00
非常抱歉,我自己也看着晕,是网页显示的问题,拷下来整理一下会好看很多的~~;)
评论人: 匿名 发布时间: 2007-3-19 11:03:00
如果一个循环很多的算法的代码要共享,必要的地方要注释,不是每个人的编程思路都是一样的,不是每个人的理解能力都那么强,不是每个人都精通C语言。
评论人: rickone 发布时间: 2007-3-14 22:30:00
以上方法比较弱,研究数独的话,建议去一些专业网站。
评论人: wm.fang 发布时间: 2007-3-14 18:44:00
不是很明白
.....
评论人: 匿名 发布时间: 2007-1-30 21:34:00
在下学艺不精,很难看明白原代码的运算,能否给我一个大概的介绍,如何得出数独的算法?
评论人: goal00001111 发布时间: 2007-1-29 23:23:00
厉害!

发表评论

您的昵称: 昵称不填为“匿名”

您的Email: (可选)

评论内容:(字数请控制在500字以内)