• 手把手教你写数独计算器(1)


        最近在一个数独网站玩数独游戏,网站地址为:http://www.sudokufans.org.cn/。

        由于自己数独能力不是特别强,解题比较慢,但是自己是程序猿,所以,我想,自己写个数独计算器吧,让电脑帮我去算得了。

        由于我是C程序猿,所以第一步要做的是,先不管界面,做一个黑底白字的win32控制台应用程序,用于验证自己的算法。

        好了,开工,所以做一个简单的儿童4阶数独,如图:

       

    我程序是这样使用的,首先,在程序的同目录下放一个input.txt文件用于输入,其中,未知数用0表示,每个数之间一个空格,例如上图的input.txt文件的内容为:

    4 0 3 0
    3 0 0 0
    0 0 0 0
    0 0 0 1

    然后点击根据我代码生成的程序,就得到输出结果。

         由于数据比较简单,比较才是4X4的数独,所以也没做什么优化,就是通过数据结构里图论中的DFS从第一个未知数开始,至上而下,从左到右依次枚举每种可能解。

    代码如下:

      1 #include <iostream>
      2 #include <fstream>
      3 #include <set>
      4 #include <vector>
      5 using namespace std;
      6 
      7 //#define DEBUG
      8 
      9 vector<vector<int> > Sudo;
     10 
     11 void PrintSudo()
     12 {
     13     for (int i=0; i<4; i++)
     14     {
     15         for (int j=0; j<4; j++)
     16         {
     17             cout << Sudo[i][j] << " ";
     18         }
     19         cout << endl;
     20     }
     21 }
     22 
     23 bool DFS(int X, int Y)
     24 {
     25     if (Y >= 4)
     26     {
     27         return true;
     28     }
     29 
     30     if (X >= 4)
     31     {
     32         return DFS(0, Y + 1);
     33     }
     34 
     35     if (Sudo[Y][X] != 0)
     36     {
     37         return DFS(X + 1, Y);
     38     }
     39 
     40     set<int> HaveExist;
     41     int i, j;
     42 
     43     for (i=0; i<4; i++)
     44     {
     45         if (Sudo[Y][i] != 0)
     46         {
     47             HaveExist.insert(Sudo[Y][i]); //同行中已存在的数
     48         }
     49 
     50         if (Sudo[i][X] != 0)
     51         {
     52             HaveExist.insert(Sudo[i][X]); //同列中已存在的数
     53         }
     54     }
     55 
     56     for (i=Y/2*2; i<Y/2*2 + 2; i++)
     57     {
     58         for (j=X/2*2; j<X/2*2 + 2; j++)
     59         {
     60             if (Sudo[i][j] != 0)
     61             {
     62                 HaveExist.insert(Sudo[i][j]);
     63             }
     64         }
     65     }
     66 
     67 
     68     for (i=1; i<=4; i++)
     69     {
     70         if (HaveExist.find(i) == HaveExist.end()) //数字i在当前数独还未存在,是候选数
     71         {
     72             Sudo[Y][X] = i;
     73 #ifdef DEBUG
     74             cout << "X=" << X << ", Y=" << Y << endl;
     75             cout << "已存在的数:";
     76 
     77             for (set<int>::iterator it=HaveExist.begin(); it!=HaveExist.end(); it++)
     78             {
     79                 cout << *it << " ";
     80             }
     81             cout << endl;
     82             cout << "将Sudo[" << Y << "][" << X << "]设置成" << i << endl;
     83             PrintSudo();
     84 #endif
     85             if (DFS(X+1, Y))
     86             {
     87                 return true;
     88             }
     89         }
     90     }
     91 
     92     Sudo[Y][X] = 0;
     93     return false;
     94 }
     95 
     96 int main()
     97 {
     98     ifstream cin("input.txt");
     99 
    100 
    101 
    102     for (int i=0; i<4; i++)
    103     {
    104         vector<int> vecTmp;
    105         for (int j=0; j<4; j++)
    106         {
    107             int nTmp;
    108 
    109             cin >> nTmp;
    110             vecTmp.push_back(nTmp);
    111         }
    112         Sudo.push_back(vecTmp);
    113         vecTmp.clear();
    114     }
    115 
    116     if(!DFS(0, 0))
    117     {
    118         cout << "输入数据有误" << endl;
    119     }
    120 
    121     for (int i=0; i<4; i++)
    122     {
    123         for (int j=0; j<4; j++)
    124         {
    125             cout << Sudo[i][j] << " ";
    126         }
    127         cout << endl;
    128     }
    129 
    130     while (true)
    131     {
    132 
    133     }
    134 
    135     return 0;
    136 }

    好了,尝试了4X4的数独之后,再来尝试9X9的数独,首先,我们先简单的将之前的4改成9(当然,同区域的56和58的2改成3)看看情况会怎么样;

    看代码

      1 #include <iostream>
      2 #include <fstream>
      3 #include <set>
      4 #include <vector>
      5 using namespace std;
      6 
      7 //#define DEBUG
      8 
      9 vector<vector<int> > Sudo;
     10 
     11 void PrintSudo()
     12 {
     13     for (int i=0; i<9; i++)
     14     {
     15         for (int j=0; j<9; j++)
     16         {
     17             cout << Sudo[i][j] << " ";
     18         }
     19         cout << endl;
     20     }
     21 }
     22 
     23 bool DFS(int X, int Y)
     24 {
     25     if (Y >= 9)
     26     {
     27         return true;
     28     }
     29 
     30     if (X >= 9)
     31     {
     32         return DFS(0, Y + 1);
     33     }
     34 
     35     if (Sudo[Y][X] != 0)
     36     {
     37         return DFS(X + 1, Y);
     38     }
     39 
     40     set<int> HaveExist;
     41     int i, j;
     42 
     43     for (i=0; i<9; i++)
     44     {
     45         if (Sudo[Y][i] != 0)
     46         {
     47             HaveExist.insert(Sudo[Y][i]); //同行中已存在的数
     48         }
     49 
     50         if (Sudo[i][X] != 0)
     51         {
     52             HaveExist.insert(Sudo[i][X]); //同列中已存在的数
     53         }
     54     }
     55 
     56     for (i=Y/3*3; i<Y/3*3 + 3; i++)
     57     {
     58         for (j=X/3*3; j<X/3*3 + 3; j++)
     59         {
     60             if (Sudo[i][j] != 0)
     61             {
     62                 HaveExist.insert(Sudo[i][j]);
     63             }
     64         }
     65     }
     66 
     67 
     68     for (i=1; i<=9; i++)
     69     {
     70         if (HaveExist.find(i) == HaveExist.end()) //数字i在当前数独还未存在,是候选数
     71         {
     72             Sudo[Y][X] = i;
     73 #ifdef DEBUG
     74             cout << "X=" << X << ", Y=" << Y << endl;
     75             cout << "已存在的数:";
     76 
     77             for (set<int>::iterator it=HaveExist.begin(); it!=HaveExist.end(); it++)
     78             {
     79                 cout << *it << " ";
     80             }
     81             cout << endl;
     82             cout << "将Sudo[" << Y << "][" << X << "]设置成" << i << endl;
     83             PrintSudo();
     84 #endif
     85             if (DFS(X+1, Y))
     86             {
     87                 return true;
     88             }
     89         }
     90     }
     91 
     92     Sudo[Y][X] = 0;
     93     return false;
     94 }
     95 
     96 int main()
     97 {
     98     ifstream cin("input.txt");
     99 
    100 
    101 
    102     for (int i=0; i<9; i++)
    103     {
    104         vector<int> vecTmp;
    105         for (int j=0; j<9; j++)
    106         {
    107             int nTmp;
    108 
    109             cin >> nTmp;
    110             vecTmp.push_back(nTmp);
    111         }
    112         Sudo.push_back(vecTmp);
    113         vecTmp.clear();
    114     }
    115 
    116     if(!DFS(0, 0))
    117     {
    118         cout << "输入数据有误" << endl;
    119     }
    120 
    121     for (int i=0; i<9; i++)
    122     {
    123         for (int j=0; j<9; j++)
    124         {
    125             cout << Sudo[i][j] << " ";
    126         }
    127         cout << endl;
    128     }
    129 
    130     while (true)
    131     {
    132 
    133     }
    134 
    135     return 0;
    136 }

    好,用上面提到的,在input.txt中输入下面的数独,测试一下。

    我的能够成功,速度也还接受得了。

    好了,下面来点有难度的了。

    例如下面的数独:

    这个数独,用上面的程序计算的话,那就不是一般的慢了。

    所以,必须考虑优化算法。

    那么该怎么优化呢?我想先听听大家的看法。

    本文待续......

    首先分析下为什么上面的程序解上图中的数独时会很慢,因为前面的程序是暴力枚举所以可能的情况,直到找到可行解为止,而这个数独的已知数只有17个,而未知数却有81-17=64个,我假设平均每个格子有4个可能解,那么人品不好的话,可能要尝试4的64次方,这个数大得太恐怖了,所以必须进行剪枝。

    怎么剪枝呢?我利用的是人脑解数独的一些方法,为了方便描述,我将横排编号为A-I,竖排编号为1-9,这样左上角的坐标便是A1,右下角的坐标便是I9,我先假设每个格子都可以填1-9这九种可能的数字,然后根据已知数,不断删除每个格子的可能性数,例如上图中根据已知数,可能得到可能性表:

    接下了,就是很重要的优化步骤了,根据我们解数独方法,我们可以知道,在右上区域,只有I1有可能值为5,该区域其他格子都没有成为5的可能,所以I1必为5(玩过数独的应该很容易理解)。确定I1为5后,又可以删除同行、同列其他格子5的可能情况:

    同理,可以确定E5=6,C9=6,等等,因此程序的流程已经比较清晰,由于不会画流程图,所以只能先用文字描述程序流程,求会画流程图的大神提供帮助。

    1.初始化数独的可能性表,让每个格子都有1-9这9种可能;

    2.输入已知数,每输入一个已知数,便确定了一个值;

    3.根据该确定值删除同行、同列、同区域中其他格子的该确定值的可能值;

    4.在删除格子可能值的时,判断删除完后,该格子是否只剩唯一的可能值了,如果是,则说明又确定一个格子的值,执行步骤3;

    5.输入完已知数后,判断每个格子包含的可能值是该行或该列或该区域其他格子的可能性表中没有的,则可确定该格的值便是这个特有的可能值,执行步骤3.

    6.对于剩下的未知数,根据其可能性表做DFS,求得最终可行解。

    代码如下:

      1 #include <iostream>
      2 #include <fstream>
      3 #include <list>
      4 #include <vector>
      5 #include <algorithm>
      6 using namespace std;
      7 
      8 const int SUDOSIZE = 9;
      9 
     10 //#define DEBUG
     11 typedef vector<vector<list<int> > > SudoPoss_t;
     12 SudoPoss_t g_SudoPoss; //数独每个位置可选择数字集合数组
     13 
     14 inline int IntSqrt(int n);
     15 //初始化可能性表
     16 //一开始每个格子都有填1-9的可能
     17 void Init();
     18 void ShowGridPossiNums(SudoPoss_t SudoPoss, const int X, const int Y);
     19 bool AssumeOneValue(SudoPoss_t &SudoPoss, const int& X, const int& Y, const int& Value);
     20 bool IsOnlyPossibleInSameRow(int X, int Y, int PossiVal);
     21 bool IsOnlyPossibleInSameCol(int X, int Y, int PossiVal);
     22 bool IsOnlyPossibleInSameArea(int X, int Y, int PossiVal);
     23 //例如
     24 //0 2 0 0 1 0 0 0 0
     25 //0 0 0 0 0 4 0 8 3
     26 //0 0 0 0 0 5 0 7 X
     27 //0 0 0 0 0 8 0 0 0
     28 //7 0 0 0 0 3 0 0 0
     29 //0 9 0 0 0 0 1 0 0
     30 //8 0 0 0 0 0 0 0 0
     31 //0 0 0 0 2 0 6 0 0
     32 //0 0 0 0 9 0 0 4 0
     33 //只有X可以为1,因为该区域内只有X有为1的可能性,所以可以确定X为1,排除此处其他可能性
     34 void ConfirmOnlyPossible();
     35 void ReadInput();
     36 void ShowAllPossNums();
     37 bool DFS(SudoPoss_t SudoPoss, int X, int Y);
     38 
     39 int main()
     40 {
     41 
     42 
     43     Init();
     44 
     45     try
     46     {
     47         ReadInput();
     48     }
     49     catch (int e)
     50     {
     51         cout << "输入数独数据错误" << endl;
     52         return -1;
     53     }
     54 
     55     ConfirmOnlyPossible();
     56 
     57     if(!DFS(g_SudoPoss, 0, 0))
     58     {
     59         cout << "输入数据有误" << endl;
     60     }
     61 
     62     for (int i=0; i<SUDOSIZE; i++)
     63     {
     64         for (int j=0; j<SUDOSIZE; j++)
     65         {
     66             cout << *g_SudoPoss[i][j].begin() << " ";
     67         }
     68         cout << endl;
     69     }
     70 
     71     while(true)
     72     {
     73 
     74     }
     75 
     76     return 0;
     77 }
     78 
     79 inline int IntSqrt(int n)
     80 {
     81     if (1 == n || 0 == n)
     82     {
     83         return n;
     84     }
     85 
     86     for (int i = n / 2; i>=1; i--)
     87     {
     88         if (i * i == n)
     89         {
     90             return i;
     91         }
     92     }
     93     return -1;
     94 }
     95 
     96 
     97 //初始化可能性表
     98 //一开始每个格子都有填1-9的可能
     99 void Init()
    100 {
    101     //set<int> setTmp;
    102     list<int> listTmp;
    103 
    104     for (int i=1; i<=9; i++)
    105     {
    106         listTmp.push_back(i);
    107     }
    108 
    109     vector<list<int> > vecTmp;
    110     for (int j=0; j<SUDOSIZE; j++)
    111     {
    112         vecTmp.push_back(listTmp);
    113     }
    114 
    115     for (int i=0; i<SUDOSIZE; i++)
    116     {
    117         g_SudoPoss.push_back(vecTmp);
    118     }
    119 }
    120 
    121 //显示第(X,Y)位置格子可供选择的数字
    122 void ShowGridPossiNums(SudoPoss_t SudoPoss, const int X, const int Y)
    123 {
    124     cout << "SudoPoss[" << Y << "][" << X << "] :" ;
    125     for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); it++)
    126     {
    127         cout << " " << *it ;
    128     }
    129     cout << " size() = " << SudoPoss[Y][X].size();
    130     cout << endl;
    131 }
    132 
    133 //假设(X,Y)位置处确定为值Value,删除其他位置的Value值的可能情况,从而进行剪枝
    134 bool AssumeOneValue(SudoPoss_t &SudoPoss, const int& X, const int& Y, const int& Value)
    135 {
    136     if (SudoPoss[Y][X].size() == 0)
    137     {
    138         return false;
    139     }
    140 
    141     //如果某个位置是已知数,则将该位置其他可能数删除
    142     for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); )
    143     {
    144         if (*it != Value)
    145         {
    146             SudoPoss[Y][X].erase(it);
    147             it=SudoPoss[Y][X].begin();
    148             continue;
    149         }
    150 
    151         it++;
    152     }
    153 
    154     //在同行中其他格子中删除该已知数
    155     for (int i=0; i<SUDOSIZE; i++)
    156     {
    157         if (i == X)
    158         {
    159             continue; 
    160         }
    161 
    162         list<int>::iterator it = find(SudoPoss[Y][i].begin(), SudoPoss[Y][i].end(), Value);
    163         if (it != SudoPoss[Y][i].end())
    164         {
    165             SudoPoss[Y][i].erase(it);
    166 
    167             //如果某格没有任何可能的情况  则表示该推测错误
    168             if (0 == SudoPoss[Y][i].size())
    169             {
    170                 return false;
    171             }    
    172             //通过剪枝使某一个只有一种可能的情况,则针对该格继续剪枝
    173             else if (1 == SudoPoss[Y][i].size())
    174             {
    175                 if (!AssumeOneValue(SudoPoss, i, Y, *SudoPoss[Y][i].begin()))
    176                 {
    177                     return false;
    178                 }
    179             }
    180         } 
    181     }
    182 
    183     //在同列中其他格子删除该已知数
    184     for (int i=0; i<SUDOSIZE; i++)
    185     {
    186         if (i == Y)
    187         {
    188             continue; 
    189         }
    190 
    191         list<int>::iterator it = find(SudoPoss[i][X].begin(), SudoPoss[i][X].end(), Value);
    192         if (it != SudoPoss[i][X].end())
    193         {
    194             SudoPoss[i][X].erase(it);
    195 
    196             if (0 == SudoPoss[i][X].size())
    197             {
    198                 return false;
    199             }
    200             else if (1 == SudoPoss[i][X].size())
    201             {
    202                 if (!AssumeOneValue(SudoPoss, X, i, *SudoPoss[i][X].begin()))
    203                 {
    204                     return false;
    205                 }
    206             }
    207         } 
    208     }
    209 
    210     //在同区域中其他格子删除该已知数
    211     for (int i=Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); i<Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); i++)
    212     {
    213         for (int j=X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); j<X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); j++)
    214         {
    215             if (i == Y && j == X)
    216             {
    217                 continue;
    218             }
    219 
    220             list<int>::iterator it = find(SudoPoss[i][j].begin(), SudoPoss[i][j].end(), Value);
    221             if (it != SudoPoss[i][j].end())
    222             {
    223                 SudoPoss[i][j].erase(it);
    224 
    225                 if (0 == SudoPoss[i][j].size())
    226                 {
    227                     return false;
    228                 }
    229                 else if (1 == SudoPoss[i][j].size())
    230                 {
    231                     if (!AssumeOneValue(SudoPoss, j, i, *SudoPoss[i][j].begin()))
    232                     {
    233                         return false;
    234                     }
    235                 }
    236             } 
    237         }
    238     }
    239     return true;
    240 }
    241 
    242 bool IsOnlyPossibleInSameRow(int X, int Y, int PossiVal)
    243 {
    244     for (int i=0; i<SUDOSIZE; i++)
    245     {
    246         if (i == X)
    247         {
    248             continue;
    249         }
    250 
    251         if(find(g_SudoPoss[Y][i].begin(), g_SudoPoss[Y][i].end(), PossiVal) != g_SudoPoss[Y][i].end())
    252         {
    253             return false;
    254         }
    255     }
    256     return true;
    257 }
    258 
    259 bool IsOnlyPossibleInSameCol(int X, int Y, int PossiVal)
    260 {
    261     for (int i=0; i<SUDOSIZE; i++)
    262     {
    263         if (i == Y)
    264         {
    265             continue;
    266         }
    267 
    268         if(find(g_SudoPoss[i][X].begin(), g_SudoPoss[i][X].end(), PossiVal) != g_SudoPoss[i][X].end())
    269         {
    270             return false;
    271         }
    272     }
    273     return true;
    274 }
    275 
    276 bool IsOnlyPossibleInSameArea(int X, int Y, int PossiVal)
    277 {
    278     //在同区域中其他格子删除该已知数
    279     for (int i=Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); i<Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); i++)
    280     {
    281         for (int j=X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); j<X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); j++)
    282         {
    283             if (i == Y && j == X)
    284             {
    285                 continue;
    286             }
    287 
    288             if(find(g_SudoPoss[i][j].begin(), g_SudoPoss[i][j].end(), PossiVal) != g_SudoPoss[i][j].end())
    289             {
    290                 return false;
    291             }
    292         }
    293     }
    294     return true;
    295 }
    296 
    297 //例如
    298 //0 2 0 0 1 0 0 0 0
    299 //0 0 0 0 0 4 0 8 3
    300 //0 0 0 0 0 5 0 7 X
    301 //0 0 0 0 0 8 0 0 0
    302 //7 0 0 0 0 3 0 0 0
    303 //0 9 0 0 0 0 1 0 0
    304 //8 0 0 0 0 0 0 0 0
    305 //0 0 0 0 2 0 6 0 0
    306 //0 0 0 0 9 0 0 4 0
    307 //只有X可以为1,因为该区域内只有X有为1的可能性,所以可以确定X为1,排除此处其他可能性
    308 void ConfirmOnlyPossible()
    309 {
    310     for (int i=0; i<SUDOSIZE; i++)
    311     {
    312         for (int j=0; j<SUDOSIZE; j++)
    313         {
    314             if (g_SudoPoss[i][j].size() == 1)
    315             {
    316                 continue;
    317             }
    318 
    319             for (list<int>::iterator it=g_SudoPoss[i][j].begin(); it!=g_SudoPoss[i][j].end(); it++)
    320             {
    321                 if (IsOnlyPossibleInSameArea(j, i, *it)
    322                     || IsOnlyPossibleInSameCol(j, i, *it)
    323                     || IsOnlyPossibleInSameRow(j, i, *it))
    324                 {
    325                     //    cout << "确定Sudo[" << i << "][" << j << "]为" << *it << endl;
    326                     AssumeOneValue(g_SudoPoss, j, i, *it);
    327                     //重新开始循环
    328                     i = -1;
    329                     j = SUDOSIZE;
    330                     break;
    331                 }
    332             }
    333         }
    334     }
    335 }
    336 
    337 void ReadInput()
    338 {
    339     ifstream cin("input.txt");
    340 
    341     for (int i=0; i<SUDOSIZE; i++)
    342     {
    343         for (int j=0; j<SUDOSIZE; j++)
    344         {
    345             int nTmp;
    346 
    347             cin >> nTmp;
    348 
    349             if (0 == nTmp)
    350             {
    351                 continue;
    352             }
    353 
    354             if (!AssumeOneValue(g_SudoPoss, j, i, nTmp))
    355             {
    356                 throw 0;
    357             }
    358         }
    359     }
    360     cin.close();
    361 }
    362 void ShowAllPossNums()
    363 {
    364     for (int i=0; i<SUDOSIZE; i++)
    365     {
    366         for (int j=0; j<SUDOSIZE; j++)
    367         {
    368             ShowGridPossiNums(g_SudoPoss, j, i);
    369         }
    370     }
    371 }
    372 bool DFS(SudoPoss_t SudoPoss, int X, int Y)
    373 {
    374     if (Y >= SUDOSIZE)
    375     {
    376         g_SudoPoss = SudoPoss;
    377         return true;
    378     }
    379 
    380     if (X >= SUDOSIZE)
    381     {
    382         return DFS(SudoPoss, 0, Y + 1);
    383     }
    384 
    385     if (SudoPoss[Y][X].size() == 1)
    386     {
    387         return DFS(SudoPoss, X + 1, Y);
    388     }
    389 
    390     for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); it++)
    391     {
    392         SudoPoss_t TmpSudoPoss = SudoPoss;
    393 
    394         if (!AssumeOneValue(TmpSudoPoss,X, Y, *it))
    395         {
    396             continue;
    397         }
    398         if (!DFS(TmpSudoPoss, X + 1, Y))
    399         {
    400             continue;
    401         }
    402         else
    403         {
    404             return true;
    405         }
    406     }
    407     return false;
    408 }

    底层算法暂时到这里,欢迎大家继续提出优化建议,至于前端的界面,我目前也还正在学习win32GUI编程,等掌握好后,再做前端界面。

    所以,本文待续......

  • 相关阅读:
    Unix Programming :文件IO
    Git 小记
    Effective C++ Placement new
    Effective C++ 避免数组多态
    系列文章:云原生Kubernetes日志落地方案
    阿里巴巴大数据产品最新特性介绍--机器学习PAI
    Apache Flink 1.9.0版本新功能介绍
    Flink Checkpoint 问题排查实用指南
    进击的 Java ,云原生时代的蜕变
    8 分钟入门 K8s | 详解容器基本概念
  • 原文地址:https://www.cnblogs.com/BTMaster/p/2892787.html
Copyright © 2020-2023  润新知