【软件工程基础】数独生成器以及解答器
github地址
Sudoku地址
需求
實現一個能生成數獨終局并且能夠求解數獨問題的控制臺程序。
能生成不重復的數獨終局至文件。
讀取文件內的數獨問題,求解并將結果輸出到文件。
主要需求標黃。
環境
win10+IntelliJ IDEA
PSP
| Planning | 計劃 | 60 | 60 |
| ·Estimate | ·預估這個任務需要多少時間 | 1440 | 1440 |
| Development | 開發 | 480 | 600 |
| ·Analysis | ·需求分析(包括學習新技術) | 800 | 680 |
| ·Design Spec | ·生成設計文檔 | 60 | 30 |
| ·Design Review | 設計復審(和同事審核設計文檔) | 30 | 0 |
| ·Coding Standard | ·代碼規范(為目前的開發制定合適的規范) | 60 | 30 |
| ·Design | ·具體設計 | 200 | 140 |
| ·Coding | ·具體編碼 | 300 | 420 |
| ·Code Review | ·代碼復審 | 60 | 60 |
| ·Test | ·測試 | 60 | 60 |
| ·Reporting | ·報告 | 30 | 30 |
| ·Test Report | ·測試報告 | 30 | 30 |
| ·Size Measurement | ·計算工作量 | 60 | 45 |
| ·Postmortem & Process Improvement | 事后總結,并計算出過程改進計劃 | 240 | 120 |
| 合計(有重疊部分) | 1500 |
解題思路
問題可以分解成三個部分,一個是數獨的生成,一個是解數獨,一個是本地化存儲和讀取。
數獨生成
生成部分也可以分步驟來實現:先生成數獨終局,然后根據需要挖掉對應的格子數。格子數應當擁有一個上限和一個下限,不同的格子數對應著不同的難度。也可以寫死,具體的根據開發進度決定是否加上難度選擇的額外需求。
最初的版本可以將一個已經有的數獨終局作為基礎,并且交換盤上的數字來達到不同棋局的效果。但是這樣子做能夠生成的棋局有一個上限,不過也能夠滿足部分的需求。
解數獨
最初的設想是可以通過暴力的方式來解數獨,當然純暴力的方式對于時間和空間的消耗都太大了,所以需要借助記憶化存儲或者回溯的方式來剪枝。可能在設計不出來的時候會在網上查找參考資料。
已經通過回溯的方法實現了解數獨的算法。
本地化存儲
需要使用到Java的IO流操作,比較簡單,代碼如下:
/*** 從txt文件讀取matrix* @param path* @param fileName* @return* @throws IOException*/public static ArrayList<int[][]> readMatrixFromTxt(String path, String fileName) throws IOException {ArrayList<int[][]> result = new ArrayList<>();BufferedReader br = new BufferedReader(new FileReader(path+"/"+fileName));String temp = br.readLine();StringBuilder sb = new StringBuilder();while(temp!=null){sb.append(temp);temp = br.readLine();}String[] matrixs = sb.toString().replaceAll("\n","").split("_");int index = 0;for (String s : matrixs) {index = 0;int[][] matrix = new int[9][9];for(int i = 0 ; i < 9 ; i++){for(int j = 0 ; j < 9 ; j++){matrix[i][j] = s.charAt(index++) - '0';}}result.add(matrix);}return result;} /*** 保存數據* @param fileName 文件名稱* @param data 對應生成的棋局或解的棋局*/public void saveDataToFile(String fileName,ArrayList<String> data){if(data==null||data.size()==0){System.out.println("data錯誤");return;}BufferedWriter writer = null;//TODO:添加對應的路徑String destDirName=" ";File dir = new File(destDirName);if (dir.exists()) {System.out.println("創建目錄" + destDirName + "失敗,目標目錄已經存在");}if (!destDirName.endsWith(File.separator)) {destDirName = destDirName + File.separator;}//創建目錄if (dir.mkdirs()) {System.out.println("創建目錄" + destDirName + "成功!");} else {System.out.println("創建目錄" + destDirName + "失敗!");}File file = new File(destDirName+ fileName + ".txt");System.out.println(file+"file");//如果文件不存在,則新建一個if(!file.exists()){try {file.createNewFile();} catch (IOException e) {e.printStackTrace();}System.out.println(fileName + ".txt文件不存在");}//寫入try {writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file,false), "UTF-8"));for(String str:data){writer.write(str+"\n"); }} catch (IOException e) {e.printStackTrace();}finally {try {if(writer != null){writer.close();}} catch (IOException e) {e.printStackTrace();}}System.out.println("文件寫入成功!");}設計實現過程
大部分的類都是用了構造器模式,提升了代碼的優雅程度,個人比較喜歡這種RxJava的風格。
FileUtils.writeToTXT(FileUtils.PATH, FormatUtils.formatArray(generator.createSampleArray().generateSudokuArray().createEmptySpace().build()), FileUtils.MATRIX);...... SudokuPuzzleSolver.create().initMatrix(matrix).solve();生成數獨
目前的思路是使用交換法來實現不同的數獨終局。期初給定幾個種子終局,通過生成隨機序列,然后以隨機序列的順序交換每個數字。
每個終局可以隨機挖0到12 20個空,所以生成的終局數量是可觀的。
以下是核心算法:
種子矩陣生成自己隨意去網上找幾個數獨終局即可,在java中有個坑就是二維數組深拷貝,需要自己寫,不然達不到深拷貝目的,因為java中二維數組相當于數組的數組,clone()方法拷貝的知識一維數組的地址,所以需要自己寫。
/*** 二維數組深拷貝* @param array* @return*/public static int[][] cloneArray(int[][] array){int[][] result = new int[9][9];for(int i = 0 ; i < 9 ; i++){for(int j = 0 ; j < 9 ; j++){result[i][j] = array[i][j];}}return result;}解數獨
回溯法解數獨,也就是填了一個空之后遞歸解決剩下的空,如果無解就返回。
public SudokuPuzzleSolver solve(){this.backTrace(0,0);return this;}/*** 數獨算法** @param i* @param j*/private void backTrace(int i, int j) {if (i == 8 && j == 9) {FileUtils.writeToTXT(FileUtils.PATH,"獲得正確解\n",FileUtils.SOLVE);FileUtils.writeToTXT(FileUtils.PATH,FormatUtils.formatArray(matrix),FileUtils.SOLVE);return;}//已經到了列末尾了,還沒到行尾,就換行if (j == 9) {i++;j = 0;}//如果i行j列是空格,那么才進入給空格填值的邏輯if (matrix[i][j] == 0) {for (int k = 1; k <= 9; k++) {//判斷給i行j列放1-9中的任意一個數是否能滿足規則if (check(i, j, k)) {//將該值賦給該空格,然后進入下一個空格matrix[i][j] = k;backTrace(i, j + 1);//初始化該空格matrix[i][j] = 0;}}} else {//如果該位置已經有值了,就進入下一個空格進行計算backTrace(i, j + 1);}}/*** 判斷給某行某列賦值是否符合規則** @param row* @param line* @param number* @return*/private boolean check(int row, int line, int number) {//判斷該行該列是否有重復數字for (int i = 0; i < 9; i++) {if (matrix[row][i] == number || matrix[i][line] == number) {return false;}}//判斷小九宮格是否有重復int tempRow = row / 3;int tempLine = line / 3;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {if (matrix[tempRow * 3 + i][tempLine * 3 + j] == number) {return false;}}}return true;}輸入異常類
如果輸入錯誤就會拋出這個異常
/*** 參數輸入錯誤的異常*/ public class WrongArgsException extends Exception { }軟件測試
劃分為如下等價類:合法輸入,非法輸入。
合法輸入分為:可解矩陣、不可解矩陣、正確生成矩陣、錯誤數字生成矩陣。
非法輸入分為:缺行、多行、非法字符、錯誤命令行參數、錯誤命令函參數數量。
規定,0為需要填的空
合法輸入
使用如下矩陣:
可解矩陣:
輸出結果:
獲得正確解 256431879 431879256 879256431 524613798 613798524 798524613 362145987 145987362 987362145 _無解矩陣:
550001879 501879056 879256031 524503798 610798524 798024613 362145987 145987062 987302145輸出結果:
無解正確生成矩陣:
-c 20
輸出結果:
正確生成文件。
錯誤數字生成矩陣:
-c -1
輸出結果:
非法輸入
缺行:
550001879 501879056 879256031 524503798 610798524 798024613 362145987 145987062輸出結果:
數獨矩陣錯誤,請檢查矩陣是否合法多行:
550001879 501879056 879256031 524503798 610798524 798024613 362145987 145987062 987302145 123456789輸出結果:
數獨矩陣錯誤,請檢查矩陣是否合法非法字符:
550001879 501879056 87b256031 524503798 610798a24 798024613 362145987 145987062 987302145輸出結果:
數獨矩陣錯誤,請檢查矩陣是否合法錯誤命令行參數:
-c abc
輸出結果:
錯誤命令函參數數量:
-c -1 20
輸出結果:
性能分析
使用的是JProfiler,具體安裝使用教程:傳送門
由于String的底層是byte,所以byte[]的使用量是非常高的,上圖是跑20萬個矩陣生成時的性能分析。可以通過多線程來跑生成矩陣的代碼,這是可以優化的一個點。當然解數獨也可以同樣這么優化。同時生成數獨和解數獨時會不斷創建Generator和Solver的對象,所以再數量及其大的時候會出現GC。這是可以優化的第二個點。
反思總結
1、git提交的時候應該明確提交格式,github會將第一行抽取為當前commit的標題,所以推薦的格式如下:
【修改類型】 修改描述1、修改描述1 2、修改描述2 ...2、推薦使用阿里的代碼規約掃描代碼之后按照意見修改之后再上傳。
3、應該設置好IDEA新建類時的自動注釋生成。
參考資料
回溯法解數獨
生成數獨的方式
總結
以上是生活随笔為你收集整理的【软件工程基础】数独生成器以及解答器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PDF编辑技巧 PDF怎么复制页面
- 下一篇: 栅栏密码加密与解密以及特征