1. 程式人生 > >#2018BIT軟件工程基礎#個人項目:數獨

#2018BIT軟件工程基礎#個人項目:數獨

算法 我認 遍歷 png 一場 make 合法性 前三 分享圖片

一、開發時間

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

二、解題思路描述

顯然-c與-s要通過不同的方法去實現,畢竟一個是生成數獨,另一個是解數獨,看起來是兩種不同的操作。

1)關於數獨的生成(-c)

網上的做法多種多樣,其中思路較為簡單的是回溯法。但在我看來,回溯的方法總是效率低下的,所以我采用了用一個種子(即9X9宮格的部分)去生成整個數獨終局的辦法,只要保證種子的隨機性與不重復性,即可保證整個數獨終局的隨機性與不重復性。

對於9X9的數獨,取左上角3X3的宮格進行分析。由於我的學號末尾是49,則最左上角的宮格為(4+9)%9+1=5,則剩下的宮格有8!=40320種排列,與要求的1000000個不同數獨還差兩個0的數量級。這時候把目光轉向第二個3X3的宮格,則第一行填法有A(6,3)種,而A(6,3)=120正好為兩個0的數量級。這樣一來,只要確保第一個3X3宮格與第二個3X3宮格第一行(即圖中的ABC)的唯一性,通過如圖交換行列的做法,就可以生成1000000個不同的數獨。

技術分享圖片

2)關於數獨的解決(-s)

在網上以及和宿舍同學學習了一圈,找到一種業內公認最快的方法叫DXL(舞蹈鏈)的做法。盡管還是用到了我不太喜歡的dfs,但起碼這個dfs是有界的,就在一個鏈表中遞歸。算法的具體說明在舍友推薦的這篇博客裏已經說得清楚明白http://www.cnblogs.com/grenet/p/3163550.html。我本人對於一些細節的實現還一知半解,但會用就行。

把解數獨轉化為精確覆蓋問題關鍵在於怎麽生成雙向鏈表。對於鏈表的行來說,一共9X9個小格,每個小格有9種填法,則可生成9X9X9=729行;對於列,有(9+9+9)X9+81=324列,前面三個9代表數獨宮格中的9行9列9小塊,乘九意思是九種可能,81代表9X9共81個格子中每格只能放一個數字。讀入數據後,如果為0,則表示可放9種數字,建9行,否則只能放一個已知數字,建一行。

生成了雙向鏈表,就可以用dfs愉快地解題了!


三、設計實現

在主函數main中,包含以下三個類:

1)輸入處理類:根據參數調用下列2)、3)函數進行相應處理(包括參數合法性判斷)。
合法性判斷包括:參數是否合法,是否輸入非法字符,輸入數字是否越界。

2)終盤生成類:終盤生成函數(generator)、排列組合函數(PailieZuhe)、查重函數(CheckRepete)、輸出函數(Display),調用關系如圖。

技術分享圖片
3)數獨求解類:鏈接構建函數(Makecolhead)、結點插入函數(Add)、移除函數(Remove)、恢復函數(Resume)、深度優先遍歷函數(Dfs),核心的Dfs函數流程圖如下:

技術分享圖片


四、優化改進

1、對於“-c”操作,由於生成種子法的先天優勢,在Release x64模式下生成1000000數獨僅需要22s。根據性能分析器顯示,主要的時間開銷集中在一個叫[ucrtbase.dll]的塊中。點開詳細一看,調用[ucrtbase.dll]最多的函數是printf()輸出函數。

技術分享圖片

技術分享圖片

根據網上的資料顯示,用put類型字符串輸出的效率比printf()效率要高,於是將輸出終局改為用puts()以字符串形式輸出,效率果然大大提高。整個程序的花銷主要變為是main函數與Generator模塊這兩個整體,而不是某一個局部,我認為繼續優化的空間不大了。

技術分享圖片

優化前後生成1000000w數獨終局的時間對比如下:

模式 優化前 優化後
Release x64 22.21s 5.995s

同理,對於“-s”操作,輸入采用getchar(),輸出采用puts(),時間也有了十幾秒左右的提高。

