QT编写的数独求解软件、一个数独快速高效的求解算法

时间:2022-05-19 17:18:50

前言

本算法可以快速高效无误的求解数独矩阵,比较完善,不会因用户输入的错误陷入无意义的循环。
可在github上获得算法的源代码算法的源码
本人已用QT编写出软件,软件在此,准确无误高效且方便,完全可以用这个软件去检验某些网站/软件上生成的数独游戏是否有唯一解。
如需软件QT窗口部分的源代码可以私信本人

1.基本工作

数独是9×9的矩阵,为了代码清晰、使用方便,声明一个类进行封装

        typedef std::vector<int> vint;//头文件vector
typedef std::vector<vint> vvint;//二维数组
class SudokuCrack
{
public:
SudokuCrack(vvint &);
int beginCrack();//开始求解
private:
vvint vvnum;//0表示没有数字,1-9为正常数字
static vvint result;//用于保存结果,静态成员变量,需要在cpp文件中初始化
};

现在这个类简单的一个成员变量,一个构造函数还有一个接口函数。构造函数直接接受一个二维数组。

2.求解函数

我们的基本思路是按照人做数独题的方式去求解。
* 选择最优的点
我们在做数独题的时候会大概看一遍整个9×9矩阵,选择一个周围的数字比较多的点来进行游戏,周围的数字越多,这个点上可排除的数字就越多,可能的情况是最少的。
我们就按照人的思路来,先遍历9×9矩阵,选择一个缺少的数字的数量最少的点,当做最优点,“猜”这个点上的数字。
定义一个选点函数:

        //c++11,需要头文件random,windows可能还需要头文件ctime
static std::default_random_engine e(time(0));
//0和1,用于当最优点有好几个时进行随机选择
static std::uniform_int_distribution<int> u(0,1);
//该函数返回值为int,不是bool,这样我们可以发生错误时返回一个错误代码
int SudokuCrack::selectBetter()
{
vint min_locate ( 2,0 );//保存选择的点的坐标
int min_lack=10;//可能的最大值是9,这个这是初始值,用于被代替
int lack_num = 10;
for( int i=0 ; i<9 ;++i )
{
for( int j=0; j<9; ++j )
{
//此处先进行判断,如果这个点上的有数字,那这个点可能的数字个数就是10
//如果这个点上没有数字,那就计算这个点上可能的数字的个数
lack_num = vvnum[i][j] ? 10 : countLackNum(i,j);
if( lack_num < min_lack )//该点是目前的最优点
{
min_lack = lack_num;
min_locate[0]=i;//保存坐标
min_locate[1]=j;
}
else if(lack_num == min_lack)//缺少的个数一样,都是最优点
{
if( u(e) )//就随机进行选择
{
min_locate[0]=i;
min_locate[1]=j;
}
}
}
}
if( min_lack == 10 )//最小的点上缺少的个数为10个,就表明所有点上都有数字了,没有下一步了,只能判断是否胜利
{
if( checkwin() )//如果胜利了
{
result=vvnum;
return 1;//返回1求值成功,不再继续计算
}
else return 0;//不然就失败
}
return attemptLocate( min_locate[0] ,min_locate[1] );//对最优点一个一个的“猜”,猜的结果返回
}

这个选择最优的点的函数用到了另外三个函数countLackNum(i,j)checkwin()attemptLocate(i,j)。我们先继续按照思路走,这些函数先放着最后再写

  • “猜”最优点上缺少的数字
    首先需要确定这个这个点上缺少哪些数字,然后对这个点上可能的数字进行一个一个的试。我们首先想到的用一个固定的1-9数组,每当存在一个数字我们就从这个数组中删去这个数字,这个数组上需要的操作只有删除。如果删除数字时可以不用检查数组中是否包含这个数字,操作会方便很多。最后决定选择set作为容器,可以简单且快速的的进行删除
    两个函数:
        typedef std::set<int> sint;//需要头文件set
int SudokuCrack::attemptLocate( int i, int j )//“猜”数字的函数
{
sint d = getLackedNumSet( i , j );//保存有该点可能的所有数字
sint::iterator it=d.begin();//迭代器,用于遍地所有数字,一个一个试
for( ; it!=d.end(); ++it )
{
vvint vvnum_new=vvnum;//新的9×9矩阵
vvnum_new[i][j]=*it;
SudokuCrack sucr_t(vvnum_new);
if( sucr_t.selectBetter() )//对新矩阵进行求解
return 1;
}
return 0;
}
sint& SudokuCrack::getLackedNumSet(int i,int j )
{
static sint d;//静态成员
sint s_temp={1,2,3,4,5,6,7,8,9};//一个1-9的数组
using std::swap;
swap(d , s_temp);//使用swap进行交换,避免复制
for(int m=0; m<9; ++m)
{
d.erase(vvnum[i][m]);//删除行上的数字,数字是0-9之间的任意值,不用进行检查
d.erase(vvnum[m][j]);//删除列上的数字
}
int m =i/3*3, n=j/3*3;
for( int q=0; q<3; ++q )
{
for( int p=0 ;p<3; ++p )
{
d.erase( vvnum[m+q][n+p] );//删除3×3方格上的数字
}
}
return d;
}
  • 缺少的函数
    至此主要矛盾已解决,上述代码将递归求解数独矩阵。
    现在把上面缺少的函数countLackNum(i,j)checkwin()补充进去就可以运行了
        int SudokuCrack::countLackNum(int i ,int j)
{
return getLackedNumSet(i,j).size();//调用已有函数
}
//用了set确实很方便,插入的时候也不会重复插入
bool SudokuCrack::checkwin()
{
//sint见上一节的typedef
const sint ss={1,2,3,4,5,6,7,8,9};//固定的1-9,用于检查是否和它相等
for( int i=0 ;i<9; ++i )
{
sint a,b;
for(int j=0; j<9; ++j)
{
a.insert(vvnum[i][j]);//每行的数字集合
b.insert(vvnum[j][i]);//每列的数字集合
}
if( a!=ss || b!=ss )//任意一个不相等,就没有求解成功
return 0;
}
for( int m=0 ;m<7; m+=3 )//3×3个
{
for( int n=0 ;n<7; n+=3 )
{
sint d;
for(int i=0 ;i<3; ++i)//3×3宫格
{
for( int j=0 ;j<3; ++j )
{
d.insert(vvnum[m+i][n+j]);
}
}
if(d!=ss)
return 0;//求解失败
}
}
return 1;//求解成功!
}

