吴昊品游戏核心算法 Round 2 —— 吴昊教你玩数独AI(DFS)(HDOJ 1426)

Round 2之后,我会给出一些棋类游戏的模拟算法或者是AI算法。以下节选自百度百科(翻阅文史性质的文献资料,我认为维基百科更加靠谱,原因,你懂的,但是,我认为翻阅学术文献资料,百度百科更加靠谱,或者说,描述地更加完备一些)

  

数 独(すうどく,Sudoku)是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一 列、每一个粗线宫内的数字均含1-9,不重复。 每一道合格的数独谜题都有且仅有唯一答案,推理方法也以此为基础,任何无解或多解的题目都是不合格的。这里给出了一点,也就是说,任何一个合格的数独问题 都是有唯一解的,这倒是给了AI很多方便。同时,在《编程之美》的P91和P307中都有关于数独描述的章节,一个是《构造数独》,一个是《唯一解数 独》,这里仅仅提出,毕竟,不是我的一些想法,不纳入本文。

关于人工解数独的技巧还是很多的,这里引自百度百科的一些:解题的本质有二:隐性唯一解(Hidden Single)及显性唯一(Naked Single),他们的名称是在候选数法的基础上命名的。

根据解题本质发展出来的解题方法有二种:

摒除法

1.摒除法:用数字去找单元内唯一可填空格,称为摒除法,数字可填唯一空格称为摒余解(隐性唯一解)。

根据不同的作用范围,摒余解可分为下述三种:

1.1 数字可填唯一空格在「宫」单元称为宫摒余解(Hidden Single in Box),这种解法称宫摒除法。

1.2 数字可填唯一空格在「行」单元称为行摒余解(Hidden Single in Row),这种解法称行摒除法。

1.3 数字可填唯一空格在「列」单元称为列摒余解(Hidden Single in Column),这种解法称列摒除法。

1.4 行摒余解和列摒余解合称行列摒余解(Hidden Single in Line)。

1.5 得到行列摒余解的方法称为行列摒除法。

余数法

 

2.余数法:用格位去找唯一可填数字,称为余数法,格位唯一可填数字称为唯余解(Naked Single)。

余数法是删减等位群格位(Peer)已出现的数字的方法,每一格位的等位群格位有 20 个,如图七所示。

辅助解法

3.上述方法称为基础解法(Basic Techinques),其他所有的解法称为进阶解法(Advanced Techniques),是在补基本解法之不足,所以又称辅助解法。

进阶解法包括:区块摒除法(Locked Candidates)、数组法(Subset)、四角对角线(X-Wing)、唯一矩形(Unique Rectangle)、全双值坟墓(Bivalue Universal Grave)、单数链(X-Chain)、异数链(XY-Chain)及其他数链的高级技巧等等。已发展出来的方法有近百种之多。

其中前两种加上基础解法为一般数独书中介绍并使用的方法,同时也是大部分人可以理解并掌握的数独解题技法。

4.通过基础解法出数只需一种解法,摒除法或唯余法,超出此范围而需要施加进阶解法时,解题点需要进阶解法协助基础解法来满足隐性唯一或显性唯一才能出数,该解题点的解法需要多个步骤协力完成,因此称做组合解法。

5.解题必须以逻辑为依归,猜测的方法被称为“暴力型”解法(Brute Force),这不是提倡数独的本意。

 

问题来源于HDOJ(杭电的OJ)——

自从2006年3月10日至11日的首届数独世界锦标赛以后,数独这项游戏越来越受到人们的喜爱和重视。
据说,在2008北京奥运会上,会将数独列为一个单独的项目进行比赛,冠军将有可能获得的一份巨大的奖品———HDU免费七日游外加lcy亲笔签名以及同hdu acm team合影留念的机会。
所以全球人民前仆后继,为了奖品日夜训练茶饭不思。当然也包括初学者linle,不过他太笨了又没有多少耐性,只能做做最最基本的数独题,不过他还是想得到那些奖品,你能帮帮他吗?你只要把答案告诉他就可以,不用教他是怎么做的。

数独游戏的规则是这样的:在一个9x9的方格中,你需要把数字1-9填写到空格当中,并且使方格的每一行和每一列中都包含1-9这九个数字。同时还要保 证,空格中用粗线划分成9个3x3的方格也同时包含1-9这九个数字。比如有这样一个题,大家可以仔细观察一下,在这里面每行、每列,以及每个3x3的方 格都包含1-9这九个数字。

 

 

这里同样略去了输入输出的样例,只给出AI算法的相关思想。

首 先,我想聊聊棋盘的数据结构。为什么将此与一般的数据结构分开呢?来源于棋盘的独特性,因为,棋盘是稳定的,自己不能随意地伸缩,所以,一般情况下都可以 采用一个二维数组来装载。不像一些DBMS系统,由于其数据结构必须要具备一定的灵活性,所以只能采用链表的方式来伸缩。

这里,定义一个map[10][10],并用一个结构体P中的x,y两个元素来表征(x,y)坐标。由于数独往往是9*9的盘面,所以,这里需要81个点。这里有一个非常好的想法,就是利用结构体的方式来描绘点,可以更加便于操作。

