C语言数独游戏暴力求解生成数独终盘

本文详细介绍了使用暴力求解法生成数独终盘的过程,包括生成初始17个数字的数独盘面,通过排除法确定每个空格可能的填充数,并递归填数,遇到错误则回溯。算法核心涉及solve数组和num数组的维护,展示了从随机初盘到正确终盘的每一步操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

这篇文章介绍的生成数独终盘的方法是暴力求解,就是通过将数独初盘生成数独终盘过程中的所有可能性全部列出,直到生成数独终盘

实现思路

具体的实现思路就是先生成一个只有17个数字的数独初盘,依次排除每个未填的格子中不能再填的数字,并计算出每个未填的格子还能填几个数,找到能填的数最少的未填空,找到之后,判断该空能填的数有几个,若只有一个则直接填入,若有一个以上则先将当前的状态保存,将能填的数当中最小的那个填入该空,之后再将该数从该空所在的列、行、宫中不能再填这个数的未填空排除掉。之后继续上面的步骤,若出现错误,则读取之前保存的状态,重新填数。
下面这张图片是大致的一个流程图:
在这里插入图片描述

过程示意图

首先是生成一个数独初盘,数独初盘生成的思路是随机生成一个行号,一个列号,一个值,利用一个布尔函数判断该数在随机生成的行、列和所在的宫中是否已被填写,有则重新生成,没有则存储数独的数组当中,下面这张图片是随机生成的一个数独初盘
在这里插入图片描述
接下来说一下暴力求解的具体实现,首先需要构建一个solve数组和一个num数组,solve数组是用来存储每个未填空还可以填哪些数,solve数组储存还能填哪些数的思路是,利用一串有九位的二进制数表示的,能填的数用1表示,不能填的数用0表示,之后再将二进制转换为是进制存储到solve数组中,例如一个未填空可以填的数为1,4,6,7,则二进制序列为001101001,转换为十进制就是1×2∧0+0×2∧1+0×2∧2+1×2∧3+0×2∧4+1×2∧5+1×2∧6+0×2∧7+0×2∧8=1051\times2\land0+0\times2\land1+0\times2\land2+1\times2\land3+0\times2\land4+1\times2\land5+1\times2\land6+0\times2\land7+0\times2\land8=1051×20+0×21+0×22+1×23+0×24+1×25+1×26+0×27+0×28=105,将105储存到solve数组当中,需要填入数时再将105转换为二进制选择数填入未填空中,num数组是用来存储每个未填空还可以填几个数 ,num数组储存还能填几个数的思路是利用solve数组来进行计算,就是将solve数组中的十进制数转换成二进制数,再判断二进制数中有几个1,下面这张图片是上面生成的初盘及还未填写的刚初始化的solve数组和num数组
在这里插入图片描述
下面这张图片是第一个未填空进行排除已填数和计算未填数个数的图片,下图中sudoku中标黄的部分是判断第一个空还有哪些数可以填的判断范围,根据判断第一个空不能填的数为2、5、6、8,则还可以填的数为1、3、4、7、9这5个数,那么二进制序列为101001101,转化为十进制为1×2∧0+0×2∧1+1×2∧2+1×2∧3+0×2∧4+0×2∧5+1×2∧6+0×2∧7+1×2∧8=3331\times2\land0+0\times2\land1+1\times2\land2+1\times2\land3+0\times2\land4+0\times2\land5+1\times2\land6+0\times2\land7+1\times2\land8=3331×20+0×21+1×22+1×23+0×24+0×25+1×26+0×27+1×28=333,所以在solve数组的第一个空填入333,num数组的第一个空填入5
在这里插入图片描述
下面这张图是根据上面的处理方法对所有未填空进行排除和计算之后的结果
在这里插入图片描述
接下来就是开始进行填数操作,首先是将整个num数组进行遍历,找出个数最少的未填空开始填,若该空可填的个数大于1,将储存当前状态,就是将当前sudoku、solve和num储存的数据存到另一个用于储存这部分信息的数组当中,再进行填数操作,下面这张图片中标黄的部分就是当前需要操作的空
在这里插入图片描述
之后就是进行填数,先将这一空中solve从十进制转换成二进制找出要填入该空的数,十进制转换二进制的方法如下图所示,以41为例,就是将41求余,余数写到右边,用41减去余数除以2之后得到的商用于下一步计算,计算方法相同,直到商为1,二进制数就是右边的余数从下往上读写,没有满九位的话高位写0,图片最下面就是41的二进制表示(下面的图片是我用PS画的画得不是很好还请多多担待,嘻嘻嘻!)
在这里插入图片描述

