了解一下数独

  • 数独规则
  • 最常规的回溯法
  • 再看这
  • 第一种,唯一候选数法
  • 第二种,隐式唯一候选数法
  • plan B
  • 此处附上我的测试
  • 写在最后
  • 附上代码


数独规则

数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次。此处出现的候选数我们稍后再提。

※不要求斜线也满足条件

数独算法python 数独算法有哪些_数独算法python

最常规的回溯法

对于常规,也可以说是最常使用的一种算法,回溯法。它可以用来解决很多问题,比如这里的数独问题。
先说说它的优点,对于程序员来说,代码量很少,往往几层嵌套循环就可以解决问题,逻辑也很简单, 编写也很快捷。但这里我们为什么不推荐使用呢,因为它对于这个问题,时间复杂度有些高。
首先这个算法类似与我们平时说的试。先对于一个空格,依次读取该格所在行,所在列,所在宫,保证所填数与三者中任一数都不重复。比如上面的图,第一行第二列那个格,检查该行列宫后,出现数为123567,所以它可以填489,根据回溯法,就填4,然后看第二空格,以此类推,直到有一空格没有数字可填,则将上一空格所填数更改,如果该格也没有可以更改的数,那么再退上一格。。。。。所以到后来,如果运气不好,这个方法可能会试非常非常多次,所以我认为对于单纯的计算机效率来说,这个方法不算优。

再看这

此时,再将我要写的算法之前,先讲一下人工数独解法。此处要引入一个概念,叫做候选数。
候选数顾名思义,就是候选的数字。对于每一个要填的空格,都有一个候选数组。比如第一行第二列的那个空格,489就是它的候选数,如果还是不懂,百度一下你就知道!

第一种,唯一候选数法

其实这些解法只是数独解法的冰山一角,往往一个复杂的数独题目要使用很多很多的方法。具体可参考这里 但是对于实现起来的逻辑,我挑选了一共两种解法交替使用。第一种,也是最好用的,唯一候选数法。即按照上面提到的,将每一个空白格按照行列宫依次查找,最后得出其候选数组,然后找到那个候选数组中候选数个数为1的,说明这个格只能填这个数字,然后填入。之后更新所填格所在的行列宫的其他空白格的候选数组,也就是减去新填入的这个数字。

数独算法python 数独算法有哪些_数独_02

当某行已填数字的宫格达到8个,那么该行剩余宫格能填的数字就只剩下那个还没出现过的数字了。成为行唯一解。
唯一候选数法的思路是:选定一个空白格,从其初始候选数中删除与它在同一行、同一列以及同一宫中已经确定的数字,当这一格的候选数为1时,这一格正确的数字就是剩下的唯一一个候选数。如此重复,直至找到全部解或者无法再用此方法进行下去。
比如这个题,我们可以发现中间那个格子,填5,然后将该行列宫中所有的候选数都删去5,发现该行第二列那个各自只能填2,依次类推,到后来会出现越来越多的唯一候选数格子。

第二种,隐式唯一候选数法

与第一种方法类似,只是有时我们发现所有空白格中,没有一个候选数个数为1的,此时第一种方法已经行不通了,我们就要用到隐式法了。虽然表面上没有候选数

当某个数字在某一行(列、宫)各单元格的候选数中只出现一次时,那么这个数字就是这一列的唯一候选数了.这个单元格的值就可以确定为该数字. 这时因为,按照数独游戏的规则要求每一行(列、宫)都应该包含数字1~9,而其它单元格的候选数都不含有该数,则该数不可能出现在其它的宫格,那么就只能出现在这个宫格了。

数独算法python 数独算法有哪些_数独_03


比如这个题,会发现没有唯一候选数格子,此时第一种方法失败,然后用第二种方法,可以发现第三行第七列中候选为5的那个格子,该行的候选数中再没有出现5,所以这里必填5,然后更新该行列宫的候选数,以此方法,一旦出现候选数为1的格子,立刻停止此方法,调用第一个方法,直到第一个方法再次失败,再调用此方法。

plan B

如果两种方法都失败了呢?那就无可奈何了,我的算法逻辑仅写到这里。有兴趣的朋友可以写更多的算法,根据上面提到的链接,或者百度一下数独的解法。
但对于我的方法就结束了,这时数独如果还没解完怎么办?那就回到经典的回溯法去解了。但有人也许会问,这不还是一样吗?还是需要回溯法。
其实不然,回溯法之所以对于复杂数度题目解起来很慢,是因为空白格太多,所以可能的情况太多了。而此时我们经过上面两种方法交替使用后,可以发现填出来很多格子,再不济只填出一个格子,那样对于回溯法来说,少的复杂度也是几何倍数的。所以总体来说,经过上面两种解法解过的数度题,要不解完,要不难度也不会很大了。此时调用回溯法完善就很快捷了。
PS:如果上面两种方法一个空白格也填不出来,我也没办法了,因为数独解法太多了,有兴趣的朋友可以继续写写Plan c,Plan d。。。。

