C++使用回溯法生成数独

数独,应该不用我说明吧,是一个9*9的矩阵,矩阵里的每一个数字都是1~9中的一个。在每一行、每一列每个数字只能出现一次,另外,在每一个九宫格里每个数字也只能出现一次。

我曾经在网上看过一种“假”的数独生成法,生成的数独其实是有规律的,只要把中间的九宫生成好,那么周围的8个九宫就可以按照规律自动生成,但是这样的话便达不到“随机”的效果,于是经过苦想我想出了下面的方法:

按行生成数字。如果生成了与该行或该列相同的数字,或在这个九宫里有重复的数字,则删除刚刚生成的相同数字并且重新生成该数字。简而言之,就是先检查行,再检查列,再检查九宫,直到把这81个数字都生成出来。如果某一行出现“无解”的情况,则重新生成上一行,称之为“回溯”。

可想而知,这样的程序效率是不高的。首先的问题就是,要如何生成第一行没有重复的数字?最原始的办法就是从1~9依次随机,如果生成的数与该行某个数相同,则重新生成。但是这样的话程序效率很低,因为随机的次数会很多,比方说假设到了要生成某一行的最后一个数了,那么这个数成功生成的概率只有1/9,电脑平均要随机9次才可以生成。因此我想的方法是把每一行能够填入的数保存在一个数组avail里,并且初始化int avail[9]= {1,2,3,4,5,6,7,8,9},一旦成功生成某个数,那么就在avail里删除这个数,例如生成了3,那么就从avail里删掉‘3’,这样的话就可以大大的减少随机的次数。

按照这个思路,我设计了一下代码:

#include <iostream>

#include <stdlib.h>

#include <time.h>

#include <conio.h>

using namespace std;

int sudoku[9][9],avail[9]={1,2,3,4,5,6,7,8,9};//定义数独数组和可选数字数组

代码中的sudoku是用来保存数独的二维数组,avail是每一行可以填入的数字,可以知道,每当行数向下加一的时候avail就必须要重置一次。

void remove(int t[],int);//声明移除前置函数

void restore(void);//声明重置函数

void print(void);//声明打印数独函数

void init(void);//声明初始化函数

int search(int t[],int);//声明搜索下标函数

int en=9;//可选avail下标

Remove函数是用来删除t数组(也就是avail数组)里某个特定的数的,restore函数是用于重置avail数组的,print函数是用于打印数组的,init函数是用于初始化sudoku二维数组。Search函数用于查找avail数组中某个数的位置(下标),以便删除。如果这个数不存在则返回-1。整数en是可选数字的最大下标,每从avail里删除一个数,则都要有en–

下面是函数的定义:其中srand是根据时间来随机化。

void init() //初始化

{

int x,y;

srand(time(NULL));

for (y=0;y<=8;y++)

{

for (x=0;x<=8;x++)

{

sudoku[y][x]=0;

}

}//初始化数组

}

void remove(int t[],int n) //n为下标,移除排除下标,项前移

{

int i;

for (i=n;i<=7;i++)

{

t[i]=t[i+1];

}

en–;//可选数字在原有基础上减

}

int search(int t[],int n) //n为搜索的数字,返回下标,如果没有则返回-1

{

int i;

for (i=0;i<=en-1;i++)

if (t[i]==n)

return i;

return -1;

}

void restore() //初始化en,avail

{

int i;

en=9;

for (i=0;i<=8;i++)

avail[i]=i+1;

}

void print(void)

{

int i,j;

for (i=0;i<=8;i++)

{

for (j=0;j<=8;j++)

{

cout << sudoku[i][j] << ” “

}

cout << endl;

}

}

接下来就只有main函数了。

我的思路是每生成一个数,马上判断它的同行、同列和同九宫里有没有与之相同的数,如果有则重新生成该数,没有则从avail里删除这个数。判断同行和同列比较方便,用简单的循环就可以搞定,排除九宫则稍微复杂一点,它需要两个循环。假设有两个整型变量xy,分别是sudoku数组的列坐标和行坐标,已知某数的是sudoku[y][x],由于在C++中两个整型数相除的结果还是整数,那么sudoku[(y/3)*3][(x/3)*3]就是该数所在的九宫的第一个数,然后进行循环便可以判断九宫了。如果是无解,则重新生成上一行。Main函数源代码如下:

int main()

{

int x=0,y=0;

int kill;//已存在的数的小标

init();

cout << 正在生成一个数独…” << endl;

for (y=0;y<=8;y++)

{

for (x=0;x<=8;x++)

{

/*思路:每将生成一个新数字的时候,重新生成avail数列*/

int i,j;

for (i=0;i<=x;i++)//排除横行

{

kill=search(avail,sudoku[y][i]);

if (kill!=-1)

remove(avail,kill);

}

for (i=0;i<=y;i++)//排除纵行

{

kill=search(avail,sudoku[i][x]);

if (kill!=-1)

remove(avail,kill);

}

for (i=0;i<=2;i++) //排除九宫

{

for (j=0;j<=2;j++)

{

kill=search(avail,sudoku[(y/3)*3+i][(x/3)*3+j]);

if (kill!=-1)

remove(avail,kill);

}

}

if (en==0 && y>0) //排除无解

{

y-=2;

break;

}

else if (en==0 && y==0)

{

init();

y=-1;

break;

}

sudoku[y][x]=avail[rand()%en];

restore();

}

restore();

}

print();//打印数独

getch();

return 0;

}

心得:尽管经过优化,这个程序运算速度仍然要靠人品。这是个纯计算问题,没有用到一些高深的知识。没有用到类,函数与函数之间的耦合性也非常的强。但是这还是需要掌握对数组的操作。该程序在VC6.0下编译通过。

 

 

    原文作者:回溯法
    原文地址: https://blog.csdn.net/froser/article/details/6486408
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