至此数据的求解已经完成,但还有一些地方需要补充

3.运行前错误检查

我们不能寄希望于用户提供的数字是完美的,相反,用户提供的数字常常可能伴着错误,比如直接在同一行里输入了两个1。如果带着这些错误运行,程序不卡死也会陷入巨大数量的循环中,几乎会对没有提供的数字集合进行一个遍历。假设用户提供了20个数字,其中有一行有两个1,那么程序会对剩下的71个点,每个点有n种(1< n< 9)可能,程序会进行n^71次计算,,最后返回一个0表示失败。这是一个不可能完成的任务。
所以我们需要提前检查错误,发现错误直接返回,不进入递归中。
定义一个函数isBad()用于运行前检查,同样选择set,可以快速插入,快速查找

        int SudokuCrack::isBad()
{
for( int i=0 ;i<9; ++i )
{
sint a,b;//sint见前面的typedef
for(int j=0; j<9; ++j)
{
if(vvnum[i][j]!=0)//数值为0就跳过
{
if( a.find(vvnum[i][j])==a.end() ) //是否找到了
a.insert(vvnum[i][j]);//没找到就插入
else//找到了相同值就报错
{
return 1;//错误代码1
}
}
if(vvnum[j][i]!=0)
{
if( b.find(vvnum[j][i])==b.end() )
b.insert(vvnum[j][i]);
else//找到了相同值就报错
return 2;
}
}
}
for( int m=0 ;m<7; m+=3 )//3×3×3×3的四重循环
{
for( int n=0 ;n<7; n+=3 )//3×3个
{
sint d;
for(int i=0 ;i<3; ++i)//3×3矩阵
{
for( int j=0 ;j<3; ++j )
{
if(vvnum[m+i][n+j]==0)//值为0就跳过
continue;
if( d.find(vvnum[m+i][n+j])==d.end() )
d.insert(vvnum[m+i][n+j]);
else
{
return 3;//错误代码3
}
}
}
}
}
return 0;//用户的输入正确
}

添加了错误处理,就要在代码中用到,我们的beginCrack()函数只在第一次调用,正是放置运行前错误处理的地方,beginCrack()如下

        int SudokuCrack::beginCrack()
{
int n=isBad();
if( n!=0 )
return n;
result = vvint(1,vint(1,0));
selectBetter();
return 0;
}

在使用的时候可根据beginCrack()的返回值不同,进行不同的错误处理。返回为0则正常运行。可以定义一个静态成员函数用来获取求解的结果

     const vvint& getResult(){  return result   ;   }

4.代码实例

至此我们有了一个稳健的求解方法,下面是一个实例

        #include<iostream>
#include "sudokucrack.h"
using namespace std;
int main()
{
vvint d{
{4,0,0,5,2,0,7,0,3},
{0,0,0,0,0,3,0,0,0},
{1,0,0,0,0,7,0,0,0},
{0,1,4,0,0,0,0,0,6},
{7,0,0,0,5,0,0,0,1},
{5,0,0,0,0,0,4,2,0},
{0,0,0,4,0,0,0,0,5},
{0,0,0,8,0,0,0,0,0},
{2,0,1,0,7,6,0,0,8}
};
SudokuCrack s(d);
int m=s.beginCrack();
if(m==0)
{
const vvint &r=s.getResult();
for(const vint & a : r)//c++11,基于范围的for循环
{
for( int d : a)
cout<<d<<" ";
cout<<"\n";
}
cout<<endl;
}
else if(m==1)
cout<<"数独非法!在某行中有重复数字!"<<endl;
else if(m==2)
cout<<"数独非法!在某列中有重复数字!"<<endl;
else if(m==3)
cout<<"数独非法!在某3×3矩阵中有重复数字!"<<endl;
return 0;
}

瞬间完成,输出如下

    4 9 6 5 2 8 7 1 3 
8 7 5 1 4 3 6 9 2
1 3 2 9 6 7 8 5 4
3 1 4 2 8 9 5 7 6
7 2 9 6 5 4 3 8 1
5 6 8 7 3 1 4 2 9
6 8 7 4 9 2 1 3 5
9 4 3 8 1 5 2 6 7
2 5 1 3 7 6 9 4 8