解數獨——命令行程序的實現


更新日志:
20/01/19 初版,單元測試、性能改進、流程圖需改進
20/04/02 第二版,增加作業頭等要求,嘗試做單元測試——失敗

這個作業屬於哪個課程 班級的鏈接
這個作業要求在哪里 作業要求
這個作業的目標 初步接觸軟件工程的開發流程,學習和練習單元測試及性能測試
作業正文 如下
參考文獻① LeetCode——解數獨官方題解
參考文獻② bilibili up主happygirlzt 回溯法系列三:解數獨(Sudoku Solver)

Github項目地址

地址

PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(min) 實際耗時(min)
Planning 計划 30 15
Estimate 估計這個任務需要多少時間 48*60 12*60
Development 開發 20*60 10*60
Analysis 需求分析 (包括學習新技術) 2*60 5*60
Design Spec 生成設計文檔 20 15
Design Review 設計復審 10 10
Coding Standard 代碼規范 (為目前的開發制定合適的規范) 30 60
Design 具體設計 2*60 60
Coding 具體編碼 12*60 5*60
Code Review 代碼復審 20 30
Test 測試(自我測試,修改代碼,提交修改) 60 60
Reporting 報告 60 2*60
Test Repor 測試報告 30 30
Size Measurement 計算工作量 15 10
Postmortem & Process Improvement Plan 事后總結, 並提出過程改進計划 15 10
Sum 合計 5320 2340

思路描述

一開始拿到這道題,其實我是拒絕的,一直比較抗拒這種矩陣處理的題,看測試數據和題目描述就已經讓我想放上幾天〒▽〒。后來啊,DeadLine到了~~~
不管了,硬着頭皮也要上!首先是仔細地讀了一遍題,原來是需要實現一個能解數獨的程序!之前的畏難心理已經消退了不少;靜下心后打開搜索引擎,搜索數獨相關的資料,了解到數獨是需要我們去按規則填寫數字:同一行同一列不能出現相同數字,非素數階數還需要滿足每個宮內不能有相同數字

第一種想法 —— 蠻力法

既然是定好范圍的數字中選數填空,那肯定可以使用蠻力法:
生成所有可能用范圍內數字填充格子為0的解,檢查是否合法並保留解,但這樣的話,每個格子都需要跑一遍,這將是一個指數級算法,不能采用。

第二種想法 —— DFS

使用搜索來確定每個格子可以填充的數字,嵌套遞歸能使代碼量降低,但是自己只懂一點皮毛,目前花大量時間去練DFS有點來不及啦,遂作罷。

第三種想法 —— 回溯法

這個想法不是自己想到的,在閱讀大量關於求解數獨的 博客[1] 以及一個 題解視頻[2] 后,明白了其中的奧秘,代碼也不難,就決定是你了!

設計實現過程

本次程序設計采用定義與實現分離的寫法,所有操作由stdafx.cpp完成,聲明在stdafx.h中,參數接收由Sudoku.cpp完成,最終由編譯器鏈接成為Sudoku.exe:

  • 類(1個):solveSudoku
  • 函數(4個):init、helper、isValid為私有函數,solve為公有函數

其中solve函數整合了輸入輸出操作、init初始化盤面函數以及helper解題函數,在helper中使用isValid函數進行數字合法與否的判斷。流程圖如下:

單元測試

初版原文:關於測試方面,目前只會通過運行整個程序並通過輸入輸出是否達到預期的這種方式來測試。《構建之法》第二章提到了單元測試對於程序的重要性,這也是我目前學習的目標,截止到現在還未去進行實際操作,只了解到VS可以新建一個單元測試項目,導入需要測試的函數,若完成了測試會顯示綠勾(還是不太明白其中的原理,難道是想PTA那樣對比輸入輸出是否滿足預期?),之后補上。目前只會通過運行整個程序並通過輸入輸出是否達到預期的這種方式來測試,如下圖所示,最后三張圖片為異常:

前幾天課上講了一個三角形的單元測試例子,課后試下通過了——好像也不難嘛!於是想着去填坑,去嘗試做單元測試,我的思路是這樣的:

  1. 原項目的輸入輸出皆為文件,那么我需要將項目的實際輸出文件轉換為可以比較的內容。
  2. 轉換好后與預期結果用Assert::areEqual()比較就好了。