此处附上我的测试

就下图这个较为复杂数独来说,比如此时右侧数独,因为所给数不多,而且分布较为集中,所以此时若按常规的回溯方法来解,势必回有较大的复杂度。

数独算法python 数独算法有哪些_c语言_04


我在同一台设备上进行了多次重复实验。单一采用回溯法,我们解题大概需要0.28s左右

数独算法python 数独算法有哪些_数独_05


但用了我的两种方法后,数独变成了这个样子

数独算法python 数独算法有哪些_数独算法python_06


此时我的两种方法都用不了了,调用Plan B回溯法,但就这么几个空白格,相信人都可以手动推算出来,所以此时时间复杂度大大降低。

数独算法python 数独算法有哪些_数独_07


而当我们使用综合方法时,此时的时间大致在0.16s左右

※也许看起来快了其实并没有多少,但是你要知道,这么复杂的题目,对于计算能力恐怖的计算机来说,对于计算机来说,0.01s的优势都是多大的优化。

但是对于一些简单的数独题目,这个方法的优势就不是很大了

下图是回溯法的

数独算法python 数独算法有哪些_数独_08


这是我的方法的

数独算法python 数独算法有哪些_数据结构_09


但对于复杂题目,再来个例子

回溯法的

数独算法python 数独算法有哪些_数独算法python_10


我的

数独算法python 数独算法有哪些_数据结构_11


还是快了很多哦!

写在最后

第一次写这个博客,可能说的不是很清楚,我也只是一个大二学生,程序小白,这个代码的优化程度根本不够,而且逻辑很复杂,很庞大。总之,写的太傻了,但是我认为的比较好的解数独方法,大家不要杠我~~最后欢迎各位提个意见!

附上代码

觉得不错的话,记得顶一下哦!
有问题可以评论区留言哦

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

//原题
/*int a[9][9]={{5,3,0,0,7,0,0,0,0},
             {6,0,0,1,9,5,0,0,0},
             {0,9,8,0,0,0,0,6,0},
             {8,0,0,0,6,0,0,0,3},
             {4,0,0,8,0,3,0,0,1},
             {7,0,0,0,2,0,0,0,6},
             {0,6,0,0,0,0,2,8,0},
             {0,0,0,4,1,9,0,0,5},
             {0,0,0,0,8,0,0,7,9}};*/
//难度3
/*int a[9][9]={{0,0,0,1,0,0,2,6,0},
             {7,0,0,0,3,0,0,0,0},
             {3,0,2,0,8,0,4,0,0},
             {0,0,0,4,0,8,0,0,1},
             {0,3,5,0,0,0,9,4,0},
             {2,0,0,3,0,5,0,0,0},
             {0,0,6,0,5,0,7,0,9},
             {0,0,0,0,4,0,0,0,8},
             {0,5,7,0,0,9,0,0,0}};*/
//难度4

/*int a[9][9]={{0,0,1,0,5,0,0,0,0},
               {9,0,7,0,0,3,0,0,0},
               {0,4,3,9,2,8,0,0,0},
               {0,5,0,0,0,0,0,4,2},
               {0,7,0,0,0,0,0,8,0},
               {1,9,0,0,0,0,0,6,3},
               {0,0,0,5,1,6,4,2,0},
               {0,0,0,3,0,0,9,0,6},
               {0,0,0,0,7,0,8,0,0}};*/
/*int a[9][9]={{0,1,0,0,0,8,4,0,7},
             {9,5,0,0,0,0,0,0,0},
             {0,0,8,0,1,0,0,0,0},
             {0,8,2,0,0,0,0,0,0},
             {7,0,0,4,0,6,0,0,8},
             {0,0,0,0,0,0,6,2,0},
             {0,0,0,0,5,0,7,0,0},
             {0,0,0,0,0,0,0,8,2},
             {5,0,3,2,0,0,0,1,0}};*/

//难度5
int a[9][9]={{9,0,0,0,1,7,0,0,2},
             {5,6,3,0,0,0,0,0,0},
             {0,0,0,0,0,0,0,0,0},
             {0,0,0,5,0,0,0,8,0},
             {0,7,0,0,0,0,4,0,0},
             {0,2,0,0,0,0,0,3,0},
             {8,0,0,3,0,0,0,0,0},
             {4,0,0,0,0,0,0,0,7},
             {0,0,0,0,0,0,0,0,0}};

/*int a[9][9]={{9,0,0,0,1,7,0,0,2},
             {5,6,3,0,0,0,0,0,0},
             {0,0,0,0,0,0,0,0,0},
             {0,0,0,5,0,0,0,8,0},
             {0,7,0,0,0,0,4,0,0},
             {0,2,0,0,0,0,0,3,0},
             {8,0,0,3,0,0,0,0,0},
             {4,0,0,4,8,9,0,0,7},
             {0,0,0,0,0,0,0,0,0}};*/

struct King
{
	int num;
	int candidata[9] = {1,2,3,4,5,6,7,8,9};  //候选字数组
	int row;
	int line;
	int Gnum;
	int cannum = 9;
};

struct King shudu[81];
int G[9];
int check = 0;      //检查标志,若数独完成,则check = 1
int measure1 = 0,measure2 = 0,measure3 = 0;     //判断使用哪些方法


void del(King *shudu,int num);
int read(int candidata[]);
int update(int row,int line,int Gnum,int num);
void seek();
void sub(int backup[],int candidata[]);
void implicit();
void search(int n);
int canplace(int n,int i);
void output();
void renew();
void out();

int main()
{
	int i,j;
    int m,n,b;
    int e,d;
    int answer = 1;     //数独解情况
    printf("解数独程序开始!\n原数独为:\n");
    output();
    printf("\n数独解为:\n");
	for(i=0;i<9;i++)     //初始化数独结构体数组
    {
        int n = 0;
        int m = 0;
        for(j=0;j<9;j++)
            {
                int x = i*9+j;
                shudu[x].num = a[i][j];
                shudu[x].row = i;
                shudu[x].line = j;
                if(i<3)
                    if(j<3)
                        {
                            G[0] = 0;
                            shudu[x].Gnum = 0;
                        }
                    else if(j<6)
                        {
                            G[1] = 3;
                            shudu[x].Gnum = 1;
                        }
                    else
                        {
                            G[2] = 6;
                            shudu[x].Gnum = 2;
                        }
                else if(i<6)
                    if(j<3)
                        {
                            G[3] = 27;
                            shudu[x].Gnum = 3;
                        }
                    else if(j<6)
                        {
                            G[4] = 30;
                            shudu[x].Gnum = 4;
                        }
                    else
                        {
                            G[5] = 33;
                            shudu[x].Gnum = 5;
                        }
                else
                    if(j<3)
                        {
                            G[6] = 54;
                            shudu[x].Gnum = 6;
                        }
                        else if(j<6)
                        {
                            G[7] = 57;
                            shudu[x].Gnum = 7;
                        }
                        else
                        {
                            G[8] = 60;
                            shudu[x].Gnum = 8;
                        }
            }
    }
    for(i=0;i<81;i++)     //初始化候选字
    {
        if(shudu[i].num == 0)
        {
            for(j=shudu[i].row*9;j<shudu[i].row*9+9;j++)
                {
                    if(shudu[j].num!=0)
                    {
                        del(&shudu[i],shudu[j].num);
                    }
                }
                for(m=shudu[i].line;m<81;m=m+9)
                {
                    if(shudu[m].num!=0)
                    {
                        del(&shudu[i],shudu[m].num);
                    }
                }
                d=G[shudu[i].Gnum];
                for(e=0;e<3;e++)
                {
                    for(j=0;j<3;j++)
                    {
                        if(shudu[d].num!=0)
                        {
                            del(&shudu[i],shudu[d].num);
                        }
                        d++;
                    }
                    d=d+6;
                }
        }
    }

        seek();     //调用唯一候选数解法
        renew();
        for(i=0;i<9;i++)
        {
            for(j=0;j<9;j++)
            {
                if(a[i][j] == 0)
                {
                    answer = 0;
                    break;
                }
            }
            if(answer == 0)
            {
                break;
            }
        }
        if(answer == 1)
        {
            printf("\n解此数独使用方法:\n");
            if(measure1 == 1)
                printf("唯一候选法 ");
            if(measure2 == 1)
                printf("隐式候选法 ");
            if(measure3 == 1)
                printf("回溯法 ");
            printf("\n");
        }
        else
        {
            printf("此数独无解!\n");
        }
}

int change;                 //调用隐式法开关
void seek()                 //唯一候选数解法
{
    int shut = 0;        //遍历开关
    change = 1;              //隐式法开关打开
    check = 1;
	int v,i;
	for(i=0;i<81;i++)//寻找唯一解
    {
        if(shudu[i].num == 0&&shudu[i].cannum!=1)
        {
            shut = 1;                   //发现空单元,需再次遍历,打开开关
            check = 0;
        }
        if(shudu[i].cannum == 1)
        {
            shudu[i].num = read(shudu[i].candidata);//读取唯一候选字并填入
            shudu[i].cannum--;
            update(shudu[i].row,shudu[i].line,shudu[i].Gnum,shudu[i].num);	//更新所在行列的元素候选字
            change = 0;         //关闭隐式法开关
            measure1 = 1;
        }
        if(i == 80)
        {
            if(change == 0)         //若隐式法法开关为关闭
            {
                if(shut == 1)
                {
                    seek();
                    shut =0;     //遍历开关关闭
                }
                else
                {
                    out();
                }
            }
            else
            {
                if(check == 0)
                {
                    implicit();
                }
                else
                {
                    out();//输出
                }
            }
        }
    }
}

void del(King *shudu,int num)
{
	int i;
	for(i=0;i<9;i++)
	{
		if(shudu->candidata[i] == num)
		{
			shudu->candidata[i] = NULL;
			if(shudu->cannum>0)
                shudu->cannum--;
		}

	}
}
int read(int candidata[])
{
	int i;
	for(i=0;i<9;i++)
	{
		if(candidata[i] != NULL)
			return(candidata[i]);
	}
                                                    //需要添加读取失败提示!!!!!!
}
int notice;     //出现唯一候选数开关(默认为关)
int update(int row,int line,int Gnum,int num)
{
	int i,j;
	int x = G[Gnum];
	for(i=row*9;i<row*9+9;i++)
	{
		if(shudu[i].num==0)
		{
			del(&shudu[i],num);
			if(shudu[i].cannum == 1)
			{
                notice = 1;         //打开唯一候选数开关
			}
		}
	}
	for(i=line;i<81;i=i+9)
	{
		if(shudu[i].num==0)
		{
			del(&shudu[i],num);
			if(shudu[i].cannum == 1)
			{
                notice = 1;         //打开唯一候选数开关
			}
		}
	}
	for(i=0;i<3;i++)
	{
		for(j=0;j<3;j++)
		{
			del(&shudu[x],num);
            if(shudu[i].cannum == 1)
            {
                notice = 1;         //打开唯一候选数开关
            }
            x++;
		}
		x=x+6;
	}

}
int count = 0;              //backup元素个数计数器
int repeat;             //隐式遍历开关(若有更新则打开)
void implicit()             //隐式唯一候选数解法
{
    int i,j,m,n,f,g;
    notice = 0;     //关闭唯一候选数开关
    repeat = 0;     //关闭隐式遍历开关
    int check1;     //数独单元完成开关
    for(i=0;i<81;i++)
    {
        int backup[9];              //候选字备用数组
        if(shudu[i].num == 0)
        {
            check1 = 0;      //数独单元未完成
            for(g=0;g<9;g++)
            {
                backup[g] = shudu[i].candidata[g];
            }
            count = shudu[i].cannum;
            for(j=shudu[i].row*9;j<shudu[i].row*9+9;j++)        //按行搜索隐式候选字
            {
                if(shudu[j].num == 0 && j!=i)
                {
                    sub(backup,shudu[j].candidata);
                    if(count == 0)
                        break;
                }
            }
            if(count == 1)
            {
                shudu[i].num = read(backup);//读取隐式唯一候选字并填入
                shudu[i].cannum = 0;
                update(shudu[i].row,shudu[i].line,shudu[i].Gnum,shudu[i].num);	//更新所在行列的元素候选字
                repeat = 1;
                check1 = 1;
                measure2 = 1;
                if(notice == 1)
                {
                    break;
                }
            }
            for(g=0;g<9;g++)
            {
                backup[g] = shudu[i].candidata[g];
            }
            count = shudu[i].cannum;
            for(m=shudu[i].line;m<81;m=m+9)             //按列搜索隐式候选字
            {
                if(shudu[m].num == 0 && m!=i)
                {
                    sub(backup,shudu[m].candidata);
                    if(count == 0)
                        break;
                }
            }
            if(count == 1)
            {
                shudu[i].num = read(backup);//读取隐式唯一候选字并填入
                shudu[i].cannum = 0;
                update(shudu[i].row,shudu[i].line,shudu[i].Gnum,shudu[i].num);	//更新所在行列的元素候选字
                repeat = 1;
                check1 = 1;
                measure2 = 1;
                if(notice == 1)
                {
                    break;

                }
            }
            for(g=0;g<9;g++)
            {
                backup[g] = shudu[i].candidata[g];
            }
            count = shudu[i].cannum;
            n=G[shudu[i].Gnum];
            for(f=0;f<3;f++)                //按宫搜索隐式候选字
            {
                for(j=0;j<3;j++)
                {
                    if(shudu[n].num == 0 && n!=i)
                    {
                        sub(backup,shudu[n].candidata);
                        if(count == 0)
                        {
                            break;
                        }
                    }
                    n++;
                }
                n=n+6;
                if(count == 0)
                {
                    break;
                }
            }
            if(count == 1)
            {
                shudu[i].num = read(backup);//读取隐式唯一候选字并填入
                shudu[i].cannum = 0;
                update(shudu[i].row,shudu[i].line,shudu[i].Gnum,shudu[i].num);	//更新所在行列的元素候选字
                repeat = 1;
                check1 = 1;
                measure2 = 1;
                if(notice == 1)
                {
                    break;
                }
            }
            if(check1 == 0)
            {
                check = 0;
            }
        }
    }
    if(repeat == 1)         //若数独有更新,则再次搜索隐式候选字
    {
        if(check == 0)      //若数独未完成
        {
            implicit();
        }
        else
        {
            out();//输出
        }
    }
    else
    {
        if(check == 0)      //若数独未完成
        {
            if(notice == 1)     //若出现唯一候选字,则调回唯一法
            {
                seek();
            }
            else
            {
                renew();//out();
                measure3 = 1;
                search(0);// 调用回溯!!!!!!!!!!!!!!!!!

            }
        }
        else
        {
            out();
        }
    }
}

