第二次作業——個人項目實戰


第二次作業——個人項目實戰

標簽(空格分隔): 軟工實踐


github的傳送門:work2work2-附加題2

題目的傳送門

上次作業的評分總結

一、解題思路

看到題目的一反應就是:在不考慮效率的前提下,這題搜索是一定可以做的。
但是有要求:

25分為正確性評分,正確性測試中輸入范圍限制在 1-1000,要求程序在 60 s 內給出結果,超時則認定運行結果無效。
10分為性能評分,性能測試中輸入范圍限制在 10000-1000000,沒有時間的最小要求限制。

所以普通的搜索應該是不行的,但是思考一下,如果使用回朔法(暴力+剪枝)效果怎么樣呢?因為回溯的時間復雜度比較玄學,我不敢下定論,
思考了一會后,我覺得,數獨應該是一個比較成熟的項目,應該可以比較容易的找到與其生成方法的相關資料。

百度了之后,發現提到的數獨生成算法,大部分都是用類似'換行換列'之類的思想:先預處理生成或者直接手動輸入一個數獨,然后進行交換,這樣子就可以生成新的數獨.

好像很有道理的樣子,所以我決定,搜索和上面提到的這個算法都實現一下,比比效率和實現的難易程度(這個時候我把自己坑到了,我一直以為數獨是同行同列不能重復數字,沒有注意到同九宮格也不能重復數字,也沒有仔細看題,所以一開始的思路也是錯的,但是誤打誤撞還真分析了出了點東西)。

為了體現隨機性,我使用隨機排列函數random_shuffle來隨機第一行的數字1~9的填放方法。
那么問題來了,這樣子的隨機對搜索來說,搜索的起點變了,但搜索的終點(如果需要全部搜索完畢的話)是不會變的,因此會造成搜索到的方法數變小,那么會不會出現比作業需求的方案數少的情況呢?

數獨-- 一個高效率生成數獨的算法中我得知,數獨方案數約有6.67×10的21次方種,因此,第一排的隨機,即使最壞情況下:生成的第一排的排列為1~9的逆序:

9 8 7 6 5 4 3 2 1

但即使如此,方案數最壞情況下是:上述的數字除以9的階乘,回溯能查詢到的方案數依然遠大於於題目的需求。
而'換行換列'的方法,更不會局限於此。

搜索的方法很簡單:把9*9的方格用0~81來標號,暴力的從10~81依次填入1~9,(0~8事先填好了),每填一個數字前先判斷一下填入當前的數字是否滿足填入規則:不能與已經填入的格子的數字產生沖突,如果可行,就選擇下一個格子繼續填,不行就放棄這個方案,很裸的暴力,思考量很小。

關於'換行換列':由於本人的不認真查閱資料和閱讀,我只看見了這一句話沒認真思考便動手碰起了鍵盤

於是我產生了一個看上去比較WS的算法,思路如下,我先寫一個數獨出來,用二維數組A表示,然后再生成一個亂序的1-9一維數組,用一維數組b表示。

--from 數獨-- 一個高效率生成數獨的算法

我是想先生成一個數獨,再用用了全排列函數,想通過全排列函數來生成新的排列來達到生成新數獨的目的。
(當然...后面發現我這樣子想實際上是錯的= =...)

二、設計實現

1、函數設計

函數關系2
設想用一個函數solve()來全局的處理,期間調用一個init()函數來初始化所有需要用到的變量,然后通過dfs()函數來進行回溯和搜索操作,並且在dfs()函數中來輸出方案。

形式如下:

void solve()
{   
	init();     
	.......     
	dfs(10);    //回溯,暴搜+剪枝
}

dfs()函數需要完成三個功能:遞歸、剪枝、方案的輸出。
其中剪枝又有兩種:無效方案的去除以及搜索了足夠多(滿足輸入需求)的方案后的剪枝。
形式如下:

