0
点赞
收藏
分享

微信扫一扫

4.9 数独问题


1. 前言

本文的一些图片, 资料 截取自编程之美

2. 问题描述

额, 4.9 这里的问题是 给定一个残缺的数独, 粗鲁额的计算一下大概有多少种解法, 以及多少种独立的解答, 。。。一看 这个便是一个数学问题,,,
算了 跳过算了, 跳到了1.9, 这个是关于如果解决填充残缺的数独的问题的

4.9 数独问题_数据

其实, 对于数独的解法, 我也是在贴吧看见一个家伙, 在说, 貌似是那家伙要找一个提高他自己的解决数独效率, 然后 后来我自己没事情的时候, 就写了写 思路 是基于书中的第一种思路

3. 问题分析

解法一 : 获取所有的空格子的所有可能的取值, 遍历所有的空格子, 尝试填充所有的可能, 当不符合数独的特性的时候, 进行回溯, 将当前的空格子设置为当前空格的下一种可能的数字, 如果当前空格没有了可能的数字, 则回溯到前一个空格, 设置该空格的值为该空格的下一种可能, 然后继续尝试当前空格的所有可能, 这种思路能够找到所有的解, 但是复杂度较高

解法二 : 非用言语能够表达.. 上图吧

4.9 数独问题_i++_02

请注意 后面有一句, 这种方法并不能解出所有的情况 !
牺牲了一定程度的正确性

我的思路

我的这里的思路是基于第一种解法的, 不过每次假设值的时候, 找出备选数最少的坐标假设值

4.9 数独问题_数据_03

4. 代码

1 : Candidate 存储每一个方格的备选数目的数据结构

/**
 * file name : Candidates.java
 * created at : 7:58:14 PM Apr 22, 2015
 * created by 970655147
 */

package com.hx.sudoku02;

// 备选的数据集合
public class Candidate {

    // 当前对象 对应的方格的行, 列  以及备选的数据集合
    private Integer row;
    private Integer col;
    private List<Integer> candidates;

    // 初始化
    public Candidate() {
        candidates = new ArrayList<Integer>();
    }
    public Candidate(List<Integer> candidates, int row, int col) {
        this.candidates = candidates;
        this.row = row;
        this.col = col;
    }

    // setter & getter
    public void putCandidate(Integer candidate) {
        this.candidates.add(candidate);
    }
    public Integer getCandidate() {
        return candidates.remove(candidates.size() - 1);
    }
    public boolean hasCandidates() {
        return candidates.size() > 0;
    }
    public int size() {
        return candidates.size();
    }
    public Integer getRow() {
        return row;
    }
    public Integer getCol() {
        return col;
    }

    // for debug ...
    // 如果candidates中数据为[1, 2, 3] 返回1, 2, 3
    public String toString() {
        if(candidates.size() == 0) {
            return "[...]";
        }

        StringBuilder sb = new StringBuilder(candidates.size() * 3);
        for(int i=0; i<candidates.size(); i++) {
            sb.append(candidates.get(i) + ", ");
        }

        return sb.substring(0, sb.length() - 2);
    }

    // 获取除了给定的val的其他的candidates
    public Candidate updateCandidate(int val) {
        List<Integer> dstCandidates = new ArrayList<Integer>(candidates.size());
        for(int i=0; i<candidates.size(); i++) {
            if(candidates.get(i).intValue() != val) {
                dstCandidates.add(candidates.get(i));
            }
        }

        return new Candidate(dstCandidates, row, col);
    }

    // 判定两个Candidate对象相同
    public boolean equals(Candidate can) {
        for(int i=0; i<candidates.size(); i++) {
            if(candidates.get(i).intValue() != can.candidates.get(i).intValue()) {
                return false;
            }
        }

        return true;
    }

    // 复制当前的Candidate对象
    public Candidate copy() {
        List<Integer> dstCandidates = new ArrayList<Integer>(candidates.size());
        for(int i=0; i<candidates.size(); i++) {
            dstCandidates.add(candidates.get(i));
        }

        return new Candidate(dstCandidates, row, col);
    }

}

2 Sudoku : 数独数据结构