理想很豐滿,現實很骨感。在寫的過程中,我嘗試了以下方法:

  • 預期結果為文件,輸出結果為文件
    1. 用輸入流對其進行逐個字符的比較,每個字符都需要相同
    2. 將兩個文件中的內容在測試中全部先轉換為string,再比較string
  • 預期結果為string,輸出結果也為string
    1. 預期結果:在測試中用const string定下來
    2. 輸出結果:在類中提供一個方法,將輸出文件轉換為string
    3. 輸出結果:在單元測試項目中寫一個文件轉string的方法,運行解數獨程序后調用該方法
    4. 比較兩個string

上述方法皆以失敗告終——無論我如何去調用[3]solveSudoku類中的方法,返回的string皆為空,我佛了。(后來通過清空output輸出文件里面的內容,我才發運行單元測試時,這個類根本就沒有被運行!!!)

欲知后事如何===請看下次更新嗚嗚嗚嗚😭😭😭

三階

四階

五階

六階

七階

八階

九階

異常

  • 無解情況

  • argv參數設置錯誤

    后來發現argv[0]是路徑....學到了

性能分析

這次的性能分析用到了Visual Studio自帶的分析工具,路徑為:調試 >> 性能探測器
自動分析完后點擊切換到函數的視圖,點擊關心的調用次數 / 占用時間比,將自動按大小排序。

先上圖,這是整體概覽:

------------------------------------------ 分割線 ------------------------------------------

在《構建之法》第二章中根據函數調用次數及所耗時間比,優化了一個對象調用其大小的for循環條件,優化了程序性能。通過這張圖可以發現滿足優化條件的函數分別為helper和isValid,雖然vector的[]運算符重載函數被調用了7萬次,但對我而言是無法去做優化的,只能平靜的接受了~
helper優化的思路是加上DFS並剪枝(目前只在說說階段,還不知道怎么去實現),isValid優化的話——目前只想到將算術運算轉為二進制位運算來提升速度。

關鍵代碼及解析

要我選出這次程序設計的關鍵代碼,我會選擇helper函數:

/*
	@param board 盤面
	@param size  盤面大小
	該函數為數獨的回溯函數,通過反復調用和回溯達到解決問題的目的。
*/
bool solveSudoku::helper(vector<vector<char>>& board, int size)
{
	for (int i = 0; i < size; i++)	// 盤面遍歷
	{
		for (int j = 0; j < size; j++)
		{
			if (board[i][j] == '0')		// 填數條件
			{
				char max_num = size + '0';
				//	選數
				for (char num = '1'; num <= max_num; num++)	
				{
					// 合法性判斷
					if (isValid(board, size, i, j, num))
					{
						board[i][j] = num;
						// 進入下一層
						if (helper(board, size))  return true;	// 不回溯
						board[i][j] = '0';	// 回溯后的還原
					}
				}
				return false;	// 回溯
			}
		}
	}

	return true;	// 遍歷完成結束標志
}

首先我們從頭到尾遍歷盤面,如果當前位置是0,我們就進行填數操作,並判斷合法性。緊接着若數字合法,我們就將數填入盤面中,並進入到下一層,這是回溯法的關鍵:如果下一層判斷不合法,將回退到上一層並還原為0。在這一次次的推進與回溯中,程序將完成數獨的填寫操作。

心路歷程與收獲

  • 從畏難心理到坦然面對
  • 遇到問題應該靜下心來多讀幾遍題面和分析,而不是直接放掉
  • 了解到《構建之法》中關於如何完整構建一個軟件的思路和過程
  • 明白了單元測試的重要性,並將付諸實現
  • 了解到更加深層次的程序性能分析和優化,不能去盲目優化
  • 更清晰的了解到自己目前的水平,還需努力呀!

  1. 詳見參考文獻①。 ↩︎

  2. 詳見參考文獻②。 ↩︎

  3. 第一種調用:先 solveSudoku s; 然后 s.某函數;
    第二種調用:先 solveSudoku* s = new solveSudoku(); 然后 s->某函數; ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM