当然基本思路还是离不开穷举,只不过将穷举的次序进行改进:建立一个矩阵,记录下数独中每个空位可能填写的数字的个数,每次只对数独中可能性个数最少的空位操作。
另外,程序中会使用之前写的泛型栈,可参考《C语言实现一个泛型栈》
数据和函数声明:
#include <stdio.h>
#include "stack.h"
#define BLANK 0
#define TOPIC 1
#define TRY 2
static int sudo[9][9];
static int sudo2[9][9];
static int sudo3[9][9];
typedef struct
{
int v;
int x;
int y;
}recorder;
static void sudo2Min(int *y, int *x);
static int sudo_2_Init();
static int sudo1_3Init();
static int sudoInit(int arr[9][9]);
static int sudo2ElemUpdate(int y, int x);
static int sudo2RowUpdate(int y, int x);
static int sudo2ColUpdate(int y, int x);
static int sudo2SquUpdate(int y,int x);
static int sudo2Update(int y, int x);
static int verSquare(int y, int x);
static int verRow(int y, int x);
static int verCol(int y, int x);
static int verify(int y, int x);
static int verifyAll();
static void next(int *y, int *x);
void sudoPrt();
int sudoGo(int arr[9][9]);
数据说明:
- sudo[]9[9] 记录数独每个位置所填写的数字, sudo2[9][9] 记录每个空位能够填写的数字个数, sudo3[9][9] 记录每个位置的状态
- 宏定义BLANK、TOPIC、TRY分别代表空位,题目给出所占用的位,已尝试填写的位
- 结构recorder记录坐标和填写的数字,用于压栈和弹栈。
函数说明:
static int verCol(int y, int x) 检查坐标所在的列所填的数字能否通过
static int verCol(int y, int x)
{
int n;
for(n = 0; n < 9; n++){
if(n == y)continue;
if(sudo[n][x] == sudo[y][x])
return 0;
}
return 1;
}
static int verRow(int y, int x) 检查坐标所在的行所填的数字能否通过
static int verRow(int y, int x)
{
int n;
for(n = 0; n < 9; n++){
if(n == x)continue;
if(sudo[y][n] == sudo[y][x])
return 0;
}
return 1;
}
static int verSquare(int y, int x) 给出一个坐标,检查这个坐标所填的数字在的9格方块中能否通过。
static int verSquare(int y, int x)
{
int dx,dy;
int begx = (x / 3) * 3; // 起始坐标
int begy = (y / 3) * 3; // 起始坐标
for(dy = 0; dy < 3; dy++){
for(dx = 0; dx < 3; dx++){
if((begx + dx == x) && (begy + dy == y))continue;
if(sudo[begy + dy][begx + dx] == sudo[y][x])
return 0;
}
}
return 1;
}
static int verify(inty, int x) 调用以上3个函数,验证一个坐标所填的数字能否通过
static int verify(int y, int x)
{
if(sudo[y][x] != 0){
if(verSquare(y,x) == 0 || verCol(y,x)== 0 || verRow(y,x) == 0 ){
return 0;
}
}
return 1;
}
static int verifyAll() 遍历整个数独,如果全部通过即找到解答
static inline int verifyAll()
{
int x,y;
for(y = 0; y < 9; y++)
for(x = 0; x < 9; x++){
if(sudo[y][x] == 0)
return 0;
}
for(y = 0; y < 9; y++)
for(x = 0; x < 9; x++){
if(verify(y,x) == 0)
return 0;
}
return 1;
}
void sudoPrt() 在屏幕上打印出数独
void sudoPrt()
{
int y,x;
for(y = 0; y < 9; y++){
for(x = 0; x < 9; x++){
printf("%d ",sudo[y][x]);
}
printf("\n");
}
}
static void next(int *y, int *x) 选出下一个空位
static void next(int *y, int *x)
{
*x += 1;
*y += *x / 9;
*x = *x % 9;
if(sudo3[*y][*x] != BLANK && *y < 10)
next(y,x);
}
static void sudo2Min(int *y, int *x) 在sudo2中查找可能的数字最少的坐标
static void sudo2Min(int *y, int *x)
{
int x2,y2;
*x = -1;
*y = 0;
x2 = -1;
y2 = 0;
next(y,x);
while(y2 < 9){
next(&y2,&x2);
if(sudo2[y2][x2] < sudo2[*y][*x]){
*y = y2;
*x = x2;
}
}
}
static int sudo2ElemUpdate(int y, int x) 给出一个坐标,从sudo2里更新这个位置能够填写数字的个数:
static int sudo2ElemUpdate(int y, int x)
{
int i, v, tmpSudo, ableNum;
ableNum = 0;
tmpSudo = sudo[y][x]; // 先记下这个位置的数字
for(v = 1; v < 10; v++){ // 然后从 1到9 进行遍历
sudo[y][x] = v; // 如果验证通过ableNum++
if(verify(y,x))
ableNum++;
}
sudo[y][x] = tmpSudo; // 还原这个位置的数字
if(ableNum == 0 && sudo3[y][x] == BLANK){
return 0; // 如果存在一个空位置,并且不能填写任何数字,那么返回0
}
sudo2[y][x] = ableNum;
return 1;
}
static int sudo2RowUpdate(int y, int x) 在sudo2中,更新坐标所在行的每个空位的可填数字个数
static int sudo2RowUpdate(int y, int x)
{
int i,rowX, rowY;
rowX = x; // 行起始坐标
rowY = 0;
for(i = 0; i < 9; i++){
if(sudo3[rowY + i][rowX] != BLANK)continue;
if(sudo2ElemUpdate(rowY + i,rowX) == 0)
return 0;
}
return 1;
}
static int sudo2ColUpdate(int y,int x) 在sudo2中,更新坐标所在列的每个空位的可填数字个数
static int sudo2ColUpdate(int y,int x)
{
int i,colX, colY;
colX = 0; // 列起始坐标
colY = y;
for(i = 0; i < 9; i++){
if(sudo3[colY][colX + i] != BLANK)continue;
if(sudo2ElemUpdate(colY,colX + i) == 0)
return 0;
}
return 1;
}
static int sudo2SquUpdate(int y, int x) 在sudo2中,更新坐标所在9格方块的每个空位的可填数字个数
static int sudo2SquUpdate(int y, int x)
{
int i,squX, squY;
squX = (x / 3) * 3; // 9格方块起始坐标
squY = (y / 3) * 3;
for(i = 0; i < 9; i++){
if(sudo3[squY + i / 3][squX + i % 3] != BLANK)continue;
if(sudo2ElemUpdate(squY + i / 3,squX + i % 3) == 0)
return 0;
}
return 1;
}
static int sudo2Update(int y,int x) 在sudo2中,利用以上3个函数对坐标所影响到的位置更新
static int sudo2Update(int y,int x)
{
if(sudo2RowUpdate(y,x) && sudo2ColUpdate(y,x) && sudo2SquUpdate(y,x))
return 1;
return 0;
}
static int sudo1_3Init(int arr[9][9]) 对sudo和sudo3进行初始化
static int sudo1_3Init(int arr[9][9])
{
int x, y;
for(y = 0; y < 9; y++)
for(x = 0;x < 9; x++){
sudo[y][x] = arr[y][x];
if(verify(y,x) == 0)
return 0;
if(sudo[y][x] != 0)
sudo3[y][x] = TOPIC;
else
sudo3[y][x] = BLANK;
}
return 1;
}
static int sudo_2_Init() 对sudo2进行初始化
static int sudo_2_Init()
{
int x,y;
x = -1;
y = 0;
while(y < 9){
next(&y, &x);
if(sudo2ElemUpdate(y, x) == 0)
return 0;
}
return 1;
}
static int sudoInit(int arr[9][9]) 利用以上两个函数做初始化
static int sudoInit(int arr[9][9])
{
if(sudo1_3Init(arr) && sudo_2_Init())
return 1;
return 0;
}
int sudoGo(int arr[9][9]) 驱动以上函数,寻找解答,Go
int sudoGo(int arr[9][9])
{
int y,x,val;
stack s;
recorder re;
val = 1;
if(sudoInit(arr) == 0)
return -1; // 首先进行初始化,失败说明给出的题目有问题,返回-1
newStack(&s,sizeof(recorder)); // 初始化栈
sudo2Min(&y,&x); // 首先在sudo2中找出可填数字最少的坐标
do{ // while (!isEmpty(&s));
while(val < 10){ // 从1到10开始尝试
sudo[y][x] = val;
if(verify(y,x)){ // 如果通过测试
if(verifyAll()){ // 如果整个数独都通过测试
disposeStack(&s,NULL); // 销毁栈,找出了解答,返回1
return 1;
}
if(sudo2Update(y, x) == 0){ // 在sudo2中更新所影响到的位置sudo2可填写数字个数
val++; // 如果导致某个位置既是空位,又不能填写任何数字
continue; // 从下个数字尝试
}
re.x = x; // 记录下这个坐标和数字
re.y = y;
re.v = val;
sudo[y][x] = val;
sudo3[y][x] = TRY; // 修改状态
push(&s,&re); // 压栈
val = 1; // 新位置从数字1开始重新遍历
sudo2Min(&y,&x); 找出下一个可填数字最少的坐标
}else{ val++; } // 坐标验证没通过,那么从下一个数字开始
}
sudo[y][x] = 0; // 数字遍历到9了,如果没通过验证
sudo3[y][x] = BLANK; // 当前状态和数字全部清0
pop(&s,&re); // 弹栈,返回到上一次的地方
val = re.v + 1; // 从下一个数字开始
x = re.x;
y = re.y;
}while(!isEmpty(&s));
disposeStack(&s, NULL);
return 0;
}
下面是测试:
#include <stdio.h>
#include <time.h>
extern int sudoGo(int arr[9][9]);
extern void sudoPrt();
int main(int argc, const char *argv[])
{
int c,result;
time_t t1,t2;
int sudo[9][9] = {{9,0,0,0,6,0,8,2,0},
{0,0,0,2,0,8,0,0,5},
{0,0,0,4,0,9,0,6,0},
{2,9,0,1,0,0,6,0,3},
{0,6,0,0,0,0,0,7,0},
{7,0,4,0,0,6,0,5,2},
{0,7,0,8,0,3,0,0,0},
{5,0,0,6,0,7,0,0,0},
{0,2,6,0,4,0,0,0,8}};
t1 = time(NULL);
for(c=0; c < 1000; c++)
result = sudoGo(sudo);
if(result == 1)
sudoPrt(0);
else if(result == 0)
printf("\nNo result\n");
else if(result == -1)
printf("\nError input");
t2 = time(NULL);
printf("\nBegin is %s\n",ctime(&t1));
printf("End is %s\n",ctime(&t2));
printf("Executive spend %f sec.\n", difftime(t2,t1));
return 0;
}
循环1000次的运行结果
9 4 3 7 6 5 8 2 1
6 1 7 2 3 8 9 4 5
8 5 2 4 1 9 3 6 7
2 9 5 1 7 4 6 8 3
1 6 8 3 5 2 4 7 9
7 3 4 9 8 6 1 5 2
4 7 9 8 2 3 5 1 6
5 8 1 6 9 7 2 3 4
3 2 6 5 4 1 7 9 8
Begin is Mon Nov 12 13:13:27 2012
End is Mon Nov 12 13:13:30 2012
Executive spend 3.000000 sec.
之前的穷举法循环1000次需要花费15秒,而用这个算法1000次只花费了3秒,效率提高了5倍,当然对于不同难度的数独测试结果也不同,我是虚拟机上装linux测试的,如果在本地环境下应该会更快。