2020軟件工程作業03


這個作業屬於哪個課程 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

數獨的實現

類圖

Sudoku類圖

代碼圖

Sudoku代碼圖

  • InitializeWithFilePath用於從文件初始化

  • InitializeWithArray用於從array初始化

  • Solve用於求解

    根據m,n添加約束關系然后塞進Microsoft.Solver.Foundation線性規划庫中求解

  • ToString用於將array轉換成string

Sudoku類使用流程

  1. 構造函數 Sudoku
  2. 初始化 InitializeWithFilePath/InitializeWithArray
  3. 求解 Solve
  4. 轉成string ToString

Launcher

整個程序的入口,見輸入的參數轉化成鍵值對進行解析並調用數獨

UnitTest

使用Microsoft.VisualStudio.QualityTools.UnitTestFramework進行單元測試


5、性能改進

使用參數-m 9 -n 20 -i Z:\input.txt -o Z:\o.txt

vs性能探察器

vs性能探察器調用樹

dotTrace allcall

dotTrace overview

ConcurrencyVisualizer使用率

ConcurrencyVisualizer線程

絕大部分的時間花在了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的開銷

ConcurrencyVisualizer使用率20個多線程

ConcurrencyVisualizer線程20個多線程

在100個棋盤時單線程 2015ms

ConcurrencyVisualizer使用率100個單線程

ConcurrencyVisualizer線程100個單線程

在100個棋盤時多線程 988ms

ConcurrencyVisualizer使用率100個多線程

ConcurrencyVisualizer線程100個多線程

在100個棋盤時多線程 內存分析

dotMemory


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

數獨3x3

  • 數獨4x4

數獨4x4

  • 數獨5x5

數獨5x5

  • 數獨6x6

數獨6x6

  • 數獨7x7

數獨7x7

  • 數獨8x8

數獨8x8

  • 數獨9x9

數獨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)))));


免責聲明!

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



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