数独问题之排除法和唯余法

您所在的位置:网站首页 数独的排除法规则 数独问题之排除法和唯余法

数独问题之排除法和唯余法

2024-05-28 22:33| 来源: 网络整理| 查看: 265

关于数独的规则,在这里就不介绍了,蓝天十分钟内教你玩转数独对数独的规则和基本方法进行了解释。

这里主要介绍采用程序的方法来解数独问题。

数独问题最基本的方法是排除法和唯余法。

解数独问题,基本方法都是从数独满足的三个条件而得来的:

1、每行数字填入1-9,且不能重复  2、每列数字填入1-9,且不能重复  3、每宫数字填入1-9,且不能重复(宫就是黑粗线框出的3×3单元格的区域) 

所谓排除法,就是如果i行j列的位置的数字为d,那么i行、i列以及该位置所在的宫的其它位置不能有d的存在,否则就违背了数独的条件。

而唯余法则是用自己本身所在的行、列和宫的其它数字来得到自己可以放置的数字。

不过,应该注意的是,这些基本的方法只能解一些相对比较简单的题目。

排除法是用自己去排除其它格可以放置的数字,因此,就必须保存每个格被排除的数字。依次遍历81个格,如果某个格初始时已经填入了数字,就要用它来排除它所在行、列、宫可以放置的数字,行和列是很好判断的,关键的问题是宫,可以通过某个格获得该格所在的宫,然后依次遍历宫内的各个元素,这是通过in_square()来完成的。如果最后某个格被经过排除后只能放一个数字,那么,该位置就放置该数字。

唯余法是用别人来得到自己可以放置的数字,因此,必须保存每个格可以放置的数字。依次遍历81个格,如果某个宫初始时没有填入数字,就要用它所在的行、列、宫的其它数字来得到它可以放置的数字。其实代码跟上面的类似。

下面解释一下各数据结构和函数的功能:

char sudoku[MAXN][MAXN];    整个数独棋盘,这里为了简单起见,使用二维数组 int flag[MAXN][MAXN];              因为数独棋盘中,有些数字是固定的,保存一个二维数组用于标识这些数字。

                                                 当flag[i][j] = 0,说明sudoku[i][j]需要填入数字,如果flag[i][j] = 1,说明 sudoku[i][j]不需要填入数字。 char *exclude[89];                      包含89个指针的数组,每个指针保存某个格的排除的相关信息。

                                                  sudoku[i][j]对应的排除信息保存在exclude[i * 10 + j]中,它是一个包含9个元素的字符数组。exclude[i * 10 + j][k]保存的是sudoku[i][j]中是否可以放置k + 1,如果exclude[i * 10 + j][k] = 0表示可以放置k + 1,否则表示不能放置k + 1。

int empty_digit;                           记录未填入数字的个数,可以用于结束程序的条件。

以下就是数据结构的定义:

#define MAXN 9 #define TRI 3 char sudoku[MAXN][MAXN]; int flag[MAXN][MAXN]; char *left_digits[89]; char *exclude[89]; int empty_digit;

void set_sudoku()                       将文件中的未完成的数独读取到sudoku二维数组中,主要是为了方便起见,不用每次都输入数独数据。数独在文件中保存的格式是是类似.6.593...9.1...5...3.4...9.1.8.2...44..3.9..12...1.6.9.8...6.2...4...8.7...785.1.这样的,为了简单,81个格的信息都放在一行,"."表示需要填入数字。该函数的实现涉及到了文件的基本操作,打开文件,然后依次读取81个元素,然后关闭文件。

void set_sudoku() { FILE *fp; if((fp = fopen("sudoku", "r")) == NULL) { printf("open file error!\n"); exit(-1); } char ch = 0; int i = 0, j = 0; for(i = 0; i < MAXN; ++i) { for(j = 0; j < MAXN; ++j) { if((ch = fgetc(fp)) != EOF) { if(ch == '.') { sudoku[i][j] = '.'; flag[i][j] = 0; } else { sudoku[i][j] = ch - '0'; flag[i][j] = 1; --empty_digit; } } } } if(fclose(fp) == EOF) { printf("close file error!\n"); exit(-1); } }

void print_sudoku()         打印整个数独棋盘,用两层循环就可以实现,要注意的是,在程序中将1-9当作字符还是整数来处理,这里将1-9当作整数来处理,因此,它们虽然保存在char型数组中,在输出的时候,如果是".",就要当作字符来输出,否则,就要作为整数来输出。

void print_sudoku() { int i = 0, j = 0; for(i = 0; i < MAXN; ++i) { for(j = 0; j < MAXN; ++j) { if(sudoku[i][j] == '.') printf("%c ", sudoku[i][j]); else printf("%d ", sudoku[i][j]); } printf("\n"); } printf("\n"); }

