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

用户头像
一直AC一直爽
关注
发布于: 2020 年 07 月 20 日
手把手教你写数独计算器(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从第一个未知数开始,至上而下,从左到右依次枚举每种可能解。

代码如下:



#include <iostream>
#include <fstream>
#include <set>
#include <vector>
using namespace std;
//#define DEBUG
vector<vector<int> > Sudo;
void PrintSudo()
{
for (int i=0; i<4; i++)
{
for (int j=0; j<4; j++)
{
cout << Sudo[i][j] << " ";
}
cout << endl;
}
}
bool DFS(int X, int Y)
{
if (Y >= 4)
{
return true;
}
if (X >= 4)
{
return DFS(0, Y + 1);
}
if (Sudo[Y][X] != 0)
{
return DFS(X + 1, Y);
}
set<int> HaveExist;
int i, j;
for (i=0; i<4; i++)
{
if (Sudo[Y][i] != 0)
{
HaveExist.insert(Sudo[Y][i]); //同行中已存在的数
}
if (Sudo[i][X] != 0)
{
HaveExist.insert(Sudo[i][X]); //同列中已存在的数
}
}
for (i=Y/2*2; i<Y/2*2 + 2; i++)
{
for (j=X/2*2; j<X/2*2 + 2; j++)
{
if (Sudo[i][j] != 0)
{
HaveExist.insert(Sudo[i][j]);
}
}
}
for (i=1; i<=4; i++)
{
if (HaveExist.find(i) == HaveExist.end()) //数字i在当前数独还未存在,是候选数
{
Sudo[Y][X] = i;
#ifdef DEBUG
cout << "X=" << X << ", Y=" << Y << endl;
cout << "已存在的数:";
for (set<int>::iterator it=HaveExist.begin(); it!=HaveExist.end(); it++)
{
cout << *it << " ";
}
cout << endl;
cout << "将Sudo[" << Y << "][" << X << "]设置成" << i << endl;
PrintSudo();
#endif
if (DFS(X+1, Y))
{
return true;
}
}
}
Sudo[Y][X] = 0;
return false;
}
int main()
{
ifstream cin("input.txt");
for (int i=0; i<4; i++)
{
vector<int> vecTmp;
for (int j=0; j<4; j++)
{
int nTmp;
cin >> nTmp;
vecTmp.push_back(nTmp);
}
Sudo.push_back(vecTmp);
vecTmp.clear();
}
if(!DFS(0, 0))
{
cout << "输入数据有误" << endl;
}
for (int i=0; i<4; i++)
{
for (int j=0; j<4; j++)
{
cout << Sudo[i][j] << " ";
}
cout << endl;
}
while (true)
{
}
return 0;
}



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

看代码



#include <iostream>
#include <fstream>
#include <set>
#include <vector>
using namespace std;
//#define DEBUG
vector<vector<int> > Sudo;
void PrintSudo()
{
for (int i=0; i<9; i++)
{
for (int j=0; j<9; j++)
{
cout << Sudo[i][j] << " ";
}
cout << endl;
}
}
bool DFS(int X, int Y)
{
if (Y >= 9)
{
return true;
}
if (X >= 9)
{
return DFS(0, Y + 1);
}
if (Sudo[Y][X] != 0)
{
return DFS(X + 1, Y);
}
set<int> HaveExist;
int i, j;
for (i=0; i<9; i++)
{
if (Sudo[Y][i] != 0)
{
HaveExist.insert(Sudo[Y][i]); //同行中已存在的数
}
if (Sudo[i][X] != 0)
{
HaveExist.insert(Sudo[i][X]); //同列中已存在的数
}
}
for (i=Y/3*3; i<Y/3*3 + 3; i++)
{
for (j=X/3*3; j<X/3*3 + 3; j++)
{
if (Sudo[i][j] != 0)
{
HaveExist.insert(Sudo[i][j]);
}
}
}
for (i=1; i<=9; i++)
{
if (HaveExist.find(i) == HaveExist.end()) //数字i在当前数独还未存在,是候选数
{
Sudo[Y][X] = i;
#ifdef DEBUG
cout << "X=" << X << ", Y=" << Y << endl;
cout << "已存在的数:";
for (set<int>::iterator it=HaveExist.begin(); it!=HaveExist.end(); it++)
{
cout << *it << " ";
}
cout << endl;
cout << "将Sudo[" << Y << "][" << X << "]设置成" << i << endl;
PrintSudo();
#endif
if (DFS(X+1, Y))
{
return true;
}
}
}
Sudo[Y][X] = 0;
return false;
}
int main()
{
ifstream cin("input.txt");
for (int i=0; i<9; i++)
{
vector<int> vecTmp;
for (int j=0; j<9; j++)
{
int nTmp;
cin >> nTmp;
vecTmp.push_back(nTmp);
}
Sudo.push_back(vecTmp);
vecTmp.clear();
}
if(!DFS(0, 0))
{
cout << "输入数据有误" << endl;
}
for (int i=0; i<9; i++)
{
for (int j=0; j<9; j++)
{
cout << Sudo[i][j] << " ";
}
cout << endl;
}
while (true)
{
}
return 0;
}



好,用上面提到的,在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,求得最终可行解。

代码如下:



#include <iostream>
#include <fstream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;
const int SUDOSIZE = 9;
//#define DEBUG
typedef vector<vector<list<int> > > SudoPoss_t;
SudoPoss_t g_SudoPoss; //数独每个位置可选择数字集合数组
inline int IntSqrt(int n);
//初始化可能性表
//一开始每个格子都有填1-9的可能
void Init();
void ShowGridPossiNums(SudoPoss_t SudoPoss, const int X, const int Y);
bool AssumeOneValue(SudoPoss_t &SudoPoss, const int& X, const int& Y, const int& Value);
bool IsOnlyPossibleInSameRow(int X, int Y, int PossiVal);
bool IsOnlyPossibleInSameCol(int X, int Y, int PossiVal);
bool IsOnlyPossibleInSameArea(int X, int Y, int PossiVal);
//例如
//0 2 0 0 1 0 0 0 0
//0 0 0 0 0 4 0 8 3
//0 0 0 0 0 5 0 7 X
//0 0 0 0 0 8 0 0 0
//7 0 0 0 0 3 0 0 0
//0 9 0 0 0 0 1 0 0
//8 0 0 0 0 0 0 0 0
//0 0 0 0 2 0 6 0 0
//0 0 0 0 9 0 0 4 0
//只有X可以为1,因为该区域内只有X有为1的可能性,所以可以确定X为1,排除此处其他可能性
void ConfirmOnlyPossible();
void ReadInput();
void ShowAllPossNums();
bool DFS(SudoPoss_t SudoPoss, int X, int Y);
int main()
{
Init();
try
{
ReadInput();
}
catch (int e)
{
cout << "输入数独数据错误" << endl;
return -1;
}
ConfirmOnlyPossible();
if(!DFS(g_SudoPoss, 0, 0))
{
cout << "输入数据有误" << endl;
}
for (int i=0; i<SUDOSIZE; i++)
{
for (int j=0; j<SUDOSIZE; j++)
{
cout << *g_SudoPoss[i][j].begin() << " ";
}
cout << endl;
}
while(true)
{
}
return 0;
}
inline int IntSqrt(int n)
{
if (1 == n || 0 == n)
{
return n;
}
for (int i = n / 2; i>=1; i--)
{
if (i * i == n)
{
return i;
}
}
return -1;
}
//初始化可能性表
//一开始每个格子都有填1-9的可能
void Init()
{
//set<int> setTmp;
list<int> listTmp;
for (int i=1; i<=9; i++)
{
listTmp.push_back(i);
}
vector<list<int> > vecTmp;
for (int j=0; j<SUDOSIZE; j++)
{
vecTmp.push_back(listTmp);
}
for (int i=0; i<SUDOSIZE; i++)
{
g_SudoPoss.push_back(vecTmp);
}
}
//显示第(X,Y)位置格子可供选择的数字
void ShowGridPossiNums(SudoPoss_t SudoPoss, const int X, const int Y)
{
cout << "SudoPoss[" << Y << "][" << X << "] :" ;
for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); it++)
{
cout << " " << *it ;
}
cout << " size() = " << SudoPoss[Y][X].size();
cout << endl;
}
//假设(X,Y)位置处确定为值Value,删除其他位置的Value值的可能情况,从而进行剪枝
bool AssumeOneValue(SudoPoss_t &SudoPoss, const int& X, const int& Y, const int& Value)
{
if (SudoPoss[Y][X].size() == 0)
{
return false;
}
//如果某个位置是已知数,则将该位置其他可能数删除
for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); )
{
if (*it != Value)
{
SudoPoss[Y][X].erase(it);
it=SudoPoss[Y][X].begin();
continue;
}
it++;
}
//在同行中其他格子中删除该已知数
for (int i=0; i<SUDOSIZE; i++)
{
if (i == X)
{
continue;
}
list<int>::iterator it = find(SudoPoss[Y][i].begin(), SudoPoss[Y][i].end(), Value);
if (it != SudoPoss[Y][i].end())
{
SudoPoss[Y][i].erase(it);
//如果某格没有任何可能的情况 则表示该推测错误
if (0 == SudoPoss[Y][i].size())
{
return false;
}
//通过剪枝使某一个只有一种可能的情况,则针对该格继续剪枝
else if (1 == SudoPoss[Y][i].size())
{
if (!AssumeOneValue(SudoPoss, i, Y, *SudoPoss[Y][i].begin()))
{
return false;
}
}
}
}
//在同列中其他格子删除该已知数
for (int i=0; i<SUDOSIZE; i++)
{
if (i == Y)
{
continue;
}
list<int>::iterator it = find(SudoPoss[i][X].begin(), SudoPoss[i][X].end(), Value);
if (it != SudoPoss[i][X].end())
{
SudoPoss[i][X].erase(it);
if (0 == SudoPoss[i][X].size())
{
return false;
}
else if (1 == SudoPoss[i][X].size())
{
if (!AssumeOneValue(SudoPoss, X, i, *SudoPoss[i][X].begin()))
{
return false;
}
}
}
}
//在同区域中其他格子删除该已知数
for (int i=Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); i<Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); i++)
{
for (int j=X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); j<X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); j++)
{
if (i == Y && j == X)
{
continue;
}
list<int>::iterator it = find(SudoPoss[i][j].begin(), SudoPoss[i][j].end(), Value);
if (it != SudoPoss[i][j].end())
{
SudoPoss[i][j].erase(it);
if (0 == SudoPoss[i][j].size())
{
return false;
}
else if (1 == SudoPoss[i][j].size())
{
if (!AssumeOneValue(SudoPoss, j, i, *SudoPoss[i][j].begin()))
{
return false;
}
}
}
}
}
return true;
}
bool IsOnlyPossibleInSameRow(int X, int Y, int PossiVal)
{
for (int i=0; i<SUDOSIZE; i++)
{
if (i == X)
{
continue;
}
if(find(g_SudoPoss[Y][i].begin(), g_SudoPoss[Y][i].end(), PossiVal) != g_SudoPoss[Y][i].end())
{
return false;
}
}
return true;
}
bool IsOnlyPossibleInSameCol(int X, int Y, int PossiVal)
{
for (int i=0; i<SUDOSIZE; i++)
{
if (i == Y)
{
continue;
}
if(find(g_SudoPoss[i][X].begin(), g_SudoPoss[i][X].end(), PossiVal) != g_SudoPoss[i][X].end())
{
return false;
}
}
return true;
}
bool IsOnlyPossibleInSameArea(int X, int Y, int PossiVal)
{
//在同区域中其他格子删除该已知数
for (int i=Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); i<Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); i++)
{
for (int j=X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); j<X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); j++)
{
if (i == Y && j == X)
{
continue;
}
if(find(g_SudoPoss[i][j].begin(), g_SudoPoss[i][j].end(), PossiVal) != g_SudoPoss[i][j].end())
{
return false;
}
}
}
return true;
}
//例如
//0 2 0 0 1 0 0 0 0
//0 0 0 0 0 4 0 8 3
//0 0 0 0 0 5 0 7 X
//0 0 0 0 0 8 0 0 0
//7 0 0 0 0 3 0 0 0
//0 9 0 0 0 0 1 0 0
//8 0 0 0 0 0 0 0 0
//0 0 0 0 2 0 6 0 0
//0 0 0 0 9 0 0 4 0
//只有X可以为1,因为该区域内只有X有为1的可能性,所以可以确定X为1,排除此处其他可能性
void ConfirmOnlyPossible()
{
for (int i=0; i<SUDOSIZE; i++)
{
for (int j=0; j<SUDOSIZE; j++)
{
if (g_SudoPoss[i][j].size() == 1)
{
continue;
}
for (list<int>::iterator it=g_SudoPoss[i][j].begin(); it!=g_SudoPoss[i][j].end(); it++)
{
if (IsOnlyPossibleInSameArea(j, i, *it)
|| IsOnlyPossibleInSameCol(j, i, *it)
|| IsOnlyPossibleInSameRow(j, i, *it))
{
// cout << "确定Sudo[" << i << "][" << j << "]为" << *it << endl;
AssumeOneValue(g_SudoPoss, j, i, *it);
//重新开始循环
i = -1;
j = SUDOSIZE;
break;
}
}
}
}
}
void ReadInput()
{
ifstream cin("input.txt");
for (int i=0; i<SUDOSIZE; i++)
{
for (int j=0; j<SUDOSIZE; j++)
{
int nTmp;
cin >> nTmp;
if (0 == nTmp)
{
continue;
}
if (!AssumeOneValue(g_SudoPoss, j, i, nTmp))
{
throw 0;
}
}
}
cin.close();
}
void ShowAllPossNums()
{
for (int i=0; i<SUDOSIZE; i++)
{
for (int j=0; j<SUDOSIZE; j++)
{
ShowGridPossiNums(g_SudoPoss, j, i);
}
}
}
bool DFS(SudoPoss_t SudoPoss, int X, int Y)
{
if (Y >= SUDOSIZE)
{
g_SudoPoss = SudoPoss;
return true;
}
if (X >= SUDOSIZE)
{
return DFS(SudoPoss, 0, Y + 1);
}
if (SudoPoss[Y][X].size() == 1)
{
return DFS(SudoPoss, X + 1, Y);
}
for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); it++)
{
SudoPoss_t TmpSudoPoss = SudoPoss;
if (!AssumeOneValue(TmpSudoPoss,X, Y, *it))
{
continue;
}
if (!DFS(TmpSudoPoss, X + 1, Y))
{
continue;
}
else
{
return true;
}
}
return false;
}



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

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



发布于: 2020 年 07 月 20 日 阅读数: 29
用户头像

一直AC一直爽

关注

我就是我,不一样的花火。 2020.07.19 加入

最近专注刷题。

评论

发布
暂无评论
手把手教你写数独计算器(1)