数独 ( 二 ) ——生成数独终局
我项目的github链接 :https://github.com/gmj0301/gmj
请查看前面的博客,有修改!
要求
- 在命令行中使用 -c 参数加数字 N (1<=N<=1000000) 控制生成数独终局的数量
- 将生成的数独终局用一个文本文件 (假设名字叫 sudoku.txt) 的形式保存起来,每次生成的 txt 文件需要覆盖上次生成的 txt 文件,文件内的格式如下,数与数之间由空格分开,终局与终局之间空一行,行末无空格
- 程序在处理命令行参数时,不仅能处理格式正确的参数,还能够处理各种异常的情况
- 在生成数独终局时,左上角的第一个数为:(学号后两位相加)% 9 + 1
解题思路
-
根据要求(4),我的学号后两位是90,则左上角的第一个数为(9+0)% 9 + 1 = 1
-
根据要求(1)可知,最多要生成 1000000 个不同的数独终局。
经过学习了解后,我发现数独终局的一个特点。
对于这个给定的数独终局,从第二行开始,每行分别是第一行左移 3,6,1,7,4,2,5,8 列的结果。而题目中要求我生成的数独终局左上角第一个数为 1,这样第一行有8个空可以随意填入 2 ~ 9 这 8 个数字。对于任意一个 2 ~ 9 的全排列,都可以通过上述方法生成一个数独终局。这样就有 8!= 40320 种终局。但是这远远不够,再想办法直接生成数独终局是很困难的且容易重复,最好的方法是在已生成的数独终局上进行变换产生新的终局。在网上了解学习后,总结了如下几种在已有一个数独终局的情况下,生成数独终局的方法。
(1)两个数字相互交换法
这个方法是选择两个数字,例如 1 和 9,在填 1 的地方填 9;在填 9 的地方填 1。但是因为已有的数独终局第一行是按照 2 ~ 9 的全排列,交换后可能恰好生成之前已有的终局。不能使用这种方法。(2)调整块
将前三行、中间三行、后三行作为三个块,任意交换其中两个块。但是左上角第一个数字为 1 导致第一行不能动,这样只能交换后两个块,一个数独终局只能产生一个新的数独终局,这样最终只有 80640 个数独终局,不够。同理可以将列作为块,但是因为已有的数独终局第一行是按照 2 ~ 9 的全排列,交换后可能恰好生成之前已有的终局。不能使用这种方法。(3)旋转矩阵
将矩阵旋转后,左上角第一个数字不再是 1,不能使用这种方法。(4)调整行或列
交换只发生在前三行,中间三行,最后三行,前三列,中间三列以及最后三列之间。而不能越界交换,比如第一行和第四行交换就是不允许的。
因为已有的数独终局第一行是按照 2 ~ 9 的全排列,交换列后可能恰好生成之前已有的终局。不能交换列。由于左上角不能动,我们只交换 4 ~ 6 中的任意两行或者 7 ~ 9 行中的任意两行,这样在已有的每种终局就可以变成 3! × 3! = 36种终局,一共1451520种终局,已经超过了 1000000 种,可以满足要求了。
设计函数
使用 C 语言进行编程。
经过分析,先通过全排列的方式生成第一行,经过转换生成数独终局;再通过交换行生成新的数独终局。因为交换行时,每一行内的数字的相对位置不变,只是行的顺序发生变化,所以在输出时按照新的输出顺序输出即可,不需要真的改变数独。
整体的设计实现过程如下:
函数设计如下:
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <string.h>
int shift[8] = { 3, 6, 1, 4, 7, 2, 5, 8 };
int c1[3] = { 3, 4, 5 };
int c2[3] = { 6, 7, 8 };
int snum1 = 1, snum2 = 1, flag_first = 0;
void produce(int sudo[9][9]); //第一行已知,通过变换得到完整的数独终局
void swap(int& a, int& b); //交换函数,交换a和b
bool nextPermutation(int* p, int n); //全排列函数
void jiaohan_1(int s1, int* c1); //中间4,5,6行的变换
void jiaohan_2(int s2, int* c2); //最后7,8,9行的变换
void init(int sudo[9][9], int p[8]); //初始化,规定左上角第一个为1,第一行为123456789
void output(int sudo[9][9], FILE* fp); //输出函数
void change(); //行变换的参数设置
void create(int N); //生成N个数独终局并写入文件
其中比较重要的函数的具体实现如下:
(1)全排列
由于第一位数字不动,后面 2 ~ 9 这 8 个数字进行全排列。
bool nextPermutation(int* p, int n)
{
int last = n - 1;
int i, j, k;
//从后向前查找,看有没有后面的数大于前面的数的情况,若有则停在后一个数的位置。
i = last;
while (i > 0 && p[i] < p[i - 1])
i--;
//若没有后面的数大于前面的数的情况,说明已经到了最后一个排列,返回false。
if (i == 0)
return false;
//从后查到i,查找大于p[i - 1]的最小的数,记入k
k = i;
for (j = last; j >= i; j--)
if (p[j] > p[i - 1] && p[j] < p[k])
k = j;
//交换p[k]和p[i - 1]
swap(p[k], p[i - 1]);
//倒置p[last]到p[i]
for (j = last, k = i; j > k; j--, k++)
swap(p[j], p[k]);
return true;
}
(2)生成数独终局
从第二行开始,每行分别是第一行左移 3,6,1,7,4,2,5,8 列的结果。
设置一个一维数组 shift 记录需要左移的列数,剩下每一行根据 shift 来判断当前位置填入的数字。
void produce(int sudo[9][9]) //第一行已知,通过变换得到完整的数独终局
{
for (int i = 1; i < 9; i++)
for (int j = 0; j < 9; j++)
sudo[i][j] = sudo[0][(j + shift[i - 1]) % 9];
//第i行是第一行左移shift[i]得到的 即sudo[i][j] = sudo[0][j右移shift[i]]
//j右移后可能超出9,所以要模9取余
}
(3)改变输出顺序
只交换 4 ~ 6 中的任意两行或者 7 ~ 9 行中的任意两行,这样在已有的每种终局就可以变成 36 种终局。
中间三行的顺序有:4,5,6;4,6,5;5,4,6;5,6,4;6,4,5;6,5,4 这六种。
最后三行的顺序有:7,8,9;7,9,8;8,7,9;8,9,7;9,7,8;9,8,7 这六种。
两者组合可以产生 36 种。只需要设置两个参数,一个表示中间三行的顺序,另一个表示后三行,通过组合即可表示。不需要真的改变数组。
void jiaohan_1(int s1, int* c1) //中间4,5,6行的变换
{
if (s1 == 1) { c1[0] = 3; c1[1] = 4; c1[2] = 5; }
if (s1 == 2) { c1[0] = 3; c1[1] = 5; c1[2] = 4; }
if (s1 == 3) { c1[0] = 4; c1[1] = 3; c1[2] = 5; }
if (s1 == 4) { c1[0] = 4; c1[1] = 5; c1[2] = 3; }
if (s1 == 5) { c1[0] = 5; c1[1] = 3; c1[2] = 4; }
if (s1 == 6) { c1[0] = 5; c1[1] = 4; c1[2] = 3; }
}
void jiaohan_2(int s2, int* c2) //最后7,8,9行的变换
{
if (s2 == 1) { c2[0] = 6; c2[1] = 7; c2[2] = 8; }
if (s2 == 2) { c2[0] = 6; c2[1] = 8; c2[2] = 7; }
if (s2 == 3) { c2[0] = 7; c2[1] = 6; c2[2] = 8; }
if (s2 == 4) { c2[0] = 7; c2[1] = 8; c2[2] = 6; }
if (s2 == 5) { c2[0] = 8; c2[1] = 6; c2[2] = 7; }
if (s2 == 6) { c2[0] = 8; c2[1] = 7; c2[2] = 6; }
}
输出时一个个往文件里面写入,代码如下:
void output(int sudo[9][9], FILE* fp) //输出函数
{
jiaohan_1(snum1, c1);
jiaohan_2(snum2, c2); //判断当前两个参数表示哪两种顺序
char s[18];
//输出数独
for (int i = 0; i < 9; i++)
{
int k = i;
if (k >= 3 && k <= 5) k = c1[k - 3]; //如果当前是中间三行,按照顺序输出
else if (k > 5) k = c2[k - 6]; //如果当前是中间三行,按照顺序输出
for (int j = 0; j <= 8; j++) {
int kt = j * 2;
s[kt] = sudo[k][j] + '0';
s[kt + 1] = ' ';
}
s[17] = '\0';
fputs(s, fp); //写入文件
fprintf(fp, "\n");
}
}
相关测试会在后面的博客更新。
因为一开始有些函数不完善,有进行了更改。