2、對於"-s"操作,起初改寫了網上的一版我看得懂的所謂大神DLX模板,解1000000個數獨竟用了10m29s之久,這效率實在是太低下了。根據程序性能分析器顯示,時間開銷最大的是在建立雙向列表的函數build()內一個雙重的729*324的for循環(下圖中的黃色框框內)。

技術分享圖片

技術分享圖片

這可使我犯了難,建立雙向列表是一個固有操作,應該怎麽更改?想了許久沒有頭緒。好在同宿舍的大神舍友也用DXL法,而他的程序解1000000的數獨僅用不到一分鐘。他的程序我看不太懂,但是方法思路基本掌握了,在生成行和列的同時就構建雙向鏈表,這樣就不用等到生成完了所有行和列,再用雙重for循環逐個排查來構建雙向列表。但先前的版本畢竟是仿別人的模板,我也不好修改,只能重寫一份,當做借鑒的教訓吧。如此一來,效率的確大大提高,時間的主要開銷轉移到了dfs函數。可鑒於目前我的水平,剪枝並不是我的強項,我優化的努力只能到此為止了。

技術分享圖片

優化前後解1000000數獨的時間對比如下:

模式 優化前 優化後
Release x64 10m29s 55.795s

五、關鍵代碼展示

1)輸入參數的判斷:分參數異常,出現非法字符,溢出等情況。

/*輸入檢驗*/
    if (strcmp(argv[1], "-c") == 0)//生成數獨終局
    {
        int N = 0;
        for (int i = 0; i < strlen(argv[2]); i++)
        {
            if (argv[2][i] < 48 || argv[2][i] > 57)
            {
                printf("Wrong Input!\n");//非法輸入(錯誤字符)
                return -1;
            }
            else
            {
                N = N + (argv[2][i] - 0) * pow(10, (strlen(argv[2]) - i - 1));
                if (N < 0 || N>1000000)
                {
                    printf("Overflow!\n");//非法輸入(越界)
                    return -2;
                }
            }
        }
        int sudo[9][9];
        Generator(N, sudo);
    }
    else if (strcmp(argv[1], "-s") == 0)//解數獨
    {
        
    }
    else if (strcmp(argv[1], "-c") != 0 && strcmp(argv[1], "-s") != 0)//錯誤參數
    {
        printf("Wrong Input!\n");
        return -3;
    }

2)"-c"生成隨機不重復數獨終局的generator部分的主要代碼,思路為:

先使用隨機的排列組合生成一個3X3的種子宮,使用查重函數判斷,如果不重復,則繼續。

用全排列生成第二個3X3種子宮A(6,3)=120個不同的排列,逐一與第一個3X3種子宮匹配,交換行列生成數獨終局輸出,當用完這120個排列時,回到隨機生成3X3種子宮一步。

using namespace std;

extern vector<vector<int>> arrange;