/**
 * file name : Sudoku.java
 * created at : 7:57:10 PM Apr 22, 2015
 * created by 970655147
 */

package com.hx.sudoku02;

// 数独类
// 我去, 原来还需要在每一宫都只出现一次,, 我以为我就做完了呢,,,     --2015.04.24 10:26
// 现在应该算是 做完了吧, 只不过效率没有 贴吧那家伙的高[现在去看看他的算法吧]  找到第一个结果需要240ms  为什么需要进行深拷贝哪里也不懂, 一直没有找出问题在哪里...??      --2015.04.24 14:38
public class Sudoku {

    // 当前计算的数独的数据, 最原始的给定的数据, 每一个方格对应的Candidate
    private Integer[][] data;
    private Integer[][] backupData;
    private Candidate[][] candidates;
    // 共有多少个结果, 有多少个方格确定了
    private int count;
    private int unSetted;
    // 方格的个数  每一宫的边长的方格的个数
    public final static int GRID_NUM = 81;
    public final static int LITTLE_GRID_EDGE = 3;

    // 初始化
    public Sudoku() {

    }
    public Sudoku(String data) {
        // 初始化data, backupData, candidates
        this.data = Tools.parseSudoku(data);
        backupData = this.data;
        initCandidates(this.data);
        unSetted = GRID_NUM;
    }

    // 解析数独  如果data为null 抛出异常
    public void parse() {
        if(data == null) {
            throw new RuntimeException("data can't be null...");
        }

        doParse();
    }

    // 具体的解析数独
    // 先获取备选数据最少的方格 
    // 在依次假设该方格的值为所有的备选数据  更新该方格对应行, 列的所有方格
    // 判断是否失败 或者成功   如果是 则恢复之前设置的备选数据为之前的值 [应该是可以优化的]
    // 否则继续递归doParse  ->
    private void doParse() {
        // 找到备选数最少的方格  创建一个minSizeCandidate的副本 用于假设该方格的数据, 这样的话不会更改minSizeCandidate
        Candidate minSizeCandidate = getMinCandidate(candidates);
        Candidate copyMinSizeCandidate = minSizeCandidate.copy();

        boolean isEnd = false;
        // 遍历所有的备选数据
        while(candidateNotNull(copyMinSizeCandidate)) {
            // 存储之前的candidates, minSizeCandidate对应的位的数据
            Candidate[][] oldCandidates = this.candidates;
            Integer oldData = data[minSizeCandidate.getRow()][minSizeCandidate.getCol()];

            // 假设minSizeCandidate对应的方格的值, 更新该方格对应的行, 列的所有方格的Candidate
            data[minSizeCandidate.getRow()][minSizeCandidate.getCol()] = copyMinSizeCandidate.getCandidate();
            // unSetted在updateCandidates中更新
            this.candidates = updateCandidates(minSizeCandidate);

            // 如果失败了 设置isEnd为true  之后跳出循环  失败的检测需要随时检测
            if(isFailed(this.candidates)) {
                isEnd = true;
            }

            // 优化2.
//          如果未设置的方格为为0  才检测是否成功  添加了一个unSetted变量, 但是感觉这里也没有优化多少,,, 大概30ms  1230 -> 1200
            if(unSetted == 0) {
                Log.log(data);
                count ++;
                isEnd = true;
            }

            // 如果没有失败/ 成功, 递归doParse
            if(!isEnd) {
                doParse();
            }

            // 恢复minSizeCandidate对应的方格的数据
            isEnd = false;
            data[minSizeCandidate.getRow()][minSizeCandidate.getCol()] = oldData;
            this.candidates = oldCandidates;
        }
    }

    // 判断是否当前数独的假设失败了 判定的条件是假设任意一个方格的不为null的Candidate的备选数据的个数小于1个  则视为失败
    // 因为该方格没有备选数据了
    private boolean isFailed(Candidate[][] candidates) {
        for(int i=0; i<candidates.length; i++) {
            Candidate[] row = candidates[i];
            for(int j=0; j<candidates.length; j++) {
                if(row[j] == null) {
                    continue;
                }
                if(row[j].size() < 1) {
                    return true;
                }
            }
        }

        return false;
    }

