第二次作業——個人項目實戰
標簽(空格分隔): 軟工實踐
github的傳送門:work2、work2-附加題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、函數設計
設想用一個函數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
N = abc
N = 1000000,注意文件輸出大小
N = 0,輸出為空文件
改進的過程以及性能分析
初始版本的分析
一開始的時候,我的輸出是這樣子的:
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 ;
}
粗略自己先計算一下:
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));
}
再跑一下:
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);
}
測試如下:
10W次的全排列居然要3、4s?這個就特別不科學了.....這個時候,我大概知道問題出在哪里了...
突然就回憶起了今年多校10的那道毒瘤題= =....1000W級別的輸入,6秒的限制,卡fread才能過....
然后..我打開了vs的性能分析並且調試,這次改用N = 100W的輸入
雖然我第一次弄這個性能分析,看的不是很懂,但是我還是很容易就看出來了,這幾張圖都指出了一個很明顯的一點:printf()函數以及其的調用占了很大的時間比。
結合一下耗時,好了,該甩鍋的都甩鍋把,算法實際上這題占的耗時比例並不高,最大的耗時來源是在輸出方式上。
改進一
於是乎,我就先用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,進行粗略估計
效果顯著啊
改進二
然后接下來的嘗試是用puts()一次性輸出一個方案,即最終版本采用了這個方法。
再次直接上100W
果然,更加進一步的進行了優化
用vs性能分析查看
輸出只占了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,運行測試結果如下:
ZeroTest 未通過?查了一下,發現是因為,n = 0時,沒有東西輸出,這樣子sudoku.txt就相當於沒有刷新,保留的是上一次運行的測試的輸出結果。
所以只需加一句:
if(n==0) puts("");
代碼覆蓋率檢查
vs 2015 企業版直接使用代碼覆蓋率檢查
發現generator類中有未覆蓋到的段,點擊檢查
發現是重載的構造函數未使用到,新增測試點:
generator g(5);
g.check(100);
結果如下:
注: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 | | | | |