void Get_Seedbox(vector<int> &Seed_Box)//隨機生成開頭為5的3x3種子宮
{
    Seed_Box = Pailie_Zuhe_Random(Seed_Box);
    for (int i = 0; i < 9; i++)
    {
        if (Seed_Box[i] == 5)
        {
            swap(Seed_Box[i], Seed_Box[0]);
            break;
        }
    }

}
void Set_Sudo(int(*sudo)[9], const vector<int> &Seed_Box, int count)//初始化函數
{
    for (int i = 0; i < 9; i++)
        for (int j = 0; j < 9; j++)
            sudo[i][j] = 0;

    for (int i = 0, k = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            sudo[i][j] = Seed_Box[k++];

    sudo[0][3] = arrange[count][0];
    sudo[0][4] = arrange[count][1];
    sudo[0][5] = arrange[count][2];
}
void Generator(int N, int(*sudo)[9])
{
    vector<int> Seed_Box;
    vector<int> Tmp_Box;

    for (int i = 0; i < 9; i++)
        Seed_Box.push_back(i + 1);

    while (N != 0)
    {
        Get_Seedbox(Seed_Box);
        if (!Check_Rep(Seed_Box))//檢驗重復性
            continue;

        Tmp_Box.assign(Seed_Box.begin() + 3, Seed_Box.end());//獲取第一宮第一行前三個外的6個數字,生成所有唯一的A(6,3)
        Pailie_Zuhe_All(Tmp_Box);
        int count = 0;

        while (N != 0)
        {
            if (count < 120)
                Set_Sudo(sudo, Seed_Box, count++);
            else
                break;

            for (int i = 1; i < 3; i++)//生成第二宮的第二行和第三行
            {
                Tmp_Box.assign(Seed_Box.begin(), Seed_Box.end());//刪除同行在第一宮的三個數
                Tmp_Box.erase(Tmp_Box.begin() + i * 3);
                Tmp_Box.erase(Tmp_Box.begin() + i * 3);
                Tmp_Box.erase(Tmp_Box.begin() + i * 3);

                for (int j = 0; j < Tmp_Box.size(); j++)//刪除已排序的數
                    if (Tmp_Box[j] == sudo[0][3] || Tmp_Box[j] == sudo[0][4] || Tmp_Box[j] == sudo[0][5] ||
                        Tmp_Box[j] == sudo[1][3] || Tmp_Box[j] == sudo[1][4] || Tmp_Box[j] == sudo[1][5])
                        Tmp_Box.erase(Tmp_Box.begin() + (j--));
                /*第二行時只刪了三個,多刪三個*/
                if (Tmp_Box.size() == 6)
                    Tmp_Box.erase(Tmp_Box.begin());
                if (Tmp_Box.size() == 5)
                    Tmp_Box.erase(Tmp_Box.begin());
                if (Tmp_Box.size() == 4)
                    Tmp_Box.erase(Tmp_Box.begin());

                Tmp_Box = Pailie_Zuhe_Random(Tmp_Box, Tmp_Box.size(), 3);//由於A(6,3)的唯一性,第二宮剩下的行任意生成一種排法即可
                sudo[i][3] = Tmp_Box[0];
                sudo[i][4] = Tmp_Box[1];
                sudo[i][5] = Tmp_Box[2];
            }
            for (int i = 0; i < 3; i++)//生成第三宮
            {
                Tmp_Box.assign(Seed_Box.begin(), Seed_Box.end());
                for (int j = 0; j < Tmp_Box.size(); j++)//刪除第一二宮同行的數
                    if (Tmp_Box[j] == sudo[i][0] || Tmp_Box[j] == sudo[i][1] || Tmp_Box[j] == sudo[i][2] ||
                        Tmp_Box[j] == sudo[i][3] || Tmp_Box[j] == sudo[i][4] || Tmp_Box[j] == sudo[i][5])
                        Tmp_Box.erase(Tmp_Box.begin() + (j--));
                Tmp_Box = Pailie_Zuhe_Random(Tmp_Box, 3, 3);//由於A(6,3)的唯一性,第二宮剩下的行任意生成一種排法即可
                sudo[i][6] = Tmp_Box[0];
                sudo[i][7] = Tmp_Box[1];
                sudo[i][8] = Tmp_Box[2];
            }
            for (int i = 3; i < 9; i++)//余下所有宮格由種子宮交替生成
            {
                for (int j = 0; j < 9; j++)
                {
                    if (j == 2 || j == 5 || j == 8)
                        sudo[i][j] = sudo[i - 3][j - 2];
                    else
                        sudo[i][j] = sudo[i - 3][j + 1];
                }
            }
            display(sudo);//輸出終局
            N--;
        }
    }
}

3)"-s"操作的關鍵代碼,通過dfs對雙向鏈表進行操作求得數獨終局的代碼如下:

bool dfs(const int& k)//深搜求解
{
    if (right[head] == head)//已經選夠
    {
        char s[100] = { 0 };
        char output[20];
        for (int i = 0; i<k; i++)
            s[ans[st[i]].r * 9 + ans[st[i]].c] = ans[st[i]].k + 0;//s[行*9+列]
        int count = 0;
        for (int i = 0; i < 9; i++)
        {
            int num = 0;
            output[num++] = s[count++];
            for (int j = 1; j < 9; j++)
            {
                output[num++] =  ;
                output[num++] = s[count++];
            }
            output[num] = \0;
            puts(output);
        }
        printf("\n");
        return true;
    }
    //遍歷列標元素,選一個元素最少的列(回溯率低)
    int s = oo, c = 0;
    for (int i = right[head]; i != head; i = right[i])
        if (cnt[i]<s)
        {
            s = cnt[i];
            c = i;
        }

    remove(c);//選好就移除
    //遍歷該列各“1”元素
    for (int i = down[c]; i != c; i = down[i])
    {
        st[k] = row[i];
        for (int j = right[i]; j != i; j = right[j]) // 移除與該元素同行元素的列
            remove(col[j]);
        if (dfs(k + 1))// 已選行數+1,遞歸調用
            return true;
        for (int j = left[i]; j != i; j = left[j])// 遞歸返回false,說明後續無法滿足,故恢復與該元素同行元素的列,循壞進入本列下一元素
            resume(col[j]);
    }
    resume(c);//所有後續都無法滿足,恢復

    return false;
}

六、測試

由於單元測試用軟件實現的編程實在是超出我能力範圍,我只能采用手動設計測試用例的方法去進行測試。

1)對於“-c”

由於種子宮這一方法的先天優勢,如果算法編寫正確,不可能存在重復的情況,重復性可以不作檢驗。

對於輸入,設計了1)非法參數 “-a 100” 2)非法字符 "-c abc"3)越界"-c 9999999" 三種測試用例,均輸出了輸入異常的判斷。

2)對於"-s"

將"-c"的部分輸出改為0,即可設計出最多1000000個數獨題目的用例。

此外還設計了一些無解的數獨或格式不對(3X3宮格內有重復數字)的數獨進行測試,均輸出回車表示無解。

看起來這測試結果令人滿意,希望可以通過老師的正確性測試。


心得體會

做完了個人項目,可以說是筋疲力盡。因為要較好的完成這個項目,做出一個高效的程序,自己需要自學的東西是在是太多太多了(最後完成的實際時間比預計翻了一番),對平時只關心課堂教學內容,自學動手能力不高,又想全力完成好這個項目的我,真是一個巨大的挑戰。即使我在小學期有充分的時間做這個項目,我都會感覺掉一層皮,更何況在正規的學期內從這麽多科目中抽出大量時間去完成這一個項目,掉了簡直是N層皮。在開始的要求中,能用的C++,C#我真是一個都沒學過,便盼著老師更改題目,可好歹加了C語言吧,經過一番學習調查,發現用C語言較好完成這個項目的難度,不亞於速成C++然後用它來實現這個項目。最後用將近一個月的時間加上汗水和淚水,完成了這個對我來說是巨大挑戰的項目。

總而言之,經過這次項目的實踐,我精神屬性上的提高,要大於技術屬性上的提高。一開始面對著別班上著水水的課,自己卻要做這麽難一個項目的情形,心裏是十分十分十分抵觸的。但自從老師在課上對自己的教學方法與教學目的做了一場慷慨激昂的演講,我的腦海裏浮現出這個項目時,竟多了老師一張掛著鼓勵微笑的臉。面對這個項目,我再怎麽感嘆自己水平不足,再怎麽煩惱毫無頭緒,一想起老師的鼓勵和笑容,我就又有了堅持下去的動力。如今慘痛的一個月過去了,寫下這項目的總結,真有一種說不出來的感慨萬千。盡管限於能力和時間有些要求比如單元測試和GUI,我盡最大努力也沒法按時交出一個較好的作品;還有些部分因為自學的不系統不深入淺嘗輒止釀成大禍,比如以為commit之後還可以改,釀成了commit都是“update+文件名”的慘案。但日後的實際工作中編寫項目不也是一樣嗎?盡善盡美是可遇而不可求的,但為此認真努力付出過,也沒什麽好後悔了。

最後感謝老師,讓還在大二,幾乎還是一張白紙的我超前體驗了一把自學做項目的快感。

#2018BIT軟件工程基礎#個人項目:數獨