void get_next(int ln, int col, int *next_ln, int *next_col)    获得(ln, col)的下一个位置,而且它在宫内是依次从左到右,从上到下,宫内最后一个元素的一个元素就是下一个宫的第一个元素,因此,利用该函数可以遍历九个宫。

void get_next(int ln, int col, int *next_ln, int *next_col) { if(!((ln + 1) % TRI) && !((col + 1) % TRI)) { if(ln == 8 && col == 8) { *next_ln = ln + 1; *next_col = col + 1; } else if(ln != 8 && col == 8) { *next_ln = ln + 1; *next_col = 0; } else { *next_ln = ln - 2; *next_col = col + 1; } } else if(((ln + 1) % TRI) && !((col + 1) % TRI)) { *next_ln = ln + 1; *next_col = col - 2; } else { *next_ln = ln; *next_col = col + 1; } }

int in_square(int ln, int col, int digit)           判断digit是否在(ln, col)所在的宫内,首先利用计算机的四则运算来获得(ln, col)所在的宫的第一个元素,然后调用9次get_next()来遍历这个宫,如果digit存在,则digit在该宫中,否则不在。

int in_square(int ln, int col, int digit) { int s_ln = (ln / TRI) * TRI, s_col = (col / TRI) * TRI; int cnt = 9; int next_ln = 0, next_col = 0; while(cnt--) { if(sudoku[s_ln][s_col] == digit) return 1; get_next(s_ln, s_col, &next_ln, &next_col); s_ln = next_ln; s_col = next_col; } return 0; }

int is_valid(int ln, int col, char digit)        判断digit是否在(ln, col)所在的行和列,其实两个循环可以合并成一个,不过为了直观,这里并没有将它们合并。

int is_valid(int ln, int col, char digit) { int i = 0, j = 0; for(i = 0; i < MAXN; ++i) { if(i != ln && sudoku[i][col] == digit) { return 0; } } for(j = 0; j < MAXN; ++j) { if(j != col && sudoku[ln][j] == digit) { return 0; } } return 1; }

void set_exclude(int ln, int col)      为sudoku[ln][col]设置排除集,设置排除集是用别的元素来排除它,依旧是分为三个部分。

void set_all_exclude()                    为数独中所有元素设置排除集,两层循环,不做过多解释。

void set_exclude(int ln, int col) { if(flag[ln][col] == 0) return; int i = 0, j = 0; for(i = 0; i < MAXN; ++i) { if(i != ln && flag[i][col] == 0) { exclude[i * 10 + col][sudoku[ln][col] - 1] = 1; } } for(j = 0; j < MAXN; ++j) { if(j != col && flag[ln][j] == 0) { exclude[ln * 10 + j][sudoku[ln][col] - 1] = 1; } } int s_ln = (ln / TRI) * TRI, s_col = (col / TRI) * TRI; int next_ln = 0, next_col = 0; int cnt = 9; while(cnt--) { if(flag[s_ln][s_col] == 0) { exclude[s_ln * 10 + s_col][sudoku[ln][col] - 1] = 1; } get_next(s_ln, s_col, &next_ln, &next_col); s_ln = next_ln; s_col = next_col; } }

void set_left(int ln, int col)    得到(ln, col)可以放置的数字,最后,如果只有一个可以放入的数字,就放入这个数字。

void set_all_left()                   为所有元素调用set_left()。

void set_left(int ln, int col) { if(flag[ln][col] == 1) return; memset(left_digits[ln * 10 + col], 0, MAXN); char digit = 0; for(digit = 1; digit < 10; ++digit) { if(!in_square(ln, col, digit) && is_valid(ln, col, digit)) { left_digits[ln * 10 + col][strlen(left_digits[ln * 10 + col])] = digit + '0'; } } if(strlen(left_digits[ln * 10 + col]) == 1) { sudoku[ln][col] = left_digits[ln * 10 + col][0] - '0'; flag[ln][col] = 1; --empty_digit; } }

然后就可以利用set_all_exclude()和set_all_left()得到元素的排除集和余集(自己取的名字,呵呵)。

下面来看看,两种方式的结果:

对上面的数独执行set_all_exclude(),得到:

上面就是部分成员的排除集,当有8个元素时,剩下那个就是需要填入的值,执行10次排除法,就能够得到上述数独的解。

执行set_all_left(),得到:

上面是部分成员的余集,当只有一个元素时,就可以填入那个元素,如上图的[4][4]、[5][3]、[6][4]、[6][5]、[6][8]都可以直接填入了。执行5次唯余法,就可以得到上述数独的解。

下面给出上述数独的解:

数独问题是个很有意思的问题,而且数独还分为不同的难度级别,像上面这两种方法只能解决一些简单的数独问题,对于难的问题就不行了。当然,个人水平有限,从数据结构到代码可能写的不是很好,望各位批评指正!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3