void dfs(int t)
{
	if (flag) return;
	if (t>81)					
	{
	    ....//輸出方案
		if 輸出了n個方案  flag = true;
		return;
	}
				
	rep(i, 1, 10)			//當前選擇可以放的數字 
	{
		if 不滿足條件 continue;
	    .....//遞歸
	}
}

2、類設計

類的設計我一開始沒有考慮到,因為我的函數關系相對比較簡單,直接寫即可。
后來加上了一個 generator類,把上述函數封裝了起來,然后給solve()函數傳遞一個參數n,表示方案數,這樣子就可以不受main的輸入方式(XX.exe -c num or 直接運行exe輸入)的干擾。

三、代碼說明

1.隨機排列的實現

題目的輸出要求是:
隨機生成N個不重復已解答完畢的數獨棋盤.
隨機如何體現?(我覺得實際上肯定沒有隨機這個測試點),輸入同一個N如何讓輸出的答案不完全相同呢?單純的搜索是做不到的。做法就是用隨機函數來實現。但是C++的STL有封裝了類似的功能,就是用random_shuffle(begin(),end())來將其中的元素隨機打亂順序。


//初始化1~9
rep(i, 0, 10) ways[1][i] = i + '0';
//尾號48,48%9+1 = 4,第一個位置要是4
swap(ways[1][1], ways[1][4]);   
	
//實現隨機全排列,隨機的關鍵 
random_shuffle(ways[1] + 2, ways[1] + 10);

2.搜索過程

搜索的需要三組標記:行標記、列標記、九宮格標記,表示某行、列..哪些數字已經使用過了。

    int x = (t - 1) / 9 + 1;	//當前搜索到哪個格子 	
	int y = (t - 1) % 9 + 1;
	int p = belong[x][y];	//當前格子屬於哪一個九宮格		 
				
	rep(i, 1, 10)			//當前選擇可以放的數字 
	{
	    //當前行、列、九宮格中使用過
		if (row[x][i] || col[y][i] || vis[p][i]) continue;
		
        //標記 
		row[x][i] = col[y][i] = vis[p][i] = true;		
		ways[x][y] = i + '0';			
		//下一個格子 
		dfs(t + 1);						
		//去標記 
		row[x][i] = col[y][i] = vis[p][i] = false;		
	}

3.輸出方式

經過優化后的輸出方式,有一點巧妙,用puts()輸出,用字符串表示方案,一個方案一次輸出,可以快很多

預處理部分

	//預處理出輸出格式 
    cnt = 0;
	rep(i, 1, 10)
	{
	    //一個數字,一個空格 
		rep(i, 1, 9) put[cnt++] = 'X', put[cnt++] = ' ';	
		//行末無空格 
		put[cnt++] = 'X'; put[cnt++] = '\n';				
	}
	put[--cnt] = '\0';//最后一行后無'\n';

實際輸出部分

	
    if (ans++) puts("");			//兩個方案之間有空格 
    cnt = 0;				//初始化
	rep(i, 1, 10) rep(j, 1, 10)         //for循環遍歷每個格子
	{
	    //兩個數字之間的空格還是換行已經預處理過了
		put[cnt++] = ways[i][j], ++cnt;	 
    }
	puts(put);					//輸出

四、測試運行

測試數據主要從極端數據考慮:錯誤輸入0處理極限數據三個角度入手,並且自己手寫了一個check函數要進行正確性的驗證
check函數主要實現如下:

bool check()
{
	bool col[10][10] = {false};
	bool row[10][10] = {false};
	bool vis[10][10] = {false};
	string s = "";
	rep(i,1,10) rep(j,1,10)
	{
		int num = a[i][j];
		s += (char)(num+'0');
		int t = belong[i][j];
		if(vis[t][num]||col[i][num]||row[j][num]) return false;
		vis[t][num] = col[i][num] = row[j][num] = true;
	}
	if(mp[s]) return false;     //是不是輸入有重復
	mp[s]++;
	return true;
}

N = 1

運行1

N = abc

運行2

N = 1000000,注意文件輸出大小

運行3

N = 0,輸出為空文件

運行4

改進的過程以及性能分析

