當前位置:
首頁 >
4.9 数独问题
發布時間:2023/12/29
34
豆豆
1. 前言
本文的一些圖片, 資料 截取自編程之美
2. 問題描述
額, 4.9 這里的問題是 給定一個殘缺的數獨, 粗魯額的計算一下大概有多少種解法, 以及多少種獨立的解答, 。。。一看 這個便是一個數學問題,,,
算了 跳過算了, 跳到了1.9, 這個是關于如果解決填充殘缺的數獨的問題的
其實, 對于數獨的解法, 我也是在貼吧看見一個家伙, 在說, 貌似是那家伙要找一個提高他自己的解決數獨效率, 然后 后來我自己沒事情的時候, 就寫了寫 思路 是基于書中的第一種思路
3. 問題分析
解法一 : 獲取所有的空格子的所有可能的取值, 遍歷所有的空格子, 嘗試填充所有的可能, 當不符合數獨的特性的時候, 進行回溯, 將當前的空格子設置為當前空格的下一種可能的數字, 如果當前空格沒有了可能的數字, 則回溯到前一個空格, 設置該空格的值為該空格的下一種可能, 然后繼續嘗試當前空格的所有可能, 這種思路能夠找到所有的解, 但是復雜度較高
解法二 : 非用言語能夠表達.. 上圖吧
請注意 后面有一句, 這種方法并不能解出所有的情況 !
犧牲了一定程度的正確性
我的思路
我的這里的思路是基于第一種解法的, 不過每次假設值的時候, 找出備選數最少的坐標假設值
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 & 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中數據為[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);}// 判定兩個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 {// 當前計算的數獨的數據, 最原始的給定的數據, 每一個方格對應的Candidateprivate 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, candidatesthis.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的副本 用于假設該方格的數據, 這樣的話不會更改minSizeCandidateCandidate 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對應的方格的值, 更新該方格對應的行, 列的所有方格的Candidatedata[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 -> 1200if(unSetted == 0) {Log.log(data);count ++;isEnd = true;}// 如果沒有失敗/ 成功, 遞歸doParseif(!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對應的方格對應行, 列 其他方格的Candidateprivate 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 -> 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是否匹配最開始給定的數據[匹配每一個非待填的數據]private boolean isResultMatchData(Integer[][] res) {// 遍歷backupData[最開始的data], res 如果兩者中任意一個非待填數據不相等 則返回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;}// 根據data初始化 candidatesprivate void initCandidates(Integer[][] data) {candidates = new Candidate[data.length][data.length];// 遍歷data 如果該方格數據為0 則獲取該方格的Candidate 設置到candidates[i][j]中// 否則設置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;}}}}// 根據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中有行, 列的數據] 返回該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還有備選的數據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. 運行結果
6. 總結
這里的思路 也是類似于窮舉的思路
注 : 因為作者的水平有限,必然可能出現一些bug, 所以請大家指出!
總結
- 上一篇: 64位Win7系统iTunes无法识别i
- 下一篇: git仓库的使用