    // 更新minSizeCandidate对应的方格对应行, 列  其他方格的Candidate
    private Candidate[][] updateCandidates(Candidate minSizeCandidate) {
        Candidate[][] updatedCandidates = new Candidate[this.candidates.length][this.candidates.length]; 
        unSetted = GRID_NUM;

        // 遍历candidates上minSizeCandidate对应的行或者 列 上的所有方格 [如果该为的数据已经确定 设置该Candidate为null] 更新该方格的Candidate[这里的算法应该可以优化]
        // 其他的方格的Candidate不变[浅拷贝]
        for(int i=0; i<candidates.length; i++) {
            Candidate[] row = candidates[i];
            for(int j=0; j<candidates.length; j++) {
//               row[j]为空表示该i, j处的数据以确定, 
                // data[i][j]不为0 表示该位数据以确定[或者被假设]
                if(data[i][j] != 0) {
                    unSetted --;
                    updatedCandidates[i][j] = null;
                    // 我去 之前少一个continue....
                    continue;
                }

                // 2. 为什么进行深拷贝才能计算出结果...>????           --2015.04.24
                // 进行深拷贝是因为   如果不进行深拷贝的话下面的n次递归的过程中 坑定会更改这个Candidate, 这个操作时不可逆的, 因为之前没有备份,   
                // 那么更改了这个Candidate之后  如果之后因为失败或者成功的原因回溯回去  下一次在访问这个方格的Candidate的时候 他的备选数据 已经变少了  导致忽略了很多方格的备选数据, 最终导致 结果可能计算不出来
                // 所以 这里 我才用了一个比较巧妙的方法, 就是在doParse之前将minSizeCandidate备份一下  minSizeCandidate.getCandidate假设数据的时候  就用copyOfMinSizeCandidate  这样更改的就是minSizeCandidate的副本了
                // 从而深复制一个minSizeCandidate 而不用深复制minSizeCandidate更新数据是其他行列 并且不在同一个宫格内的方格的Candidate了 
                // 520ms -> 380ms
                if(i==minSizeCandidate.getRow() || j==minSizeCandidate.getCol() || Tools.isInSameGrid(i, j, minSizeCandidate.getRow(), minSizeCandidate.getCol()) ) {
                    updatedCandidates[i][j] = row[j].updateCandidate(data[minSizeCandidate.getRow()][minSizeCandidate.getCol()]);
                } else {
                    updatedCandidates[i][j] = row[j];
                }
            }
        }

        return updatedCandidates;
    }