初始版本的分析

一開始的時候,我的輸出是這樣子的:

    if(t>81)//輸出方案 
	{	
		if(ans) puts("");			//兩個方案之間有空格 
		++ans;
		rep(i,1,10) rep(j,1,10)
			printf("%d%c",ways[i][j]," \n"[j==9]);	//兩個數字之間有空格 
		if(ans>=n) flag = true;
		return ;
	}

粗略自己先計算一下:
初始版本1

10W跑了大概20+s,難道是回溯太慢了?

寫一下'換行換列'的方法:

void f(int (*a)[10])
{
	int b[10] = {0,1,2,3,4,5,6,7,8,9};
	do
	{
		if(n<=0)break;
		if(flag) puts("");
		else flag = true;
		rep(i,1,10) rep(j,1,10) 
			printf("%d%c",a[i][b[j]]," \n"[j==9]);
		n--;
	}while(next_permutation(b+1,b+10));
	
}

再跑一下:
初始版本2

exm???還是20+s?這個就很不科學了,這個的復雜度應該很低才對啊?難道是全排列函數next_permutation跑得很慢???

於是我寫了一個測試:

    int a[] = {0,1,2,3,4,5,6,7,8,9};
	int n;
	cin >> n;
	while(n--)
	{
		rep(i,1,10) printf("%d ",a[i]);printf("\n");
		next_permutation(a+1,a+10);
	} 

測試如下:
初始版本3
10W次的全排列居然要3、4s?這個就特別不科學了.....這個時候,我大概知道問題出在哪里了...
突然就回憶起了今年多校10的那道毒瘤題= =....1000W級別的輸入,6秒的限制,卡fread才能過....

然后..我打開了vs的性能分析並且調試,這次改用N = 100W的輸入

性能分析1
性能分析2
初始版本3

雖然我第一次弄這個性能分析,看的不是很懂,但是我還是很容易就看出來了,這幾張圖都指出了一個很明顯的一點:printf()函數以及其的調用占了很大的時間比

耗時2

結合一下耗時,好了,該甩鍋的都甩鍋把,算法實際上這題占的耗時比例並不高,最大的耗時來源是在輸出方式上

改進一

於是乎,我就先用putchar()進行了二次嘗試.輸出由int轉換為char,並且用putchar()。

        if(ans) puts("");			//兩個方案之間有空格 
		++ans;
		rep(i,1,10) 
		{	
			rep(j,1,9)
				putchar(ways[i][j]),putchar(' ');	//兩個數字之間有空格 
			putchar(ways[i][9]);puts(""); 
		}

直接上100W,進行粗略估計
初始版本4

效果顯著啊

改進二

然后接下來的嘗試是用puts()一次性輸出一個方案,即最終版本采用了這個方法。

再次直接上100W
初始版本5

果然,更加進一步的進行了優化

優化耗時

用vs性能分析查看
優化查看2
優化查看1

輸出只占了5.8%,dfs()本身成了耗時最大的函數。

改進三

模擬緩沖區,用puts()一次性輸出多個方案(附加題中使用)。

答案檢查以及修改

單元測試

將自己寫的check函數封裝成check類,寫入單元測試的項目中去,經過某犇犇的指點,得知了可以用文件的輸入輸出的方法,先將自己的generator類輸出的答案輸出到文件中去,然后再關閉輸出通道,同時打開該文件的讀入,這樣子就可以實現check。
check主要代碼如下:

bool CHECK::check(int k)
{
	init();
	int cas = 0;
	bool f = true;
	while (~scanf("%d",&a[1][1]))
	{
		++cas;
		rep(i, 1, 10)
		{
			if (i == 1) rep(j, 2, 10) scanf("%d", &a[i][j]);
			else rep(j, 1, 10) scanf("%d", &a[i][j]);
		}
		
        //judge用於檢測生成的數獨是不是正確,前面有帖寫過代碼
		if (!judge()) f = false;   
	}
	if (cas != k) f = false;        //是不是文件中產生了k個數獨
	return f;
}

