這個作業屬於哪個課程 | https://edu.cnblogs.com/campus/zswxy/software-engineering-2017-1/ |
---|---|
這個作業要求在哪里 | https://edu.cnblogs.com/campus/zswxy/software-engineering-2017-1/homework/10494 |
這個作業的目標 | Sudoku |
作業正文 | ↓ |
其他參考文獻 | giyf |
1、Github項目地址
https://github.com/iriszero48/20177713
2、PSP表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 5 | 5 |
Estimate | 估計這個任務需要多少時間 | 5 | 5 |
Development | 開發 | 950 | 1780 |
Analysis | 需求分析 (包括學習新技術) | 10 | 10 |
Design Spec | 生成設計文檔 | 60 | 30 |
Design Review | 設計復審 | 5 | 5 |
Coding Standard | 代碼規范 (為目前的開發制定合適的規范) | 0 | 0 |
Design | 具體設計 | 30 | 10 |
Coding | 具體編碼 | 720 | 1560 |
Code Review | 代碼復審 | 5 | 5 |
Test | 測試(自我測試,修改代碼,提交修改) | 120 | 160 |
Reporting | 報告 | 80 | 570 |
Test Repor | 測試報告 | 20 | 30 |
Size Measurement | 計算工作量 | 0 | 0 |
Postmortem & Process Improvement Plan | 事后總結, 並提出過程改進計划 | 60 | 540 |
合計 | 1035 | 2355 |
3、思路描述
知道的方法有幾種
- 回溯(蠻力搜索)
- 隨機搜索(模擬退火算法/遺傳算法/禁忌搜索算法/爬山算法/蟻群算法/霍普菲爾德神經網絡)
- amb
- 精確覆蓋問題(舞蹈鏈算法)
- 線性規划
為了偷懶選擇線性規划實現數獨求解
4、設計實現過程
建立三個項目
Sudoku
數獨的實現
類圖
代碼圖
-
InitializeWithFilePath用於從文件初始化
-
InitializeWithArray用於從array初始化
-
Solve用於求解
根據m,n添加約束關系然后塞進Microsoft.Solver.Foundation線性規划庫中求解
-
ToString用於將array轉換成string
Sudoku類使用流程
- 構造函數 Sudoku
- 初始化 InitializeWithFilePath/InitializeWithArray
- 求解 Solve
- 轉成string ToString
Launcher
整個程序的入口,見輸入的參數轉化成鍵值對進行解析並調用數獨
UnitTest
使用Microsoft.VisualStudio.QualityTools.UnitTestFramework進行單元測試
5、性能改進
使用參數-m 9 -n 20 -i Z:\input.txt -o Z:\o.txt
絕大部分的時間花在了Microsoft.Solver.Foundation的Solver上,無法優化
可以考慮對每個棋盤進行並行求解
使用OpenMP2.0對for進行並行,每次調度兩個線程
#pragma omp parallel for schedule(dynamic, 2)
for (auto i = 0; i < maps->Length; ++i)
{
...
}
在20個棋盤時會增加100~200ms的開銷
在100個棋盤時單線程 2015ms
在100個棋盤時多線程 988ms
在100個棋盤時多線程 內存分析
6、靜態分析、單元測試、運行
靜態分析
數獨9x9測試
[TestMethod]
void Sudoku9x9()
{
const Maps_ mp
{
{
{ 0,0,5, 3,0,0, 0,0,0 },
{ 8,0,0, 0,0,0, 0,2,0 },
{ 0,7,0, 0,1,0, 5,0,0 },
{ 4,0,0, 0,0,5, 3,0,0 },
{ 0,1,0, 0,7,0, 0,0,6 },
{ 0,0,3, 2,0,0, 0,8,0 },
{ 0,6,0, 5,0,0, 0,0,9 },
{ 0,0,4, 0,0,0, 0,3,0 },
{ 0,0,0, 0,0,9, 7,0,0 }
},
{
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,3, 0,8,5 },
{ 0,0,1, 0,2,0, 0,0,0 },
{ 0,0,0, 5,0,7, 0,0,0 },
{ 0,0,4, 0,0,0, 1,0,0 },
{ 0,9,0, 0,0,0, 0,0,0 },
{ 5,0,0, 0,0,0, 0,7,3 },
{ 0,0,2, 0,1,0, 0,0,0 },
{ 0,0,0, 0,4,0, 0,0,9 }
},
{
{ 1,1,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 },
{ 0,0,0, 0,0,0, 0,0,0 }
}
};
auto s = gcnew Sudoku(9, 3);
s->InitializeWithArray(ToMaps(mp));
Assert::AreEqual(R"(1 4 5 3 2 7 6 9 8
8 3 9 6 5 4 1 2 7
6 7 2 9 1 8 5 4 3
4 9 6 1 8 5 3 7 2
2 1 8 4 7 3 9 5 6
7 5 3 2 9 6 4 8 1
3 6 7 5 4 2 8 1 9
9 8 4 7 6 1 2 3 5
5 2 1 8 3 9 7 6 4
9 8 7 6 5 4 3 2 1
2 4 6 1 7 3 9 8 5
3 5 1 9 2 8 7 4 6
1 2 8 5 3 7 6 9 4
6 3 4 8 9 2 1 5 7
7 9 5 4 6 1 8 3 2
5 1 9 2 8 6 4 7 3
4 7 2 3 1 9 5 6 8
8 6 3 7 4 5 2 1 9
Unsolvable!
)", Sudoku::ToString(s->Solve()));
}
從程序基本的運行和非法值方面設計單元測試
- 數獨3x3
- 數獨4x4
- 數獨5x5
- 數獨6x6
- 數獨7x7
- 數獨8x8
- 數獨9x9
- 從文件輸入數據
- M在[3,9]外
- N小於0
- N的大小與輸入的數據不匹配
- M的大小與輸入的數據不匹配
測試結果
運行結果
- 數獨3x3
- 數獨4x4
- 數獨5x5
- 數獨6x6
- 數獨7x7
- 數獨8x8
- 數獨9x9
6、異常處理
數獨有什么異常應該是無法繼續跑,所以直接在main使用try...catch
try
{
...
}
catch (System::Exception^ exception)
{
fprintf(stderr, "Usage:\n %s -m 宮格階級 -n 待解答盤面數目 -i 指定輸入文件 -o 指定程序的輸出文件\n", argv[0]);
System::Console::Error->WriteLine(exception->ToString());
return EXIT_FAILURE;
}
參數不足8個
if (argc != 9)
{
throw gcnew System::Exception("parameter error");
}
初始化數獨,檢查mn是否有效
if (m < 3 || m > 9)
{
throw gcnew System::Exception("M must be 3 to 9");
}
if (n < 0)
{
throw gcnew System::Exception("N must be greater than -1");
}
對應單元測試,M應該是[3,9],這里初始化為2,所以拋出異常"M must be 3 to 9"
[TestMethod]
void MOutOfRange()
{
try
{
gcnew Sudoku(10, 1);
}
catch (System::Exception^ exception)
{
Assert::IsTrue(exception->ToString()->Contains("M must be 3 to 9"));
}
try
{
gcnew Sudoku(2, 1);
}
catch (System::Exception^ exception)
{
Assert::IsTrue(exception->ToString()->Contains("M must be 3 to 9"));
}
}
檢查mn是否與數據大小相匹配
if (maps->Length != n)
{
throw gcnew System::Exception("N size no matches");
}
for (auto i = 0; i < maps->Length; ++i)
{
if (maps[i]->Length != m)
{
throw gcnew System::Exception("M size no matches");
}
for (auto j = 0; j < m; ++j)
{
if (maps[i][j]->Length != m)
{
throw gcnew System::Exception("M size no matches");
}
}
}
對應單元測試,mp為9x9的數獨,保存時使用Substring去掉了第一個數字,變成了不規則數組,不滿足每行每列都是M個的要求,所以拋出異常"M size no matches"
[TestMethod]
void MSizeNoMatch()
{
const Maps_ mp
{
{
{ 0,0,5, 3,0,0, 0,0,0 },
{ 8,0,0, 0,0,0, 0,2,0 },
{ 0,7,0, 0,1,0, 5,0,0 },
{ 4,0,0, 0,0,5, 3,0,0 },
{ 0,1,0, 0,7,0, 0,0,6 },
{ 0,0,3, 2,0,0, 0,8,0 },
{ 0,6,0, 5,0,0, 0,0,9 },
{ 0,0,4, 0,0,0, 0,3,0 },
{ 0,0,0, 0,0,9, 7,0,0 }
}
};
const auto filename = gcnew System::String("FileInput.txt");
System::IO::File::WriteAllText(filename, Sudoku::ToString(ToMaps(mp))->Substring(2));
auto sudokuFile = gcnew Sudoku(9, 1);
try
{
sudokuFile->InitializeWithFilePath(filename);
}
catch (System::Exception^ exception)
{
Assert::IsTrue(exception->ToString()->Contains("M size no matches"));
}
System::IO::File::Delete(filename);
}
7、代碼說明
數獨的核心是Sudoku::Maps^ Sudoku::Solve()函數
流程圖
代碼
Sudoku::Maps^ Sudoku::Solve()
{
using namespace Microsoft::SolverFoundation::Solvers;
// 創建一個三維數組存放結果
auto res = gcnew Maps(maps->Length);
for (auto i = 0; i < maps->Length; ++i)
{
res[i] = gcnew Map(maps[i]->Length);
for (auto j = 0; j < maps[i]->Length; ++j)
{
res[i][j] = gcnew array<int>(maps[i][j]->Length);
}
}
// 並行遍歷每個盤面,同時調度兩個線程
#pragma omp parallel for schedule(dynamic, 2)
for (auto i = 0; i < maps->Length; ++i)
{
// 初始化
auto map = maps[i];
auto s = ConstraintSystem::CreateSolver();
const auto z = s->CreateIntegerInterval(1, maps[i]->Length);
auto lp = s->CreateVariableArray(z, "n", maps[i]->Length, maps[i]->Length);
// 為每行和已知條件添加約束
for (auto row = 0; row < maps[i]->Length; ++row)
{
for (auto col = 0; col < maps[i]->Length; ++col)
{
if (map[row][col] > 0)
{
s->AddConstraints(s->Equal(map[row][col], lp[row][col]));
}
}
s->AddConstraints(s->Unequal(GetSlice(lp, maps[i]->Length, row, row, 0, maps[i]->Length - 1)));
}
// 為每列添加約束
for (auto col = 0; col < maps[i]->Length; ++col)
{
s->AddConstraints(s->Unequal(GetSlice(lp, maps[i]->Length, 0, maps[i]->Length - 1, col, col)));
}
// 為不同盤面階數設置宮格大小
auto stepRow = 0;
auto stepCol = 0;
switch (maps[i]->Length)
{
case 4:
stepRow = 2;
stepCol = 2;
break;
case 6:
stepRow = 2;
stepCol = 3;
break;
case 8:
stepRow = 4;
stepCol = 2;
break;
case 9:
stepRow = 3;
stepCol = 3;
break;
default:;
}
// 如果當前盤面階數存在宮格,則為每個宮格添加約束
if (stepRow != 0 && stepCol != 0)
{
for (auto row = 0; row < maps[i]->Length; row += stepRow)
{
for (auto col = 0; col < maps[i]->Length; col += stepCol)
{
s->AddConstraints(
s->Unequal(
GetSlice(lp, stepCol * stepRow, row, row + stepRow - 1, col, col + stepCol - 1)));
}
}
}
// 求解
auto sol = s->Solve();
try
{
for (auto row = 0; row < maps[i]->Length; ++row)
{
for (auto col = 0; col < maps[i]->Length; ++col)
{
res[i][row][col] = sol->GetIntegerValue(lp[row][col]);
}
}
}
catch (System::Exception^) // 無解情況返回nullptr
{
res[i] = nullptr;
}
}
return res;
}
8、心路歷程與收獲
剛開始要求寫數獨想着用微軟的Microsoft.Solver.Foundation線性規划庫偷懶,然后看見要求“使用C++或者Java語言實現”,就決定使用C++/CLI方言,因為Microsoft.Solver.Foundation是基於.NET Framework的(后面老師說不限定語言,但是已經開工了,就繼續C++/CLI了),C++/CLI能直接用.NET Framework的庫,順便還能把FSharp的核心庫也拿來用用,寫着寫着,發現C++/CLI和F#交互太麻煩了,把函數作為參數傳遞給F#折騰了幾個小時,最后決定寫個FSharpFuncUtil來解決這個問題,FSharpFuncUtil將System.Func<'a,'b>轉換成FSharpFunc<'a,'b>,System.Func可以gcnew出來,自定義函數也能定義個ref class塞個靜態函數gcnew解決問題,原本還想整個類似於std::Bind1st一樣的函數用於FSharpFunc/System.Func,想到萬一是個大坑就沒整了,最后放上這個程序真正的一行核心代碼(x
maps = ArrayModule::Take(n, ArrayModule::Map(FSharpFuncUtil::FSharpFuncUtil::ToFSharpFunc(gcnew System::Func<array<int>^, array<array<int>^>^>(Func::ChunkBySize)), ArrayModule::ChunkBySize(m * m, ArrayModule::Map(FSharpFuncUtil::FSharpFuncUtil::ToFSharpFunc(gcnew System::Func<System::String^, int>(System::Int32::Parse)),System::IO::File::ReadAllText(path)->Replace("\n", " ")->Replace("\r", " ")->Split(mapSp, System::StringSplitOptions::RemoveEmptyEntries)))));