    // 判断res是否匹配最开始给定的数据[匹配每一个非待填的数据]
    private boolean isResultMatchData(Integer[][] res) {
        // 遍历backupData[最开始的data], res 如果两者中任意一个非待填数据不相等 则返回false
        for(int i=0; i<backupData.length; i++) {
            for(int j=0; j<backupData.length; j++) {
                if(backupData[i][j] != 0) {
                    if(backupData[i][j] != res[i][j]) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    // 根据data初始化 candidates
    private void initCandidates(Integer[][] data) {
        candidates = new Candidate[data.length][data.length];

        // 遍历data  如果该方格数据为0 则获取该方格的Candidate 设置到candidates[i][j]中
        // 否则设置candidates[i][j]为null
        for(int i=0; i<data.length; i++) {
            Integer[] row = data[i];
            for(int j=0; j<row.length; j++) {
                if(data[i][j] == 0) {
                    candidates[i][j] = getCandidate(data, i, j);
                } else {
                    candidates[i][j] = null;
                }
            }
        }
    }

    // 根据data 获取第row行, col列对应的方格的Candidate对象
    private Candidate getCandidate(Integer[][] data, int row, int col) {
        // 创建candidates对象  并添加0-9到其中
        List<Integer> candidates = new ArrayList<Integer>(data.length);
        Tools.initAllCandidates(candidates, data.length);

        // x表示第几行, y表示第几列
        // 移除row行中所有的已经确定的数
        for(int i=0; i<data.length; i++) {
            if(data[row][i] != 0) {
                candidates.remove(data[row][i]);
            }
        }

        // 移除col列中有的已经确定的数
        for(int i=0; i<data.length; i++) {
            if(data[i][col] != 0) {
                candidates.remove(data[i][col]);
            }
        }

        // 移除当前宫中确定的数
        Point leftUp = Tools.getLeftUpGridPos(row, col);
        Point rightDown = new Point(leftUp.x + LITTLE_GRID_EDGE, leftUp.y + LITTLE_GRID_EDGE);
        for(int i=leftUp.x; i<rightDown.x; i++) {
            for(int j=leftUp.y; j<rightDown.y; j++) {
                if(data[i][j] != 0) {
                    candidates.remove(data[i][j]);
                }
            }
        }

        // 根据candidates, row, col创建Candidate对象
        return new Candidate(candidates, row, col);
    }

    // 获取备选数据最少的方格
    private Candidate getMinCandidate(Candidate[][] candidates) {
        // 找到第一个存在备选数据的方格
        Candidate minSizeCandidate = findFirstCandidate(candidates);
        Point p = new Point();
        p.x = minSizeCandidate.getRow();
        p.y = minSizeCandidate.getCol();

        // 遍历所有的之后所有的方格 找到备选数据最少的方格
        for(int i=p.x; i<candidates.length; i++) {
            Candidate[] row = candidates[i];
            for(int j=p.y; j<row.length; j++) {
                if(data[i][j]==0 && candidateNotNull(row[j])) {
                    if(row[j].size() < minSizeCandidate.size()) {
                        minSizeCandidate = row[j];
                    }
                }
            }
        }

        return minSizeCandidate;
    }

    // 找到第一个存在备选数据的方格  将其行, 列存储在p中[其实现在没有必要了, 因为candidate中有行, 列的数据] 返回该Candidate
    private Candidate findFirstCandidate(Candidate[][] candidates) {
        for(int i=0; i<candidates.length; i++) {
            Candidate[] row = candidates[i];
            for(int j=0; j<row.length; j++) {
                if(candidateNotNull(row[j])) {
                    return row[j];
                }
            }
        }

        return null;
    }

    // 表示该candidate还有备选的数据
    private boolean candidateNotNull(Candidate candidate) {
        return candidate!=null && candidate.hasCandidates();
    }

    // 打印当前的数独
    public void printSudoku() {
        Log.log(data);
    }

}

3 Main : 主类, 主要包含了数独的构造的形式, 以及Sudoku的使用方法

/**
 * file name : Main.java
 * created at : 7:54:31 PM Apr 22, 2015
 * created by 970655147
 */

package com.hx.sudoku02;

// Main
public class Main {

    // 主要是用于计算找到的第一个解的时间开销
    public static long startTime = 0;

    public static void main(String []args) {
        long start = System.currentTimeMillis();
        startTime = start;

        String s11_9 = "000000039 000001005 003050800 008090006 070002000 100400000 009080050 020000600 400700000";

        String s10_2 = "800000000 003600000 070090200 050007000 000045700 000100030 001000068 008500010 090000400";

        String s8_2= "000070009 009800000 070065040 040601070 302000000 005200600 000009800 001400000 600000003";

        String s7_4= "006004000 000200005 100900042 021000003 000000097 300000008 000000000 005030070 014826000";

        String s6_7= "090004508 000020340 006000002 060003009 030000700 140000000 800010000 370000900 009008030";   

        String simpleSudoku02 = "000000010 400000000 020000000 000050604 008000300 001090000 300400200 050100000 000807000";


        // 构造Sudoku对象  并解析  输出结果, 计算时间
        Sudoku sudoku = new Sudoku(s10_2);
//      sudoku.printSudoku();

        sudoku.parse();

        long spent = System.currentTimeMillis() - start;
        Log.log("all spent : " + spent + " ms");

        // 判断给定的字符串是否符合数独的性质
//      String s10_2 = "867453192 413628579 174896253 259317846 386945721 945182637 531274968 728569314 692731485";     
//      Log.log(Tools.isResultIsSudoku(Tools.parseSudoku(s10_2)));

    }

}

5. 运行结果

4.9 数独问题_编程之美_04

6. 总结

这里的思路 也是类似于穷举的思路

注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!


举报

相关推荐

0 条评论