数独算法-递归与回溯
2016-08-08 14:19
776 查看
1.概述
数独(Sudoku)是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含1-9,不重复。1)终盘数量:
数独中的数字排列千变万化,那么究竟有多少种终盘的数字组合呢?
6,670,903,752,021,072,936,960(约为6.67×10的21次方)种组合,2005年由Bertram Felgenhauer和Frazer Jarvis计算出该数字,并将计算方法发布在他们网站上,如果将等价终盘(如旋转、翻转、行行对换,数字对换等变形)不计算,则有5,472,730,538个组合。数独终盘的组合数量都如此惊人,那么数独题目数量就更加不计其数了,因为每个数独终盘又可以制作出无数道合格的数独题目。
2)标准数独:
目前(截止2011年)发现的最少提示数9×9标准数独为17个提示,截止2011年11月24日16:14,共发现了非等价17提示数谜题49151题,此数量仍在缓慢上升中,如果你先发现了17提示数的题目,可以上传至“17格数独验证”网站,当然你也可以在这里下载这49151题。
Gary McGuire的团队在2009年设计了新的算法,利用Deadly Pattern的思路,花费710万小时CPU时间后,于2012年1月1日提出了9×9标准数独不存在16提示唯一解的证明,继而说明最少需要17个提示数。并将他们的论文以及源代码更新在2009年的页面上。
以上内容来自于百度百科。
2.算法实现(Java)
网络上有很多解数独的算法,例如舞蹈链算法、遗传算法等。参考各种算法的性能比较:递归回溯对数独情有独钟。
本文解数独用的是候选数法(人工选择)+万能搜索法,搜索+剪枝(递归+回溯),参考博文:
数独算法及源代码
1)未优化的算法-只有递归回溯(单解或多解)
从第一个位置开始依次检索所有格子(暴力),执行时间会比较长。多解与单解:很简单,在找到解的语句返回false表示继续递归寻解,返回true表示停止寻解(不会复位,不回溯)
package com.sudoku; import java.util.Date; public class Sudoku { private int[][] matrix = new int[9][9];//注意下标从0开始 private int count=0;//解的数量 private int maxCount = 1;//解的最大数量 //输入格式要求0作为占位符(表示待填),只接受数字字符串,长度为81位 public Sudoku(String input,int maxCount) throws Exception{ if(input==null||input.length()!=81||!input.matches("[0-9]+")) throw new Exception("必须为81位长度的纯数字字符串"); init(input); this.maxCount = maxCount; } public Sudoku(String input) throws Exception{ this(input,1); } public Sudoku(){ } public int getCount(){ return count; } //初始化数独 private void init(String input){ for(int i=0;i<input.length();i++) { String s = input.substring(i, i+1); int value = Integer.parseInt(s); matrix[i/9][i%9]=value; } } //万能解题法的“搜索+剪枝”,递归与回溯 //从(i,j)位置开始搜索数独的解,i和j最大值为8 private boolean execute(int i,int j){ //寻找可填的位置(即空白格子),当前(i,j)可能为非空格,从当前位置当前行开始搜索 outer://此处用于结束下面的双层循环 for(int x=i;x<9;x++){ for(int y=0;y<9;y++){ if(matrix[x][y]==0){ i=x; j=y; break outer; } } } //如果从当前位置并未搜索到一个可填的空白格子,意味着所有格子都已填写完了,所以找到了解 if(matrix[i][j]!=0){ count++; System.out.println("第"+count+"种解:"); output(); if(count==maxCount) return true;//return true 表示只找寻一种解,false表示找所有解 else return false; } //试填k for(int k=1;k<=9;k++){ if(!check(i,j,k)) continue; matrix[i][j] = k;//填充 //System.out.println(String.format("(%d,%d,%d)",i,j,k)); if(i==8&&j==8) {//填的正好是最后一个格子则输出解 count++; System.out.println("第"+count+"种解:"); output(); if(count==maxCount) return true;//return true 表示只找寻一种解,false表示找所有解 else return false; } //计算下一个元素坐标,如果当前元素为行尾,则下一个元素为下一行的第一个位置(未填数), //否则为当前行相对当前元素的下一位置 int nextRow = (j<9-1)?i:i+1; int nextCol = (j<9-1)?j+1:0; if(execute(nextRow,nextCol)) return true;//此处递归寻解,若未找到解,则返回此处,执行下面一条复位语句 //递归未找到解,表示当前(i,j)填k不成功,则继续往下执行复位操作,试填下一个数 matrix[i][j] = 0; } //1~9都试了 return false; } public void execute(){ execute(0,0);//从第一个位置开始递归寻解 } //数独规则约束,行列宫唯一性,检查(i,j)位置是否可以填k private boolean check(int i,int j,int k){ //行列约束,宫约束,对应宫的范围 起始值为(i/3*3,j/3*3),即宫的起始位置行列坐标只能取0,3,6 for(int index=0;index<9;index++){ if(matrix[i][index]==k) return false; if(matrix[index][j]==k) return false; if(matrix[i/3*3+index/3][j/3*3+index%3]==k) return false; } return true; } public void output(){ for(int i=0;i<9;i++){ for(int j=0;j<9;j++) System.out.print(matrix[i][j]); System.out.println(); } } public static void main(String[] args) { try { //Sudoku sudoku = new Sudoku("000000000000000012003045000000000036000000400570008000000100000000900020706000500"); Sudoku sudoku = new Sudoku("123456789456789123789123456234567891567891234891234567345000000000000000000000000",10); //Sudoku sudoku = new Sudoku(); sudoku.output(); Date begin = new Date(); sudoku.execute(); System.out.println("执行时间"+(new Date().getTime()-begin.getTime())+"ms"); if(sudoku.getCount()==0) System.out.println("未找到解"); } catch (Exception e) { e.printStackTrace(); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
执行效果:
原数独:
123456789 456789123 789123456 234567891 567891234 891234567 345000000 000000000 000000000
1
2
3
4
5
6
7
8
9
2)优化算法-添加(唯一法或唯余法、摒除法、三链数删减法)
由于前面一种未经过优化搜索条件,属于“暴力型”解法(Brute Force),若碰到需要递归非常大的空间时,消耗时间将是非常长的,还有可能会抛出内存溢出的异常。如果按照人的思维去解数独,绝对不会像计算机一样呆呆的一个一个地去试,相反,人工解数独首先考虑的是将候选数最少(通常为1,必填)的格子先肯定的填上去,各种方法都用尽后,所谓山穷水尽时才会考虑试填,(即计算机的运作方式:递归回溯),而试填时也是从最少的候选数的格子开始(通常为2),这样能有效的找到解,而计算机只能使用暴力。所以,在算法中加上人工智能选择的话,可以大大提高执行效率。基本解题方法:隐性唯一解(Hidden Single)及显性唯一解(Naked Single),摒除法,余数法,候选数法
进阶解题方法:区块摒除法(Locked Candidates)、数组法(Subset)、四角对角线(X-Wing)、唯一矩形(Unique Rectangle)、全双值坟墓(Bivalue Universal Grave)、单数链(X-Chain)、异数链(XY-Chain)及其他数链的高级技巧等等。参考度娘:数独技巧
要仿照人工求解模式,需要采用候选数法对候选数进行删减法,其中可以应用到唯一(余)法,摒除法(行列宫)等。对应关系:
唯一(余)法:某个格子的候选数只剩下一个数字,则该数字必填如该格子。对应于唯一候选数法
摒除法:如果某个数字在某宫所有格子的所有候选数中总共只出现一次,则该数字必填入候选数包含它的那个格子中。行列情况同理。对应于隐性唯一候选数法。
三链数删减法:找出某一列、某一行或某一个九宫格中的某三个宫格候选数中,相异的数字不超过3个的情形,
进而将这3个数字自其它宫格的候选数中删减掉的方法就叫做三链数删减法。
这样程序执行流程是:
反复应用候选数删减法寻找必填项,直到候选数未发生变化(即找不到必填项了)
然后才递归寻解(如果上一步骤找到了解,那递归寻解只输出解了)
package com.sudoku; import java.util.Date; import java.util.HashSet; import java.util.Set; public class Sudoku { private int[][] matrix = new int[9][9];//注意下标从0开始 private String[][] candidature= new String[9][9];//表示候选数 private int count=0;//用于统计解的数量 private int maxCount = 1;//解的最大数量 //输入格式要求0作为占位符(表示待填),只接受数字字符串,长度为81位 public Sudoku(String input,int maxCount) throws Exception{ if(input==null||input.length()!=81||!input.matches("[0-9]+")) throw new Exception("必须为81位长度的纯数字字符串"); init(input); output(); this.maxCount = maxCount<=0?1:maxCount; if(!isValid()) throw new Exception("无效数独(有数字重复)"); if(!initCandidature()) throw new Exception("不合格数独(无解数独)"); } public Sudoku(String input) throws Exception{ this(input,1); } public Sudoku(){ } public int getCount(){ return count; } //初始化数独和候选数 private void init(String input){ for(int i=0;i<input.length();i++) { String s = input.substring(i, i+1); int value = Integer.parseInt(s); matrix[i/9][i%9]=value; } } //校验给出的数独题目是否为有效数独(即某行列宫中有重复的数字则无效) private boolean isValid(){ Set<Integer> rowSet = new HashSet<Integer>(); Set<Integer> colSet = new HashSet<Integer>(); Set<Integer> gridSet = new HashSet<Integer>(); for(int x=0;x<9;x++){//对应于行列宫号,对应宫的起始位置为(x/3*3,x%3*3) 取余与乘除优先级相同 rowSet.clear(); colSet.clear(); gridSet.clear(); for(int index=0;index<9;index++){ if(matrix[x][index]>0&&!rowSet.add(matrix[x][index])){ //行重复 System.out.println(String.format("数独无效,第%d行重复!",x+1)); return false; } if(matrix[index][x]>0&&!colSet.add(matrix[index][x])){//列重复 System.out.println(String.format("数独无效,第%d列重复!",x+1)); return false; } if(matrix[x/3*3+index/3][x%3*3+index%3]>0&&!gridSet.add(matrix[x/3*3+index/3][x%3*3+index%3])){ System.out.println(String.format("数独无效,第%d宫重复!",x+1)); return false;//宫重复 } } } return true; } //初始化候选数(唯一法或唯余法),数独无解返回false private boolean initCandidature() throws Exception{ for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ if(matrix[i][j]>0) continue; candidature[i][j]=""; for(int k=1;k<=9;k++){ if(check(i,j,k)) { candidature[i][j] += k; } } //如果待填格子候选数个数为0,不合格数独(无解数独) if(candidature[i][j]==null||candidature.length==0) { return false;//无解数独 } //候选数个数为1,对应于唯一法或唯余法,可以100%的将该候选数填入该格子中,并重新计算候选数 if(candidature[i][j].length()==1){ int k = Integer.parseInt(candidature[i][j]); matrix[i][j] = k; System.out.println(String.format("唯一(余)法必填项(%d,%d,%d)",i,j,k)); deleteCandidature(i,j,k); } //System.out.println(String.format("(%d,%d)",i,j)+"->"+candidature[i][j]); } } return true; } //删除(i,j)等位格群上的候选数k,当(i,j)上可以肯定的填入数字k时(等位格局包含除自身外共20个格子) //每次调用此方法后,候选数发生了变化,需要再次检查唯一(余)性质 //只要有一个候选数发生了删减,则返回true private boolean deleteCandidature(int i,int j,int k){ boolean change = false; for(int index=0;index<9;index++){ if(matrix[i][index]==0&&candidature[i][index]!=null&&candidature[i][index].contains(""+k)) { candidature[i][index] = candidature[i][index].replace(""+k,""); change = true; } if(matrix[index][j]==0&&candidature[index][j]!=null&&candidature[index][j].contains(""+k)){ candidature[index][j] = candidature[index][j].replace(""+k,""); change = true; } if(matrix[i/3*3+index/3][j/3*3+index%3]==0&&candidature[i/3*3+index/3][j/3*3+index%3]!=null &&candidature[i/3*3+index/3][j/3*3+index%3].contains(""+k)){ candidature[i/3*3+index/3][j/3*3+index%3] = candidature[i/3*3+index/3][j/3*3+index%3].replace(""+k,""); change = true; } } return change; } //唯一法或唯余法或唯一候选数法,检查每个格子候选数的个数是否为1 //此为最基础的方法、应用其他方法发生了删减候选数时都要应用此方法检查一遍 private boolean single(){ System.out.println("唯一法或唯余法:"); boolean change = false;//表示是否候选数是否发生变化(当有删除候选数操作时则发生了变化) for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ if(matrix[i][j]==0&&candidature[i][j].length()==1){ int k = Integer.parseInt(candidature[i][j]); matrix[i][j] = k; System.out.println(String.format("唯一(余)法必填项(%d,%d,%d)",i,j,k)); if(deleteCandidature(i,j,k)) change = true; } } } return change;//若无删减候选数操作,意味着一个必填项都没有找到返回false } //摒除法或隐性唯一候选数法,某个数字候选数只在该宫(行列)中的某一个格子出现(按照数字),即在该宫所有格子所有候选数中总共只出现一次。 private boolean exclude(){ System.out.println("摒除法:"); boolean change = false;//表示是否候选数是否发生变化(当有删除候选数操作时则发生了变化) int rowCount = 0;//行循环时,用于统计数字k出现的次数 int colCount = 0;//列循环时,用于统计数字k出现的次数 int gridCount = 0;//宫循环时,用于统计数字k出现的次数 int rowPos = 0;//行循环时,用于标识k最后一次出现的位置 int colPos = 0;//列循环时,用于标识k最后一次出现的位置 int gridPos = 0;//宫循环时,用于标识k最后一次出现的位置 int gridFirstPos = 0;//宫循环时,用于标识k出现的第1次位置 int gridSecondPos = 0;//宫循环时,用于标识k出现的第2次位置 for(int k=1;k<9;k++){ for(int x=0;x<9;x++){//行列宫循环次数 rowCount=0; colCount=0; gridCount=0; rowPos = 0; colPos=0; gridPos =0; for(int index=0;index<9;index++){ //行,k统计 if(matrix[x][index]==0&&candidature[x][index].contains(""+k)){ rowCount++; rowPos = index;//记录k在最后一次出现的位置 } //列,k统计 if(matrix[index][x]==0&&candidature[index][x].contains(""+k)){ colCount++; colPos = index;//记录k在最后一次出现的位置 } //宫,k统计 if(matrix[x/3*3+index/3][x%3*3+index%3]==0&&candidature[x/3*3+index/3][x%3*3+index%3].contains(""+k)){ gridCount++; gridPos = index;//记录k在最后一次出现的位置 if(gridCount==1){//k第一次出现的位置 gridFirstPos = index; } if(gridCount==2) gridSecondPos=index; } } if(matrix[x][rowPos]==0&&rowCount==1){ //表示该格子只能填入k matrix[x][rowPos]=k; System.out.println(String.format("行摒除法必填项(%d,%d,%d)",x,rowPos,k)); if(deleteCandidature(x,rowPos,k)&&single())//删除等位格群上的候选数k change = true; } if(matrix[colPos][x]==0&&colCount==1){ //表示该格子只能填入k matrix[colPos][x]=k; System.out.println(String.format("列摒除法必填项(%d,%d,%d)",colPos,x,k)); if(deleteCandidature(colPos,x,k)&&single())//删除等位格群上的候选数k change = true; } if(matrix[x/3*3+gridPos/3][x%3*3+gridPos%3]==0&&gridCount==1){ //表示该格子只能填入k matrix[x/3*3+gridPos/3][x%3*3+gridPos%3]=k; System.out.println(String.format("宫摒除法必填项(%d,%d,%d)",x/3*3+gridPos/3,x%3*3+gridPos%3,k)); if(deleteCandidature(x/3*3+gridPos/3,x%3*3+gridPos%3,k)&&single())//删除等位格群上的候选数k change = true; } //特殊条件:某一个数字在某一个宫中恰好只出现2次或3次,并且出现的位置恰好形成一条线(行或列), //则可以删除该线上的其它宫格中的这个数字 //恰好只出现一次时,摒除法可以处理 if(gridCount==2){ if(gridFirstPos/3==gridPos/3){//恰好同行,则删除同行上的数字k int row = x/3*3+gridFirstPos/3;//行号 if(cutCandidature(row*9+x%3*3+gridFirstPos%3,row*9+x%3*3+gridPos%3,-1,""+k,1,1)){ if(single()) change = true; System.out.println(String.format(x+"宫中数字"+k+"恰好只出现在同行的两格:(%d,%d,"+ candidature[row][x%3*3+gridFirstPos%3]+")(%d,%d,"+candidature[row][x%3*3+gridPos%3]+")" ,row,x%3*3+gridFirstPos%3,row,x%3*3+gridPos%3)); } }else if(gridFirstPos%3==gridPos%3){//恰好同列,则删除同列上的其他数字k int col = x%3*3+gridFirstPos%3;//列号 if(cutCandidature((x/3*3+gridFirstPos/3)*9+col,(x/3*3+gridPos/3)*9+col,-1,""+k,2,1)){ if(single()) change = true; System.out.println(String.format(x+"宫中数字"+k+"恰好只出现在同列的两格:(%d,%d,"+ candidature[x/3*3+gridFirstPos/3][col]+")(%d,%d,"+candidature[x/3*3+gridPos/3][col]+ ")",x/3*3+gridFirstPos/3,col,x/3*3+gridPos/3,col)); } } } if(gridCount==3){//恰好出现3次 if(gridFirstPos/3==gridSecondPos/3 && gridFirstPos/3==gridPos/3){//恰好3个同行 int row = x/3*3+gridFirstPos/3;//行号 if(cutCandidature(row*9+x%3*3+gridFirstPos%3,row*9+x%3*3+gridSecondPos%3,row*9+x%3*3+gridPos%3,""+k,1,1)){ if(single()) change = true; System.out.println(String.format(x+"宫中数字"+k+"恰好只出现在同行的三格:(%d,%d,"+ candidature[row][x%3*3+gridFirstPos%3]+")(%d,%d,"+ candidature[row][x%3*3+gridSecondPos%3]+")(%d,%d,"+candidature[row][x%3*3+gridPos%3]+")", row,x%3*3+gridFirstPos%3,row,x%3*3+gridSecondPos%3,row,x%3*3+gridPos%3)); } }else if(gridFirstPos%3==gridPos%3 && gridFirstPos%3==gridSecondPos%3){//恰好3个同列 int col = x%3*3+gridFirstPos%3;//列号 if(cutCandidature((x/3*3+gridFirstPos/3)*9+col,(x/3*3+gridSecondPos/3)*9+col,(x/3*3+gridPos/3)*9+col,""+k,2,1)){ if(single()) change = true; System.out.println(String.format(x+"宫中数字"+k+"恰好只出现在同列的三格:(%d,%d,"+ candidature[x/3*3+gridFirstPos/3][col]+")(%d,%d,"+ candidature[x/3*3+gridSecondPos/3][col]+")(%d,%d,"+candidature[x/3*3+gridPos/3][col]+")", x/3*3+gridFirstPos/3,col,x/3*3+gridSecondPos/3,col,x/3*3+gridPos/3,col)); } } } } } return change;//若没有删减候选数操作,返回false } //隐性三链数删减法:在某行,存在三个数字出现在相同的宫格内,在本行的其它宫格均不包含这三个数字,我们称这个数对是隐形三链数。 //那么这三个宫格的候选数中的其它数字都可以排除。当隐形三链数出现在列,九宫格,处理方法是完全相同的。 /*private boolean hiddenTriplesCut(){ return false;//返回false表示没有删减 } private boolean hiddenPairsCut(){ return false;//返回false表示没有删减 }*/ //三链数删减法:找出某一列、某一行或某一个九宫格中的某三个宫格候选数中,相异的数字不超过3个的情形, //进而将这3个数字自其它宫格的候选数中删减掉的方法就叫做三链数删减法。 private boolean nakedTriplesCut(){ System.out.println("三链数删减法:"); boolean change = false; for(int x=0;x<9;x++){ //需要用3重循环遍历某行所有格子3个结合的情况 for(int aPos =0;aPos<9-2;aPos++ ){//a循环到倒数第3个即可 //行 if(matrix[x][aPos]==0&&candidature[x][aPos].length()<=3){ for(int bPos=aPos+1;bPos<9-1;bPos++){//b循环到倒数第1个即可 if(matrix[x][bPos]==0&&candidature[x][bPos].length()<=3){ for(int cPos=bPos+1;cPos<9;cPos++){ if(matrix[x][cPos]==0&&candidature[x][cPos].length()<=3){ String keys = unionSet(candidature[x][aPos],candidature[x][bPos],candidature[x][cPos]); if(keys.length()<=3){ System.out.println(String.format(x+"行找到三链数:(%d,%d,"+candidature[x][aPos]+")(%d,%d,"+ candidature[x][bPos]+")(%d,%d,"+candidature[x][cPos]+"):"+keys, x,aPos,x,bPos,x,cPos)); if(cutCandidature(x*9+aPos,x*9+bPos,x*9+cPos,keys,1,1)&&single()) change = true; } } } } } } //列 if(matrix[aPos][x]==0&&candidature[aPos][x].length()<=3){ for(int bPos=aPos+1;bPos<9-1;bPos++){//b循环到倒数第2个即可 if(matrix[bPos][x]==0&&candidature[bPos][x].length()<=3){ for(int cPos=bPos+1;cPos<9;cPos++){ if(matrix[cPos][x]==0&&candidature[cPos][x].length()<=3){ String keys = unionSet(candidature[aPos][x],candidature[bPos][x],candidature[cPos][x]); if(keys.length()<=3){ System.out.println(String.format(x+"列找到三链数:(%d,%d,"+candidature[aPos][x]+")(%d,%d,"+ candidature[bPos][x]+")(%d,%d,"+candidature[cPos][x]+"):"+keys, aPos,x,bPos,x,cPos,x)); if(cutCandidature(aPos*9+x,bPos*9+x,cPos*9+x,keys,2,1)&& single()) change = true; } } } } } } //宫 int iStart =x/3*3; int jStart = x%3*3; if(matrix[iStart+aPos/3][jStart+aPos%3]==0&&candidature[iStart+aPos/3][jStart+aPos%3].length()<=3){ for(int bPos=aPos+1;bPos<9-1;bPos++){//b循环到倒数第2个即可 if(matrix[iStart+bPos/3][jStart+bPos%3]==0&&candidature[iStart+bPos/3][jStart+bPos%3].length()<=3){ for(int cPos=bPos+1;cPos<9;cPos++){ if(matrix[iStart+cPos/3][jStart+cPos%3]==0&&candidature[iStart+cPos/3][jStart+cPos%3].length()<=3){ String keys = unionSet(candidature[iStart+aPos/3][jStart+aPos%3], candidature[iStart+bPos/3][jStart+bPos%3],candidature[iStart+cPos/3][jStart+cPos%3]); if(keys.length()<=3){ System.out.println(String.format(x+"宫找到三链数:(%d,%d,"+candidature[iStart+aPos/3][jStart+aPos%3]+")(%d,%d,"+ candidature[iStart+bPos/3][jStart+bPos%3]+ ")(%d,%d,"+candidature[iStart+cPos/3][jStart+cPos%3]+"):"+keys, iStart+aPos/3,jStart+aPos%3,iStart+bPos/3,jStart+bPos%3,iStart+cPos/3,jStart+cPos%3)); if(cutCandidature((iStart+aPos/3)*9+jStart+aPos%3,(iStart+bPos/3)*9+jStart+bPos%3, (iStart+cPos/3)*9+jStart+cPos%3,keys,3,1)&&single()){ change = true; } } } } } } } } } return change;//返回false表示没有删减 } //数对删减法,如果某宫中两个格子的候选数个数只有2个且都一样,则可以删除其他格子中的这两个候选数 //数对删减法 private boolean nakedPairsCut(){ System.out.println("数对删减法:"); boolean change =false; for(int x=0;x<9;x++){ //需要双层循环两两组合 for(int aPos=0;aPos<9-1;aPos++){//a循环到倒数第2个即可 //行 if(matrix[x][aPos]==0&&candidature[x][aPos].length()==2){ for(int bPos=aPos+1;bPos<9;bPos++){ if(matrix[x][bPos]==0&&candidature[x][bPos].length()==2){ String keys = unionSet(candidature[x][aPos],candidature[x][bPos],""); if(keys.length()==2){ System.out.println(String.format(x+"行找到数对:(%d,%d,"+candidature[x][aPos]+")(%d,%d,"+ candidature[x][bPos]+")):"+keys, x,aPos,x,bPos)); if(cutCandidature(x*9+aPos,x*9+bPos,-1,keys,1,1)&&single()) change = true; } } } } //列 if(matrix[aPos][x]==0&&candidature[aPos][x].length()==2){ for(int bPos=aPos+1;bPos<9;bPos++){ if(matrix[bPos][x]==0&&candidature[bPos][x].length()==2){ String keys = unionSet(candidature[aPos][x],candidature[bPos][x],""); if(keys.length()==2){ System.out.println(String.format(x+"列找到数对:(%d,%d,"+candidature[aPos][x]+")(%d,%d,"+ candidature[bPos][x]+")):"+keys, aPos,x,bPos,x)); if(cutCandidature(aPos*9+x,bPos*9+x,-1,keys,2,1)&&single()) change = true;; } } } } //宫 int iStart =x/3*3; int jStart = x%3*3; if(matrix[iStart+aPos/3][jStart+aPos%3]==0&&candidature[iStart+aPos/3][jStart+aPos%3].length()==2){ for(int bPos=aPos+1;bPos<9;bPos++){ if(matrix[iStart+bPos/3][jStart+bPos%3]==0&&candidature[iStart+bPos/3][jStart+bPos%3].length()==2){ String keys = unionSet(candidature[iStart+aPos/3][jStart+aPos%3], candidature[iStart+bPos/3][jStart+bPos%3],""); if(keys.length()==2){ System.out.println(String.format(x+"宫找到数对:(%d,%d,"+candidature[iStart+aPos/3][jStart+aPos%3]+")(%d,%d,"+ candidature[iStart+bPos/3][jStart+bPos%3]+")):"+keys, iStart+aPos/3,jStart+aPos%3,iStart+bPos/3,jStart+bPos%3)); if(cutCandidature((iStart+aPos/3)*9+jStart+aPos%3,(iStart+bPos/3)*9+jStart+bPos%3, -1,keys,3,1)&&single()) change = true; } } } } } } return change; } /*//四链数删减法,尽管此法应用得不多,但在特殊情况下能找到必填项 private boolean quadruplexes(){ boolean change = false; return change; }*/ /** * 删减某宫(行列)除某些格子(a、b、c)外的其他格子的候选数,或者删除某些格子中的某些候选数 * @param a a的绝对位置,取值0~80 * @param b a的绝对位置,取值0~80 * @param c c的绝对位置,取值0~80或者-1,取-1时,表示数对删减法 * @param keys 候选数 * @param type 取值1、2、3,分别表示为 行删除、列删除、宫删除 * @param method 取值1、2,分别表示为三链数(数对)删除法(删其他格子)、隐性三链数删除法(删自身格子) */ private boolean cutCandidature(int a,int b,int c,String keys,int type,int method){ boolean change = false; if(method==1){ boolean f = false;//临时变量 for(int index=0;index<9;index++){ switch(type){ case 1://行 f = matrix[a/9][index]==0&&index!=a%9&&index!=b%9; if(c>=0) f = f&&index!=c%9; if(f&&deleteKeysFromCandidature(a/9,index,keys)){ change = true; } break; case 2://列 f = matrix[index][a%9]==0&&index!=a/9&&index!=b/9; if(c>=0) f = f&&index!=c/9; if(f&&deleteKeysFromCandidature(index,a%9,keys)){ change = true; } break; case 3://宫 int absPos = (a/9/3*3+index/3)*9+a%9/3*3+index%3; //[i/3*3+index/3][j/3*3+index%3] //计算绝对位置i*9+j if(matrix[a/9/3*3+index/3][a%9/3*3+index%3]==0&&absPos!=a&&absPos!=b&&absPos!=c){ if(deleteKeysFromCandidature(a/9/3*3+index/3,a%9/3*3+index%3,keys)) change = true; } break; default: } } }else{ } return change; } //取abc三个字符串的并集 //取a,b,c字符串的并集 private String unionSet(String a,String b,String c){ if(a==null||b==null||c==null) return null; String d = a+b+c; char[] chars = d.toCharArray(); Set<Character> set = new HashSet<Character>(); StringBuilder sb = new StringBuilder(); for(int i=0;i<chars.length;i++){ if(set.add(chars[i])){ sb.append(chars[i]); } } return sb.toString(); } //从(i,j)候选数中删除指定的候选数keys private boolean deleteKeysFromCandidature(int i,int j,String keys){ boolean change = false; for(int k=0;k<keys.length();k++){ String key = keys.substring(k,k+1); if(matrix[i][j]==0&&candidature[i][j].contains(key)){ System.out.println(String.format("从(%d,%d)"+candidature[i][j]+"中删除候选数->"+key,i,j)); candidature[i][j] = candidature[i][j].replace(key,""); change = true; } } return change; } //万能解题法的“搜索+剪枝”,递归与回溯 //从(i,j)位置开始搜索数独的解,i和j最大值为8 private boolean execute(int i,int j){ //寻找可填的位置(即空白格子),当前(i,j)可能为非空格,从当前位置当前行开始搜索 outer://此处用于结束下面的双层循环,标记不赞成使用,但在此处很直观 for(int x=i;x<9;x++){ for(int y=0;y<9;y++){ if(matrix[x][y]==0){ i=x; j=y; break outer; } } } //如果从当前位置并未搜索到一个可填的空白格子,意味着所有格子都已填写完了,所以找到了解 if(matrix[i][j]!=0){ count++; System.out.println("第"+count+"种解:"); output(); if(count==maxCount) return true;//return true 表示只找寻一种解,false表示找所有解 else return false; } //试填k for(int k=1;k<=9;k++){ if(!check(i,j,k)) continue; matrix[i][j] = k;//填充 //System.out.println(String.format("(%d,%d,%d)",i,j,k)); if(i==8&&j==8) {//填的正好是最后一个格子则输出解 count++; System.out.println("第"+count+"种解:"); output(); if(count==maxCount) return true;//return true 表示只找寻一种解,false表示找所有解 else return false; } //计算下一个元素坐标,如果当前元素为行尾,则下一个元素为下一行的第一个位置(未填数), //否则为当前行相对当前元素的下一位置 int nextRow = (j<9-1)?i:i+1; int nextCol = (j<9-1)?j+1:0; if(execute(nextRow,nextCol)) return true;//此处递归寻解,若未找到解,则返回此处,执行下面一条复位语句 //递归未找到解,表示当前(i,j)填k不成功,则继续往下执行复位操作,试填下一个数 matrix[i][j] = 0; } //1~9都试了 return false; } //反复应用唯一(余)法检查每个格子的候选数的个数是否为1以及应用摒除法找寻必填数字 //直到候选数不在发生变化(即没有候选数删减操作) //最后才用递归寻解 public void execute(){ boolean flag = true; while(flag){ boolean f1 = single();//唯一(余)法,最基础的方法、应用其他方法发生了删减候选数时都要应用此方法 boolean f2 = exclude();//摒除法,优先级比唯一(余)法低一点点,也是最基础的方法 boolean f3 = nakedPairsCut();//数对删减法 flag = f1||f2||f3; if(!flag){ boolean f4 = nakedTriplesCut();//三链数删减法 flag = f4; } //再应用一次基础方法,确保万无一失 if(!flag){ f1 = single(); f2 = exclude(); flag = f1||f2; } } //outputCandidature(); System.out.println("人工方式求解:"); output(); //递归求解 execute(0,0);//从第一个位置开始递归寻解 } //数独规则约束,行列宫唯一性,检查(i,j)位置是否可以填k private boolean check(int i,int j,int k){ //行列约束,宫约束,对应宫的范围 起始值为(i/3*3,j/3*3),即宫的起始位置行列坐标只能取0,3,6 for(int index=0;index<9;index++){ if(matrix[i][index]==k) return false; if(matrix[index][j]==k) return false; if(matrix[i/3*3+index/3][j/3*3+index%3]==k) return false; } return true; } public void output(){ for(int i=0;i<9;i++){ for(int j=0;j<9;j++) { if(j%3==0) System.out.print(" "); System.out.print(matrix[i][j]); } System.out.println(); if(i%3==2) System.out.println("-------------"); } } public void outputCandidature(){ for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ if(matrix[i][j]==0){ System.out.println(String.format("候选数(%d,%d)->"+candidature[i][j],i,j)); } } } } public static void main(String[] args) { try { Sudoku sudoku = new Sudoku("000000000000001002034000050000000340000006070100000000000040000080370000200500008",1); //Sudoku sudoku = new Sudoku("000000000000001002034000050000000030000026000005000470000700000100400000680000001",1); //Sudoku sudoku = new Sudoku("123456789456789123789123456234567891567891234891234567345000000000000000000000000",20); //Sudoku sudoku = new Sudoku("000000000000001023004560000000000000007080400010002500000600000020000010300040000",1); Date begin = new Date(); sudoku.execute(); System.out.println("执行时间"+(new Date().getTime()-begin.getTime())+"ms"); if(sudoku.getCount()==0) System.out.println("未找到解"); } catch (Exception e) { e.printStackTrace(); } } }
以下是对一个标准17数独(单解)的执行结果。
(000000000000000012003045000000000036000000400570008000000100000000900020706000500):
000 000 000 000 000 012 003 045 000 ------------- 000 000 036 000 000 400 570 008 000 ------------- 000 100 000 000 900 020 706 000 500 ------------- 唯一(余)法必填项(5,7,9) 唯一(余)法必填项(5,8,1) 唯一法或唯余法: 唯一(余)法必填项(5,6,2) 摒除法: 从(0,0)124689中删除候选数->1 从(0,1)1245689中删除候选数->1 从(0,2)1245789中删除候选数->1 列摒除法必填项(7,6,1) 唯一法或唯余法: 唯一(余)法必填项(5,2,4) 行摒除法必填项(8,1,1) 唯一法或唯余法: 从(6,4)235678中删除候选数->2 从(6,5)23467中删除候选数->2 从(4,3)23567中删除候选数->3 从(4,4)1235679中删除候选数->3 从(4,5)123679中删除候选数->3 从(0,0)24689中删除候选数->4 从(0,1)245689中删除候选数->4 从(0,1)25689中删除候选数->5 从(0,2)25789中删除候选数->5 从(4,3)2567中删除候选数->5 从(4,4)125679中删除候选数->5 从(3,4)12579中删除候选数->5 从(4,3)267中删除候选数->6 从(4,4)12679中删除候选数->6 从(4,5)12679中删除候选数->6 从(6,4)35678中删除候选数->6 从(6,5)3467中删除候选数->6 数对删减法: 4宫找到数对:(5,3,36)(5,4,36)):36 5行找到数对:(5,3,36)(5,4,36)):36 三链数删减法: 5宫找到三链数:(3,6,78)(4,7,578)(4,8,578):785 唯一法或唯余法: 摒除法: 宫摒除法必填项(2,0,1) 唯一法或唯余法: 行摒除法必填项(3,3,5) 数对删减法: 4宫找到数对:(5,3,36)(5,4,36)):36 5行找到数对:(5,3,36)(5,4,36)):36 三链数删减法: 5宫找到三链数:(3,6,78)(4,7,578)(4,8,578):785 唯一法或唯余法: 摒除法: 行摒除法必填项(3,5,4) 唯一法或唯余法: 列摒除法必填项(8,3,4) 唯一法或唯余法: 唯一(余)法必填项(8,7,8) 从(0,4)1236789中删除候选数->8 从(1,4)36789中删除候选数->8 唯一法或唯余法: 摒除法: 数对删减法: 4宫找到数对:(5,3,36)(5,4,36)):36 5行找到数对:(5,3,36)(5,4,36)):36 7宫找到数对:(8,4,23)(8,5,23)):23 从(6,4)3578中删除候选数->3 从(6,5)37中删除候选数->3 从(7,4)35678中删除候选数->3 从(7,5)367中删除候选数->3 唯一法或唯余法: 唯一(余)法必填项(6,5,7) 唯一(余)法必填项(7,5,6) 8行找到数对:(8,4,23)(8,5,23)):23 从(8,8)39中删除候选数->3 唯一法或唯余法: 唯一(余)法必填项(8,8,9) 三链数删减法: 5宫找到三链数:(3,6,78)(4,7,57)(4,8,578):785 6行找到三链数:(6,6,36)(6,7,46)(6,8,34):364 从(6,0)23489中删除候选数->3 从(6,0)2489中删除候选数->4 从(6,1)234589中删除候选数->3 从(6,1)24589中删除候选数->4 唯一法或唯余法: 8宫找到三链数:(6,6,36)(6,7,46)(6,8,34):364 从(7,8)347中删除候选数->3 从(7,8)47中删除候选数->4 唯一法或唯余法: 唯一(余)法必填项(7,8,7) 唯一法或唯余法: 唯一(余)法必填项(2,8,8) 唯一(余)法必填项(4,8,5) 摒除法: 行摒除法必填项(0,7,5) 宫摒除法必填项(3,6,8) 唯一法或唯余法: 唯一(余)法必填项(4,7,7) 数对删减法: 3行找到数对:(3,0,29)(3,1,29)):29 从(3,2)129中删除候选数->2 从(3,2)19中删除候选数->9 从(3,4)1279中删除候选数->2 从(3,4)179中删除候选数->9 唯一法或唯余法: 唯一(余)法必填项(2,7,6) 唯一(余)法必填项(3,2,1) 唯一(余)法必填项(3,4,7) 唯一(余)法必填项(4,3,2) 唯一(余)法必填项(6,7,4) 唯一(余)法必填项(6,8,3) 3宫找到数对:(3,0,29)(3,1,29)):29 从(4,0)3689中删除候选数->9 从(4,1)3689中删除候选数->9 从(4,2)89中删除候选数->9 唯一法或唯余法: 唯一(余)法必填项(0,8,4) 唯一(余)法必填项(2,3,7) 唯一(余)法必填项(2,6,9) 唯一(余)法必填项(4,2,8) 唯一(余)法必填项(6,6,6) 唯一(余)法必填项(7,2,5) 唯一(余)法必填项(7,4,8) 3宫找到数对:(4,0,36)(4,1,36)):36 4行找到数对:(4,0,36)(4,1,36)):36 4行找到数对:(4,4,19)(4,5,19)):19 4宫找到数对:(4,4,19)(4,5,19)):19 4宫找到数对:(5,3,36)(5,4,36)):36 5行找到数对:(5,3,36)(5,4,36)):36 6列找到数对:(0,6,37)(1,6,37)):37 6宫找到数对:(7,0,34)(7,1,34)):34 7行找到数对:(7,0,34)(7,1,34)):34 7宫找到数对:(8,4,23)(8,5,23)):23 8行找到数对:(8,4,23)(8,5,23)):23 三链数删减法: 0宫找到三链数:(0,2,279)(1,2,79)(2,1,2):279 从(0,0)2689中删除候选数->2 从(0,0)689中删除候选数->9 从(0,1)2689中删除候选数->2 从(0,1)689中删除候选数->9 从(1,0)4689中删除候选数->9 从(1,1)45689中删除候选数->9 唯一法或唯余法: 唯一(余)法必填项(2,1,2) 唯一(余)法必填项(3,1,9) 唯一(余)法必填项(6,1,8) 唯一(余)法必填项(6,4,5) 1列找到三链数:(0,1,6)(4,1,36)(7,1,34):634 从(1,1)456中删除候选数->6 从(1,1)45中删除候选数->4 唯一法或唯余法: 唯一(余)法必填项(0,1,6) 唯一(余)法必填项(1,1,5) 唯一(余)法必填项(3,0,2) 唯一(余)法必填项(4,1,3) 唯一(余)法必填项(6,0,9) 唯一(余)法必填项(6,2,2) 唯一(余)法必填项(7,1,4) 1行找到三链数:(1,2,79)(1,5,39)(1,6,37):793 从(1,3)368中删除候选数->3 从(1,4)369中删除候选数->9 从(1,4)36中删除候选数->3 唯一法或唯余法: 唯一(余)法必填项(0,0,8) 唯一(余)法必填项(0,3,3) 唯一(余)法必填项(0,6,7) 唯一(余)法必填项(1,0,4) 唯一(余)法必填项(1,4,6) 唯一(余)法必填项(1,5,9) 唯一(余)法必填项(1,6,3) 唯一(余)法必填项(4,0,6) 唯一(余)法必填项(4,5,1) 唯一(余)法必填项(5,3,6) 唯一(余)法必填项(5,4,3) 唯一(余)法必填项(7,0,3) 唯一(余)法必填项(8,4,2) 唯一(余)法必填项(8,5,3) 唯一法或唯余法: 唯一(余)法必填项(0,2,9) 唯一(余)法必填项(0,4,1) 唯一(余)法必填项(0,5,2) 唯一(余)法必填项(1,2,7) 唯一(余)法必填项(1,3,8) 唯一(余)法必填项(4,4,9) 摒除法: 数对删减法: 三链数删减法: 唯一法或唯余法: 摒除法: 人工方式求解: 869 312 754 457 869 312 123 745 968 ------------- 291 574 836 638 291 475 574 638 291 ------------- 982 157 643 345 986 127 716 423 589 ------------- 第1种解: 869 312 754 457 869 312 123 745 968 ------------- 291 574 836 638 291 475 574 638 291 ------------- 982 157 643 345 986 127 716 423 589 ------------- 执行时间96ms
从上面示例可以看出,应用候选数删减法(人工)完全把一个标准17数独解出来了,没有用到递归。
即便候选数删减法(人工)只找出了部分的必填项,但也会大大减少了递归执行的时间
相关文章推荐
- 算法之6-回溯法解数独问题
- 递归回溯与迭代回溯算法框架,打印在n个数字中取k个数字的所有可能
- java数独生成算法(递归)
- 全序列算法递归实现――回溯
- 回溯算法解数独问题(java版)
- 蓝桥杯ALGO-125算法训练 王、后传说(回溯、递归)
- Java基于循环递归回溯实现八皇后问题算法示例
- 一个数组算法题,利用递归-回溯求解
- 【算法分析】回溯法解数独(九宫格)算法
- 递归与回溯,DFS及BFS的算法
- 算法练习(1)—— 简单递归/回溯
- 五类常见算法小记 (递归与分治,动态规划,贪心,回溯,分支界限法)
- Java回溯算法解数独问题
- 常用算法:递归,回溯
- 基础算法 | 回溯和递归--矩阵中的路径(编程之美)
- C语言 递归(回溯) 解决数独问题
- [LeetCode] Sudoku Solver 解数独,递归,回溯
- 回溯0--递归回溯算法框架
- 五类常见算法小记 (递归与分治,动态规划,贪心,回溯,分支界限法)
- C语言 递归(回溯) 解决数独问题