上面未填空的二进制序列为000001001,可填的数为1和4,一般是先填最小的数,这一空最小的数是1,所以将1填入sudoku和solve中对应的位置,同时num中对应的数变为0,结果就是下图中标黄的部分
在这里插入图片描述
填完数之后,就需要根据该数对其他空进行处理,对原本可以填该数的空进行清除操作,将该数从这些空中移除,下图中标黄的部分就是需要进行操作的范围,但是并不是所有的空都需要进行处理,所以在处理前就需要进行筛选,找出需要进行操作的空。
在这里插入图片描述
下图中标蓝的空就是要需要进行操作的空,因为这些空的可以填的未填数中都有1,所以需要进行处理,就是对solve中对应的空,将二进制数的对应位置的1变为0,转换为十进制问题也就是减去相应的值,需要减去的值就是2的对应的数减1的次方,该空就是减去2的0次方,也就是减去1,num中对应的空也都需要减去1,因为减少了一个可以填的未填数
在这里插入图片描述
下图就是经过处理之后的结果,如果未填空可以填写的数只有一个的话,需要进行的操作与多个数的相比只是少了储存当前状态这一步其他是一样的,就不再重复
在这里插入图片描述
接下来说明一下出现无解空的情况,下图是出现无解空前的状态,也就是在填数前储存的状态
在这里插入图片描述
需要进行处理的未填空是下图中标黄的部分
在这里插入图片描述
下图中蓝色部分是填写完之后,查找出的需要进行清除操作的部分
在这里插入图片描述
下图是经过处理之后的结果,出现了无解空,也就是下图中标红的部分,出现无解空,就说明之前在填写某个有多个数的未填空时,填写的数是错误的,所以需要重新进行填写,接下来的操作就是读取之前储存的状态进行重新填写
在这里插入图片描述
下图就是储存的填写错误的数前的状态,标黄的部分就是填写错误的空,从上面的图片可以看出,这一空之前填写的是5,那么在这一步就需要将5从solve中排除,也就是减去2^4,num减去1
在这里插入图片描述
处理之后得到的结果就是下图中标黄的部分,之后的操作就和前面填写的操作一样这里就不再重复
在这里插入图片描述
下图是最后经过处理之后得到的数独终盘,solve数组处理结束之后的值和sudoku数组是一样的,num数组则全部为0,所以这里就没有列出
在这里插入图片描述

完整代码

本篇文章提供的代码的编译环境是visual studio 2019,需要运行下面代码时,打开visual studio 2019新建一个项目,新建一个源文件之后,只需要将代码复制到文件里运行就可以了!
如果觉得不错的话,可以点个赞吗,万分感谢!

#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
#include<math.h>
#include<time.h>
#include <windows.h>
#include<string.h>
#include<stdbool.h>
int sudoku[81];
bool isRight(int* flag, int x, int y, int val) {//判断填入的数是否是正确的
	for (int i = 0; i < 324; i++)
		if (flag[(x - 1) * 9 + (y - 1)] == 0 &&
			flag[81 + (x - 1) * 9 + val - 1] == 0 &&
			flag[162 + (y - 1) * 9 + val - 1] == 0 &&
			flag[243 + ((x - 1) / 3 * 3 + (y - 1) / 3 + 1) * 9 + val - 1] == 0)
			return true;
	return false;
}
int* create_sudoku() {
	for (int j = 0; j < 81; j++)
		sudoku[j] = 0;//初始化
	int flag[324] = { 0 };
	int x, y, val;//行号、列号、值
	int t = 11;
	int i = 0;
	while (i < 17) {//一个数独中有17个数及以上时有唯一解
		x = rand() % 9 + 1;
		y = rand() % 9 + 1;
		val = rand() % 9 + 1;
		if (isRight(flag, x, y, val)) {
			i++;
			sudoku[(x - 1) * 9 + (y - 1)] = val;
			flag[(x - 1) * 9 + (y - 1)] = 1;
			flag[81 + (x - 1) * 9 + val - 1] = 1;
			flag[162 + (y - 1) * 9 + val - 1] = 1;
			flag[243 + ((x - 1) / 3 * 3 + (y - 1) / 3 + 1) * 9 + val - 1] = 1;
		}
	}
	return sudoku;
}