單元測試代碼:

TEST_METHOD(TestMethod2)
{
	// TODO: 在此輸入測試代碼
	generator g;
	freopen("sudoku.txt", "w", stdout);//讀出文件
	g.solve(10000); //生成數獨
	fclose(stdout); //關閉讀出文件
	freopen("sudoku.txt", "r", stdin);//讀入文件

	CHECK h;
	Assert::IsTrue(h.check(10000));
}

進行測試的數據為:0、100、1000、10000、1000000,運行測試結果如下:
代碼覆蓋率12
ZeroTest 未通過?查了一下,發現是因為,n = 0時,沒有東西輸出,這樣子sudoku.txt就相當於沒有刷新,保留的是上一次運行的測試的輸出結果。
所以只需加一句:

    if(n==0) puts("");

代碼覆蓋率檢查

vs 2015 企業版直接使用代碼覆蓋率檢查
代碼覆蓋率2

發現generator類中有未覆蓋到的段,點擊檢查

代碼覆蓋率1

發現是重載的構造函數未使用到,新增測試點:

    generator g(5);
    g.check(100);

結果如下:
代碼覆蓋率4

代碼覆蓋率8

注:judge中未覆蓋的片段為返回值為false時候的片段。

附加題二

隨機生成N個 不重復 的 有唯一解 的數獨棋盤。挖空處用數字0表示,每個數獨棋盤中至少要有 30 個以上的0。輸出格式見下輸出示例,輸出需要到文件sudoku.txt中。

解題思路**

初始版本

在第一題的基礎上,每生成一個數獨,就隨機挖掉35個空,生成5個滿足條件挖空的數獨,並且每挖一次空,都進行check(查看挖掉一個空后,對該數獨進行填空,查看是不是有唯一解)。

性能分析如下:

100W數據耗時大約:39s

改進版本

對初始版本進行性能分析后,發現,GetPoint()函數占用率十分的高,這個函數的作用是:**對當前數獨再挖一個空,並且滿足填空后數獨是唯一解**

在初始版本的基礎上,增加了隨機函數的隨機性功能,從原來的每挖一個坑就進行一個check,改成先預隨機挖掉30個空,然后進行check,如果不滿足條件,再重新生成30個空,之后每挖一個空,進行一次check.

100W數據大約耗時17s

最終版本

做完第二個版本后,總是在想,能不能跑的更快一點呢?突然就來了靈感,想到:如果一個數獨挖了40個空是滿足唯一解的,那么從中任意選31個,那么新的數獨也是唯一解的,然后算一下C(40,31) = 273438880,大於100W,因此,我的做法是:
先用改進版的方法生成一個挖了40個空的數獨,然后從用回溯的方法,選擇挖空數大於31的數獨。
100W數據大約耗時2s

遇到的困難及解決方法

沒有及時的記錄,因此記得不全。

1、vs使用生疏,代碼分析規則不了解

問題描述

vs已經很久沒有使用了,先用dev C++ 寫完初稿后,一開始不知道如何在vs上創建項目,如何使用那些性能分析之類的。
dev C++的代碼弄到vs上不能直接運行,如freopen會報錯。

做過哪些嘗試

找博客查詢、找其他同學互相幫助一起解決。

是否解決

解決了,通過查詢博客可以解決大部分問題.和其他同學探討也解決了一些

有何收獲

vs用更加熟悉了。

2、代碼覆蓋率 不知道如何下手

問題描述

vs 專業版沒有代碼覆蓋率的功能,仔細閱讀了作業要求后發現,居然,你們居然偷偷的在代碼覆蓋率前面加了插件兩個字。
然后...我是把這個留到了最后來做的..離deadline only 2 天。

做過哪些嘗試

找博客查詢、找其他同學互相幫助一起解決。
1、安裝插件 opencppcoverage,然后發現安裝這個插件后還要安裝Jenkins,然后。。Jenkins..好麻煩啊= =....我的8080端口已經有其他東西了..然后..感覺在2天之內是不可能完成的任務。
2、安裝vs 2015 企業版,在舍棄了1的方案后,我決定卸載了我的vs 專業版.因為vs卸載是一件很麻煩的事情,很可能失敗,但是我居然安裝成功了....

