4.9 数独问题
1. 前言
本文的一些圖片, 資料 截取自編程之美
2. 問題描述
額, 4.9 這里的問題是 給定一個(gè)殘缺的數(shù)獨(dú), 粗魯額的計(jì)算一下大概有多少種解法, 以及多少種獨(dú)立的解答, 。。。一看 這個(gè)便是一個(gè)數(shù)學(xué)問題,,,
算了 跳過算了, 跳到了1.9, 這個(gè)是關(guān)于如果解決填充殘缺的數(shù)獨(dú)的問題的
其實(shí), 對(duì)于數(shù)獨(dú)的解法, 我也是在貼吧看見一個(gè)家伙, 在說, 貌似是那家伙要找一個(gè)提高他自己的解決數(shù)獨(dú)效率, 然后 后來我自己沒事情的時(shí)候, 就寫了寫 思路 是基于書中的第一種思路
3. 問題分析
解法一 : 獲取所有的空格子的所有可能的取值, 遍歷所有的空格子, 嘗試填充所有的可能, 當(dāng)不符合數(shù)獨(dú)的特性的時(shí)候, 進(jìn)行回溯, 將當(dāng)前的空格子設(shè)置為當(dāng)前空格的下一種可能的數(shù)字, 如果當(dāng)前空格沒有了可能的數(shù)字, 則回溯到前一個(gè)空格, 設(shè)置該空格的值為該空格的下一種可能, 然后繼續(xù)嘗試當(dāng)前空格的所有可能, 這種思路能夠找到所有的解, 但是復(fù)雜度較高
解法二 : 非用言語能夠表達(dá).. 上圖吧
請(qǐng)注意 后面有一句, 這種方法并不能解出所有的情況 !
犧牲了一定程度的正確性
我的思路
我的這里的思路是基于第一種解法的, 不過每次假設(shè)值的時(shí)候, 找出備選數(shù)最少的坐標(biāo)假設(shè)值
4. 代碼
1 : Candidate 存儲(chǔ)每一個(gè)方格的備選數(shù)目的數(shù)據(jù)結(jié)構(gòu)
/*** file name : Candidates.java* created at : 7:58:14 PM Apr 22, 2015* created by 970655147*/package com.hx.sudoku02;// 備選的數(shù)據(jù)集合 public class Candidate {// 當(dāng)前對(duì)象 對(duì)應(yīng)的方格的行, 列 以及備選的數(shù)據(jù)集合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 & getterpublic 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中數(shù)據(jù)為[1, 2, 3] 返回1, 2, 3public 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的其他的candidatespublic 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);}// 判定兩個(gè)Candidate對(duì)象相同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;}// 復(fù)制當(dāng)前的Candidate對(duì)象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 : 數(shù)獨(dú)數(shù)據(jù)結(jié)構(gòu)
/*** file name : Sudoku.java* created at : 7:57:10 PM Apr 22, 2015* created by 970655147*/package com.hx.sudoku02;// 數(shù)獨(dú)類 // 我去, 原來還需要在每一宮都只出現(xiàn)一次,, 我以為我就做完了呢,,, --2015.04.24 10:26 // 現(xiàn)在應(yīng)該算是 做完了吧, 只不過效率沒有 貼吧那家伙的高[現(xiàn)在去看看他的算法吧] 找到第一個(gè)結(jié)果需要240ms 為什么需要進(jìn)行深拷貝哪里也不懂, 一直沒有找出問題在哪里...?? --2015.04.24 14:38 public class Sudoku {// 當(dāng)前計(jì)算的數(shù)獨(dú)的數(shù)據(jù), 最原始的給定的數(shù)據(jù), 每一個(gè)方格對(duì)應(yīng)的Candidateprivate Integer[][] data;private Integer[][] backupData;private Candidate[][] candidates;// 共有多少個(gè)結(jié)果, 有多少個(gè)方格確定了private int count;private int unSetted;// 方格的個(gè)數(shù) 每一宮的邊長的方格的個(gè)數(shù)public final static int GRID_NUM = 81;public final static int LITTLE_GRID_EDGE = 3;// 初始化public Sudoku() {}public Sudoku(String data) {// 初始化data, backupData, candidatesthis.data = Tools.parseSudoku(data);backupData = this.data;initCandidates(this.data);unSetted = GRID_NUM;}// 解析數(shù)獨(dú) 如果data為null 拋出異常public void parse() {if(data == null) {throw new RuntimeException("data can't be null...");}doParse();}// 具體的解析數(shù)獨(dú)// 先獲取備選數(shù)據(jù)最少的方格 // 在依次假設(shè)該方格的值為所有的備選數(shù)據(jù) 更新該方格對(duì)應(yīng)行, 列的所有方格// 判斷是否失敗 或者成功 如果是 則恢復(fù)之前設(shè)置的備選數(shù)據(jù)為之前的值 [應(yīng)該是可以優(yōu)化的]// 否則繼續(xù)遞歸doParse ->private void doParse() {// 找到備選數(shù)最少的方格 創(chuàng)建一個(gè)minSizeCandidate的副本 用于假設(shè)該方格的數(shù)據(jù), 這樣的話不會(huì)更改minSizeCandidateCandidate minSizeCandidate = getMinCandidate(candidates);Candidate copyMinSizeCandidate = minSizeCandidate.copy();boolean isEnd = false;// 遍歷所有的備選數(shù)據(jù)while(candidateNotNull(copyMinSizeCandidate)) {// 存儲(chǔ)之前的candidates, minSizeCandidate對(duì)應(yīng)的位的數(shù)據(jù)Candidate[][] oldCandidates = this.candidates;Integer oldData = data[minSizeCandidate.getRow()][minSizeCandidate.getCol()];// 假設(shè)minSizeCandidate對(duì)應(yīng)的方格的值, 更新該方格對(duì)應(yīng)的行, 列的所有方格的Candidatedata[minSizeCandidate.getRow()][minSizeCandidate.getCol()] = copyMinSizeCandidate.getCandidate();// unSetted在updateCandidates中更新this.candidates = updateCandidates(minSizeCandidate);// 如果失敗了 設(shè)置isEnd為true 之后跳出循環(huán) 失敗的檢測(cè)需要隨時(shí)檢測(cè)if(isFailed(this.candidates)) {isEnd = true;}// 優(yōu)化2. // 如果未設(shè)置的方格為為0 才檢測(cè)是否成功 添加了一個(gè)unSetted變量, 但是感覺這里也沒有優(yōu)化多少,,, 大概30ms 1230 -> 1200if(unSetted == 0) {Log.log(data);count ++;isEnd = true;}// 如果沒有失敗/ 成功, 遞歸doParseif(!isEnd) {doParse();}// 恢復(fù)minSizeCandidate對(duì)應(yīng)的方格的數(shù)據(jù)isEnd = false;data[minSizeCandidate.getRow()][minSizeCandidate.getCol()] = oldData;this.candidates = oldCandidates;}}// 判斷是否當(dāng)前數(shù)獨(dú)的假設(shè)失敗了 判定的條件是假設(shè)任意一個(gè)方格的不為null的Candidate的備選數(shù)據(jù)的個(gè)數(shù)小于1個(gè) 則視為失敗// 因?yàn)樵摲礁駴]有備選數(shù)據(jù)了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對(duì)應(yīng)的方格對(duì)應(yīng)行, 列 其他方格的Candidateprivate Candidate[][] updateCandidates(Candidate minSizeCandidate) {Candidate[][] updatedCandidates = new Candidate[this.candidates.length][this.candidates.length]; unSetted = GRID_NUM;// 遍歷candidates上minSizeCandidate對(duì)應(yīng)的行或者 列 上的所有方格 [如果該為的數(shù)據(jù)已經(jīng)確定 設(shè)置該Candidate為null] 更新該方格的Candidate[這里的算法應(yīng)該可以優(yōu)化]// 其他的方格的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處的數(shù)據(jù)以確定, // data[i][j]不為0 表示該位數(shù)據(jù)以確定[或者被假設(shè)]if(data[i][j] != 0) {unSetted --;updatedCandidates[i][j] = null;// 我去 之前少一個(gè)continue....continue;}// 2. 為什么進(jìn)行深拷貝才能計(jì)算出結(jié)果...>???? --2015.04.24// 進(jìn)行深拷貝是因?yàn)? 如果不進(jìn)行深拷貝的話下面的n次遞歸的過程中 坑定會(huì)更改這個(gè)Candidate, 這個(gè)操作時(shí)不可逆的, 因?yàn)橹皼]有備份, // 那么更改了這個(gè)Candidate之后 如果之后因?yàn)槭』蛘叱晒Φ脑蚧厮莼厝? 下一次在訪問這個(gè)方格的Candidate的時(shí)候 他的備選數(shù)據(jù) 已經(jīng)變少了 導(dǎo)致忽略了很多方格的備選數(shù)據(jù), 最終導(dǎo)致 結(jié)果可能計(jì)算不出來// 所以 這里 我才用了一個(gè)比較巧妙的方法, 就是在doParse之前將minSizeCandidate備份一下 minSizeCandidate.getCandidate假設(shè)數(shù)據(jù)的時(shí)候 就用copyOfMinSizeCandidate 這樣更改的就是minSizeCandidate的副本了// 從而深復(fù)制一個(gè)minSizeCandidate 而不用深復(fù)制minSizeCandidate更新數(shù)據(jù)是其他行列 并且不在同一個(gè)宮格內(nèi)的方格的Candidate了 // 520ms -> 380msif(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是否匹配最開始給定的數(shù)據(jù)[匹配每一個(gè)非待填的數(shù)據(jù)]private boolean isResultMatchData(Integer[][] res) {// 遍歷backupData[最開始的data], res 如果兩者中任意一個(gè)非待填數(shù)據(jù)不相等 則返回falsefor(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;}// 根據(jù)data初始化 candidatesprivate void initCandidates(Integer[][] data) {candidates = new Candidate[data.length][data.length];// 遍歷data 如果該方格數(shù)據(jù)為0 則獲取該方格的Candidate 設(shè)置到candidates[i][j]中// 否則設(shè)置candidates[i][j]為nullfor(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;}}}}// 根據(jù)data 獲取第row行, col列對(duì)應(yīng)的方格的Candidate對(duì)象private Candidate getCandidate(Integer[][] data, int row, int col) {// 創(chuàng)建candidates對(duì)象 并添加0-9到其中List<Integer> candidates = new ArrayList<Integer>(data.length);Tools.initAllCandidates(candidates, data.length);// x表示第幾行, y表示第幾列// 移除row行中所有的已經(jīng)確定的數(shù)for(int i=0; i<data.length; i++) {if(data[row][i] != 0) {candidates.remove(data[row][i]);}}// 移除col列中有的已經(jīng)確定的數(shù)for(int i=0; i<data.length; i++) {if(data[i][col] != 0) {candidates.remove(data[i][col]);}}// 移除當(dāng)前宮中確定的數(shù)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]);}}}// 根據(jù)candidates, row, col創(chuàng)建Candidate對(duì)象return new Candidate(candidates, row, col);}// 獲取備選數(shù)據(jù)最少的方格private Candidate getMinCandidate(Candidate[][] candidates) {// 找到第一個(gè)存在備選數(shù)據(jù)的方格Candidate minSizeCandidate = findFirstCandidate(candidates);Point p = new Point();p.x = minSizeCandidate.getRow();p.y = minSizeCandidate.getCol();// 遍歷所有的之后所有的方格 找到備選數(shù)據(jù)最少的方格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;}// 找到第一個(gè)存在備選數(shù)據(jù)的方格 將其行, 列存儲(chǔ)在p中[其實(shí)現(xiàn)在沒有必要了, 因?yàn)閏andidate中有行, 列的數(shù)據(jù)] 返回該Candidateprivate 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還有備選的數(shù)據(jù)private boolean candidateNotNull(Candidate candidate) {return candidate!=null && candidate.hasCandidates();}// 打印當(dāng)前的數(shù)獨(dú)public void printSudoku() {Log.log(data);}}3 Main : 主類, 主要包含了數(shù)獨(dú)的構(gòu)造的形式, 以及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 {// 主要是用于計(jì)算找到的第一個(gè)解的時(shí)間開銷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";// 構(gòu)造Sudoku對(duì)象 并解析 輸出結(jié)果, 計(jì)算時(shí)間Sudoku sudoku = new Sudoku(s10_2); // sudoku.printSudoku();sudoku.parse();long spent = System.currentTimeMillis() - start;Log.log("all spent : " + spent + " ms");// 判斷給定的字符串是否符合數(shù)獨(dú)的性質(zhì) // String s10_2 = "867453192 413628579 174896253 259317846 386945721 945182637 531274968 728569314 692731485"; // Log.log(Tools.isResultIsSudoku(Tools.parseSudoku(s10_2)));}}5. 運(yùn)行結(jié)果
6. 總結(jié)
這里的思路 也是類似于窮舉的思路
注 : 因?yàn)樽髡叩乃接邢?#xff0c;必然可能出現(xiàn)一些bug, 所以請(qǐng)大家指出!
總結(jié)
- 上一篇: 64位Win7系统iTunes无法识别i
- 下一篇: git仓库的使用