关于模拟算法和人工智能(AI)算法,其并不是对立的,首先,你选择的数据结构的好与坏直接联系到你模拟的效果,至于棋类的AI,一般一个像样的搜索算法都是可以搞得定的,比如这里的DFS,所以,我认为你模拟地好不好是关键。

这里的亮点有二:

(1)首先,用一个结构体来装载点,这里增加了描述的自由度!

(2)在judge函数中对于匹配要求的模拟,模拟地很完美。首先,要判定一个数独被填满之后是否符合要求,需要满足三个条件:(1)行完全匹配(2)列完全匹配(3)任何一个独立的九宫格的数字不重复,而第三个匹配最为精彩,在于代码中利用乘机+取模的办法找到了任何一个独立九宫格的起点!

源码面前,了无秘密!(这个是已经编译通过的源代码)


  1 #include<stdio.h>
  2 
  3  
  4 
  5  int flag=0,num,map[10][10];//定义一个棋盘,num代表未填方块的个数
  6 
  7  
  8 
  9  typedef struct
 10 
 11  {
 12 
 13    int x,y;       
 14 
 15  }P;
 16 
 17  
 18 
 19  P p[81];
 20 
 21  
 22 
 23  int judge(int n,int k)
 24 
 25  {
 26 
 27    int a,b;
 28 
 29    for(a=0;a<9;a++)
 30 
 31    {
 32 
 33      if(map[p[n].x][a]==k&&a!=p[n].y)//列完全匹配
 34 
 35        return 0;
 36 
 37      if(map[a][p[n].y]==k&&a!=p[n].x)//行完全匹配
 38 
 39        return 0;               
 40 
 41    }   
 42 
 43    int x=(p[n].x/3)*3;
 44 
 45    int y=(p[n].y/3)*3;//找到那个?所在的九宫格
 46 
 47    for(a=0;a<3;a++)
 48 
 49      for(b=0;b<3;b++)
 50 
 51      {
 52 
 53        if(map[a+x][b+y]==k&&((a+x)!=p[n].x)&&(b+y)!=p[n].y)
 54 
 55          return 0//新填入的数与九宫格任意的8个数都不重复               
 56 
 57      }
 58 
 59    return 1;
 60 
 61  }
 62 
 63  
 64 
 65  void dfs(int n)
 66 
 67  {
 68 
 69    int i;
 70 
 71    if(num==n)//因为最初是dfs(0),也就是说如果所有空被填完,就返回
 72 
 73    {
 74 
 75      flag=1;
 76 
 77      return;          
 78 
 79    }    
 80 
 81    for(i=1;i<=9;i++)//分别尝试填入1--9
 82 
 83    {
 84 
 85      if(judge(n,i))
 86 
 87      {
 88 
 89        map[p[n].x][p[n].y]=i;
 90 
 91        dfs(n+1);//在第n空的基础上搜索第n+1空
 92 
 93        if(flag) return;
 94 
 95        map[p[n].x][p[n].y]=0;//不行的话,退回原来的状态             
 96 
 97      }              
 98 
 99    }
100 
101  }
102 
103  
104 
105  int main()
106 
107  {
108 
109    int cas=0,g,f;//g,f分别代表x,y坐标,cas代表有不同的数独难题,但是,第一次输出是不带回车的
110 
111    char ch1[2];//为什么要多出一个空间?考虑到输入数据中每个数之间是有空格的
112 
113    while(scanf("%s",ch1)!=EOF)//每次读入一个数
114 
115    {
116 
117      num=0;
118 
119      flag=0;
120 
121      //这里,由于while循环里面已经有了一次读数,所以对第一次读数必须单独对待
122 
123      if(ch1[0]!='?')
124 
125      {
126 
127        map[0][0]=ch1[0]-'0';              
128 
129      }                     
130 
131      else
132 
133      {
134 
135        p[num].x=p[num].y=0;
136 
137        num++;
138 
139        map[0][0]=0;   
140 
141      }    
142 
143      for(g=0;g<9;g++)
144 
145      {
146 
147        for(f=0;f<9;f++)
148 
149        {
150 
151          if((g!=0||f!=0))
152 
153          {
154 
155            scanf("%s",ch1);
156 
157            if(ch1[0]!='?')
158 
159              map[g][f]=ch1[0]-'0';//将字符串存储的棋盘转换为二维数组存储
160 
161            else
162 
163            {
164 
165              map[g][f]=0;
166 
167              p[num].x=g;
168 
169              p[num].y=f;   
170 
171              num++;
172 
173            }               
174 
175          }               
176 
177        }               
178 
179      }
180 
181      if(cas) printf("\n");
182 
183      cas++;
184 
185      dfs(0);//深度优先搜索
186 
187      //输出
188 
189      for(g=0;g<9;g++)
190 
191      {
192 
193        for(f=0;f<9;f++)
194 
195        {
196 
197          if(f==0)
198 
199            printf("%d",map[g][f]);
200 
201          else
202 
203            printf(" %d",map[g][f]);//对输出数据空格的常规性处理               
204 
205        }               
206 
207        printf("\n");
208 
209      }
210 
211    }
212 
213    return 0;   
214 
215  }
216 
217 

posted on 2013-02-27 20:21  吴昊系列  阅读(1454)  评论(6编辑  收藏  举报

导航