zoukankan      html  css  js  c++  java
  • 数独算法-递归与回溯

    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
    学习记录5
    学习记录4
    学习记录3&开启新的世界
    学习记录2
    学习记录1
    类、对象和包
    班会总结
    请简单描述一下,java程序的运行过程,以及java为什么能跨平台
    JDK、JRE、JVM的区别与关系
  • 原文地址:https://www.cnblogs.com/yhtboke/p/5749139.html
Copyright © 2011-2022 走看看