是否解決

解決了。

有何收獲

對代碼覆蓋率功能有了更深入的了解.也知道了一些開源的插件。

執行力 、 泛泛而談 的理解

執行力我覺得是和個人的養成習慣有關,有的人有類似拖延症的毛病,做事情永遠是能推遲就推遲。這就導致了,在deadline臨界點,是趕工or缺交。
從某種意義上,對一件事情的重視(重要)程度,也可以在個人的執行力上體現。
也。
我習慣性的把一個大的問題分成很多個小問題,分散成很多很多個時間片去做。
其實說到底,都是意志力的問題。關鍵在於自己想不想做,願不願意花時間去做而已。
就比如..我實際上是對於作業是不拒絕的,但是我不喜歡寫博客之類的.我喜歡慢慢玩,慢慢做,東西一點一點的來做,所以經常都是理論上我作業做完了,但是由於博客之類偏理論性的東西不愛做,實際上我做一件事情(完全做完)依然會拖延到很遲。

泛泛而談,我也會這樣子做,我覺得泛泛而談有時候挺有好處的,給自己靈活變動的時間,誰都難免以外的時候突然到來,比如:今天下午我要和隊友訓練acm5小時,突然來了一個通知,今天下午補課,所以呢?為什么我訂的目標不是:盡可能的用剩下空閑的時間就來訓練。,或者說,自己發現有了更好的計划,覺得新的時間調整更加的合理,既然如此,為什么不給自己事先就預留一些靈活的使用時間,想做什么工作就做什么。
還有就是,自己本身做具體規划的時間的比較少,本身就模棱兩可不知道如何規划比較好,特別是我這種有時候會較真的人,說着做2小時,突然發現某個奇怪的問題,你可能就載進去做別人看起來沒有意義的事情,這樣子就讓時間規划變得沒有意義,但是我卻會覺得樂在其中.
關於經驗方面的泛泛而談,我也就不太了解,可能是出於謙虛的說話,或者本身沒有什么成績可說?一般人在介紹自己如果不是給專業人人士說的話,說一堆具體的還不如一句'經驗豐富'。

不過,當然,不能全部都是泛泛而談,因為泛泛而談很多時候會讓你缺乏執行力,結合了自身情況,我發現自己在晚自習的時候經常容易走神,為了盡可能的減少這個情況,我給自己定下了目標:一小時只能動一次手機且不能超過15min、每天晚上堅持晚自習,等等,制定具體的目標可以給人一種'緊張'或者說是'重視'的效果,讓自己更加有計划,不會處於'茫然'的狀態,很多人可能因為沒有具體的計划就白白浪費了一天。

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划 5 5
· Estimate · 估計這個任務需要多少時間 5 5
Development 開發 890 1020
· Analysis · 需求分析 (包括學習新技術) 180 420
· Design Spec · 生成設計文檔 120 90
· Design Review · 設計復審 (和同事審核設計文檔) 60 20
· Coding Standard · 代碼規范 (為目前的開發制定合適的規范) 120 30
· Design · 具體設計 20 10
· Coding · 具體編碼 240 60
· Code Review · 代碼復審 30 30
· Test · 測試(自我測試,修改代碼,提交修改) 120 360
Reporting 報告 240 575
· Test Report · 測試報告 60 90
· Size Measurement · 計算工作量 10 5
· Postmortem & Process Improvement Plan · 事后總結, 並提出過程改進計划 30 480
合計 1725 1600

|第N周 | 新增代碼 (行)|累計代碼(行)|本周學習耗時(小時)|累計學習耗時(小時)|重要成長|
|-------------------------|----------------|-----------------------------------------|------------------|------------------|
|0 | 1000 | 1000| 40 | 40 |vs的使用,項目創建、性能分析等|
|N | | | | |


免責聲明!

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



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