void sub(int backup[],int candidata[])         //删除缓存数组中的候选字
{
    int k,j;//for(k=0;k<9;k++)printf("backup为%d",backup[k]);
    for(k=0;k<9;k++)
    {
        if(backup[k] != NULL)
        {
            for(j=0;j<9;j++)
            {//printf("!");
                if(candidata[j] != NULL)
                {
                    if(backup[k] == candidata[j])
                    {
                        backup[k] = NULL;
                        count--;
                    }
                }

            }
        }
    }
}
void renew()
{
    int i,j;
    for(i=0;i<9;i++)
    {
        for(j=0;j<9;j++)
        {
            a[i][j] = shudu[i*9+j].num;
        }
    }
}




void search(int n)
{
    int i;
    if(n==81)
    {
        output();
        printf("\n解此数独使用方法:\n");
        if(measure1 == 1)
            printf("唯一候选法 ");
        if(measure2 == 1)
            printf("隐式候选法 ");
        if(measure3 == 1)
            printf("回溯法 ");
        printf("\n");
        exit(0);
    }
    else if(a[n/9][n%9]!=0)
    {
        search(n+1);//若该位置上已有数字,则跳转至下一个位置
    }
    else if(a[n/9][n%9]==0)
    {
        for(i=1;i<=9;i++)
        {
            if(canplace(n,i))//判断该位置上能否放置数字i,若可以为其赋值,跳转至下一位置
            {
                a[n/9][n%9]=i;
                search(n+1);
            }
            a[n/9][n%9]=0;//若若找不到可以满足条件的数放置在该位置,还原它本来的值,回溯寻找下一组可能的解,每次调用完之后都需要让它返回与原来的值
        }
    }
}

int canplace(int n,int i)
{
    int j,k,flag=1;
    for(j=0;j<=8;j++)//判断该列上是否有该数字
    {
        if(a[n/9][j]==i)
        {
            flag=0;
            break;
        }
    }
    if(flag==1)
    {
        for(j=0;j<=8;j++)//判断该行是否有该数字
        {
            if(a[j][n%9]==i)
            {
                flag=0;
                break;
            }
        }
    }
    if(flag==1)//判断其所在的小九宫格里是否有该数字
    {
        for(j=(n/9/3)*3;j<(n/9/3)*3+3;j++)
        {
            for(k=(n%9/3)*3;k<(n%9/3)*3+3;k++)
            {
                if(a[j][k]==i)
                {
                    flag=0;
                    break;
                }
            if(flag==0)
                break;
            }
        }
    }
    return flag;
}

void output()//输出数组
{
    int i,j;
    for(i=0;i<9;i++)
    {

        for(j=0;j<9;j++)
        {
            printf("%d  ",a[i][j]);
        }
        printf("\n");
    }
}

void out()
{
    int i;
    for(i=0;i<81;i++)
		{
            if(i%9 == 0)
            {
                printf("\n");
            }
            printf("%d  ",shudu[i].num);

		}
}