生成数独终局
数学原理分析
数独是一种逻辑游戏。它要求满足每一行、每一列、每一个九宫格(3×3)内的数字均含1-9,不重复。
生成数独终局的方案有很多,比如随机数先分配第一行位置然后利用回溯法解数独,在这个项目要求不重复,利用随机数的方案是危险的。我们通过观察数独的特性,用基本组有规律的构造更多的数独终局是一个有效方案。
上图是一种数独终局,我们根据数独要求,1、2、3行任意互换,仍然满足各行、各列、各九宫格1~9出现1次仅1次。对4、5、6行同理,对7、8、9行也同理。对列也如此。那么实际上由一个数独终局可以生成46656种数独终局,但由于题目中要求第一个元素是(学号后两位相加)%9+1(我的学号是1120161918,计算结果为1),因此由一个数独终局,只能生成5184种数独终局,理论上提供193个不同构的数独终局便能够达到题目要求。
利用高中数学竞赛的数论基础,进一步发现对于任意一个第一行,第二行的元素是第一行元素下标+6,第三行+3,第四行+8,第五行+5,第六行+2,第七行+7,第八行+4,第九行+1,得到的9×9局面满足数独要求。实际上此处原理是数论同余的原理,每三行的位移量属于mod 3 的同余类 (mod 3 的最小非负完全剩余系就是0,1,2)。
用这个原理可以解释为什么1、2、3互换得到的结果任然是数独终局。实际上只需要三行位移量在同一mod 3 同余类的不同值就可以了。这样我们还能得到更多的数独终局。
只需要构造第一行:第一个数字是学号要求(我的为:1),后面8个数字做全排列,然后利用剩余类构造剩下8行。由于第一行不动(以满足学号要求),可以由一个得到 2×6×6=72种,而8!=40320,一共得到2903040种数独终局,已经超过1e6了。
程序实现
如果在生成的时候利用循环实现第一行全排列赋值是相当费时间的,当然直接打表40320种会使程序变得相当长,可以主函数一开始就计算全排列存起来,方便后续多次调用,这样的效率是比较高的。
递归求取全排列实现如下:
int init[9] = { 1, 2, 3, 4, 5, 6, 7, 8, 9};
/// <summary> 递归求取全排列 </summary>
void permutation(int offset)
{
if(offset == 8) {
for(int i=0;i<9;i++) {
origin[count][i]=init[i];
}
count++;
return;
}
for(int i=offset; i<9; i++) {
swap(init[i], init[offset]);
permutation(offset + 1);
swap(init[i], init[offset]);
}
}
保存好全排列后可以先将输出顺序用数组保存,这样实现便很简单,大量减少循环结构,对软件测试是很有用的。
int shift[9] = { 0, 6, 3, 8, 5, 2, 7, 4, 1 };
int generate_1[2][3] = { {0,1,2}, {0,2,1} };
int generate_2[6][3] = { {3,4,5}, {3,5,4}, {4,3,5}, {4,5,3}, {5,3,4}, {5,4,3} };
int generate_3[6][3] = { {6,7,8}, {6,8,7}, {7,6,8}, {7,8,6}, {8,6,7}, {8,7,6} };
完整的实现代码后续将会展示。