void print(int* answer) {//打印数独
	printf("┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓\n");
	for (int i = 0; i < 81; i++) {
		if (answer[i] == 0)
			printf("┃  ");
		else
			printf("┃ %d", answer[i]);
		if (i == 80) {
			printf("┃  ");
			printf("\n");
			printf("┗━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┛\n");
		}
		else if ((i + 1) % 9 == 0) {
			printf("┃  ");
			printf("\n");
			printf("┣━━╋━━╋━━╋━━╋━━╋━━╋━━╋━━╋━━┫\n");
		}
	}
}

int* solve_sudoku() {
	int position[30][246], solve[81], x = 0, y = 0, z = 0, j = 0, k = 0, x1 = 0, y1 = 0, time = 0;  //position用于储存数独状态
	//solve用于计算每个空可以填那些数
	int sign[9], num[81];  //sign用于记录未填空不能填那些数,num用于统计未填空中可以填的数的个数
	for (int i = 0; i < 9; i++)
		sign[i] = 1;
	for (int i = 0; i < 81; i++) {
		solve[i] = 511;
		num[i] = 0;
	}
	for (int i = 0; i < 30; i++)
		for (int o = 0; o < 81; o++)
			position[i][o] = 0;
	for (int i = 0; i < 81; i++) {
		if (sudoku[i] != 0)
			solve[i] = sudoku[i];
	}
	for (int i = 0; i < 81; i++) {  //这一for循环用于排除未填空中不能填的数
		if (sudoku[i]==0){
			x = floor(i / 9);  //x表示未填空所在的行,floor是向下取整的函数
			y = i % 9;  //y表示未填空所在的列
			x1 = floor(x / 3);  //x1是用来计算该空所在的宫的最小行
			y1 = floor(y / 3);  //y1是用来计算该空所在的宫的最小列
			for (int o = 0; o < 9; o++) {
				int ox = floor(o / 3), oy = o % 3;
				if (sudoku[x * 9 + o] != 0) 
					sign[sudoku[x * 9 + o] - 1] = 0;
				if (sudoku[y + o * 9] != 0)
					sign[sudoku[y + o * 9] - 1] = 0;
				if (sudoku[(x1 * 3 + ox) * 9 + y1 * 3 + oy] != 0)
					sign[sudoku[(x1 * 3 + ox) * 9 + y1 * 3 + oy] - 1] = 0;
			}
			for (int u = 0; u < 9; u++) {  //这一for循环用于将不能填的数进行清除
				if (sign[u] == 0) {
					solve[i] = solve[i] - pow(2, u);
					sign[u] = 1;
				}
			}
		}
	}//
	for (int i = 0; i < 81; i++) {  // 这一for循环用于计算未填空可填数的个数
		if (sudoku[i] == 0) {
			int j = 0, l = 0, f = solve[i];
			do {
				j = f % 2;
				if (j == 1)
					l++;
				f = floor(f / 2);
			} while (f != 0);
			num[i] = l;
		}
	}
	printf("solve:\n");
	for (int p = 0; p < 81; p++) {
		printf("%d\t", solve[p]);
		if (p % 9 == 8)
			printf("\n");
	}
	printf("\n");
	printf("num:\n");
	for (int p = 0; p < 81; p++) {
		printf("%d\t", num[p]);
		if (p % 9 == 8)
			printf("\n");
	}
B:int f = 0, f1 = 9;
	for (int i = 0; i < 81; i++) {  //这一for循环用于找到可填数最少的未填空
		if (num[i] < f1 && num[i] != 0) {
			f = i;
			f1 = num[i];
		}
		if (sudoku[i] == 0 && num[i] == 0)  //这一语句用于判断是否有无解空
			goto C;
	}  
	if (f == 0 && num[f] == 0) {  //这一for判断是否还有无解空或者填写错误的空
		for (int o = 0; o < 81; o++) {
			if (solve[o] != sudoku[o])
				goto C;
		}
		goto D;
	}
	if (num[f] == 1) {
		num[f] = 0;
		j = solve[f];
		k = 0;
		do {
			k++;
			z = j % 2;
			j = floor(j / 2);
		} while (z != 1);  /*这一do-while循环是用于找未填数中最小的那一个,
		由于二进制和十进制之间的转换并没有什么较为简便的方法所以这里用的就是硬算的方法,
		j表示当前可填数的二进制表示的十进制数,z表示余数,k表示要填的数,当z为1时说明当前的k表示的数是可以填入该空中的*/
		sudoku[f] = k;
		solve[f] = k;
		goto E;
		printf("f=%d\n", f);
		printf("N sudoku:\n");
		print(sudoku);
		printf("\n");
		printf("N solve:\n");
		for (int p = 0; p < 81; p++) {
			printf("%d\t", solve[p]);
			if (p % 9 == 8)
				printf("\n");
		}
		printf("\n");
		printf("N num:\n");
		for (int p = 0; p < 81; p++) {
			printf("%d\t", solve[p]);
			if (p % 9 == 8)
				printf("\n");
		}
		printf("\n");
		goto B;
	}
	if (num[f] > 1) {
		for (int i = 0; i < 81; i++) {  //这一for循环用于储存当前状态
			position[time][i] = solve[i];
			position[time][81 + i] = sudoku[i];
			position[time][162 + i] = num[i];
		}
		k = 0;
		j = solve[f];
		do {
			z = j % 2;
			k++;
			j = floor(j / 2);
		} while (z != 1);
		position[time][243] = f;
		position[time][244] = k;
		position[time][245] = num[f];
		time++;
		num[f] = 0;
		sudoku[f] = k;
		solve[f] = k;
		E:x = floor(f / 9);
		y = f % 9;
		x1 = floor(x / 3);
		y1 = floor(y / 3);
		for (int o = 0; o < 9; o++) {  //这一for循环用于将其他包含该数的未填空中的该数删除
			int ox = floor(o / 3), oy = o % 3;
			if (sudoku[x * 9 + o] == 0 && x * 9 + o != f) {
				int h = 0;
				j = solve[x * 9 + o];
				do {
					h++;
					z = j % 2;
					j = floor(j / 2);
				} while (h != k);
				if (z == 1) {
					solve[x * 9 + o] = solve[x * 9 + o] - pow(2, k - 1);
					num[x * 9 + o]--;
				}
			}
			if (sudoku[y + o * 9] == 0 && y + o * 9 != f) {
				int h = 0;
				j = solve[y + o * 9];
				do {
					h++;
					z = j % 2;
					j = floor(j / 2);
				} while (h != k);
				if (z == 1) {
					solve[y + o * 9] = solve[y + o * 9] - pow(2, k - 1);
					num[y + o * 9]--;
				}
			}
			if (sudoku[(x1 * 3 + ox) * 9 + y1 * 3 + oy] == 0 && x1 * 3 + ox != x && y1 * 3 + oy != y) {
				int h = 0;
				j = solve[(x1 * 3 + ox) * 9 + y1 * 3 + oy];
				do {
					h++;
					z = j % 2;
					j = floor(j / 2);
				} while (h != k);
				if (z == 1) {
					solve[(x1 * 3 + ox) * 9 + y1 * 3 + oy] = solve[(x1 * 3 + ox) * 9 + y1 * 3 + oy] - pow(2, k - 1);
					num[(x1 * 3 + ox) * 9 + y1 * 3 + oy]--;
				}
			}
		}
		printf("f=%d\n", f);
		printf("B sudoku:\n");
		print(sudoku);
		printf("\n");
		printf("B solve:\n");
		for (int p = 0; p < 81; p++) {
			printf("%d\t", solve[p]);
			if (p % 9 == 8)
				printf("\n");
		}
		printf("\n");
		printf("B num:\n");
		for (int p = 0; p < 81; p++) {
			printf("%d\t", num[p]);
			if (p % 9 == 8)
				printf("\n");
		}
		printf("\n");
		goto B;
	}
C:time--;
	for (int i = 0; i < 81; i++) {  //这一for循环用于读取之前保存的状态
		solve[i] = position[time][i];
		sudoku[i] = position[time][81+i];
		num[i] = position[time][162 + i];
	}
	f = position[time][243];
	k = position[time][244];
	solve[f] = solve[f] - pow(2, k - 1);  //这一步是为了将之前错的数删除
	num[f] = position[time][245] - 1;
	printf("f=%d\n", f);
	printf("C sudoku:\n");
	print(sudoku);
	printf("\n");
	printf("C solve:\n");
	for (int p = 0; p < 81; p++) {
		printf("%d\t", solve[p]);
		if (p % 9 == 8)
			printf("\n");
	}
	printf("\n");
	printf("C num:\n");
	for (int p = 0; p < 81; p++) {
		printf("%d\t", num[p]);
		if (p % 9 == 8)
			printf("\n");
	}
	printf("\n");
	goto B;
	D:return solve;
}

int main()
{
	create_sudoku();
	print(sudoku);
	solve_sudoku();
	print(sudoku);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿礼服的球

觉得不错的话,请我喝瓶肥宅水吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值