数独 ( 二 ) ——生成数独终局

数独 ( 二 ) ——生成数独终局

我项目的github链接 :https://github.com/gmj0301/gmj
请查看前面的博客,有修改!

要求

  1. 在命令行中使用 -c 参数加数字 N (1<=N<=1000000) 控制生成数独终局的数量
  2. 将生成的数独终局用一个文本文件 (假设名字叫 sudoku.txt) 的形式保存起来,每次生成的 txt 文件需要覆盖上次生成的 txt 文件,文件内的格式如下,数与数之间由空格分开,终局与终局之间空一行,行末无空格
  3. 程序在处理命令行参数时,不仅能处理格式正确的参数,还能够处理各种异常的情况
  4. 在生成数独终局时,左上角的第一个数为:(学号后两位相加)% 9 + 1

解题思路

  1. 根据要求(4),我的学号后两位是90,则左上角的第一个数为(9+0)% 9 + 1 = 1

  2. 根据要求(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");
	}
}

相关测试会在后面的博客更新。
因为一开始有些函数不完善,有进行了更改。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值