闲暇之余,产生另外一个想法:用Python生成数独题目。
经典数独是9*9的方阵。最简单的生成题目思路是按照从左至右,从上至下的顺序逐个把1~9的数字随机填入到数独的格子中。然后再随机抹去一定数量的格子即可。
当然,这里随机填入的时候,还要考虑到数独的规则:
1)同一行的数字不能重复
2)同一列的数字不能重复
3)同一个九宫格的数字不能重复
所以,在随机填入的时候,每次填入之前都要检查是否符合上面的条件。或者在随机生成数字的时候,就把所在格子的行、列、九宫格已经存在的数字排除在外,然后再生成随机数字。
我采取排除的方法生成随机数字,简单代码如下:
1)先定义获取一个二维矩阵行、列、九宫格的方法
importrandomimportnumpyasnp#其中sudo参数是numpy的二维矩阵#获取格子所在的行的全部格子defget_row(sudo,row):returnsudo[row,:]#获取格子所在的列的全部格子defget_col(sudo,col):returnsudo[:,col]#获取格子所在的九宫格的全部格子defget_block(sudo,row,col):row_start=row//3*3col_start=col//3*3returnsudo[row_start:row_start+3,col_start:col_start+3]
2)书写生成逻辑:遍历,排除已被使用的数字,随机填入
3)执行
我尝试执行,都遇到没有数字可用的错误
调整代码,打印发生错误时的数独
再次执行:
执行结果,第2行第9列没有数字可用。
我们看第2行已经几乎填满了数字,剩下4可填。而在这个九宫格,数字4已经被使用(第1行第8列)。所以无数字可用。
4)初步结论
后来,我又尝试了好几次,都是出现这种结果。仔细思考,得出以下结论:
以上面的例子为例,第二行在随机填写数字的时候,没有考虑到数字4在后面3列无法使用。除了数独基本3个限制条件外,其实这3个限制条件会导致数字之间产生一种纠缠关系。而前面简单的做法没有考虑到这种潜在的影响,所以无法生成一个完整的数独。
从数独的3个限制条件出发,3个限制条件都是不同的维度。而这些维度都有交叉关系,例如一个格子同时存在于某一行和某一列、某一九宫格。就像空间坐标轴,空间上的任意一点都有x、y、z等3个坐标轴。
感觉数独是一种高纬度的矩阵,我们看到的二维矩阵只是高纬度矩阵在二维上的投影。如果用这种简单随机方法生成数独的话,很难涉及到每个数字之间的纠缠关系,可能需要用更高维度的方式去处理,例如3维矩阵、4维矩阵。
后来,我尝试提高维度去解决问题,也遇到同样无法直接生成数独。可能我的数学知识还不够,不能用数学的方式表达数独。
在不断翻阅资料时,偶然发现一个规律:在已有的数独结果上,同一个九宫格内任意两个格子,调换其所在的行或者所在的列后的结果,还是一个有效的数独。
例如交换D1和E3两个格子所在的行:
数独3个限制条件都没有被破坏。进一步发现,其实只要在九宫格范围里面的任意两行或两列交换都可以。
例如交换D行和E行,D行和E行都在九宫格的范围内。而A行和D行就不能交换。
如此一来,我们只要用一个简单的数独(这个术语叫基本盘),随机多次交换不会破坏数独完整性的行或列,形成最终结果(术语叫终盘);最后再随机抹去一些数据即可变成数独题目。
1)生成基本盘
生成基本盘的代码如下(延用上面的get_row、get_col、get_block方法):
defcreate_base_sudo():#9*9的二维矩阵,每个格子默认值为0sudo=np.zeros((9,9),dtype=int)#随机生成起始的基数(1~9)num=random.randrange(9)+1#遍历从左到右,从上到下逐个遍历forrow_indexinrange(9):forcol_indexinrange(9):#获取该格子对应的行、列、九宫格sudo_row=get_row(sudo,row_index)sudo_col=get_col(sudo,col_index)sudo_block=get_block(sudo,row_index,col_index)#如果该数字已经存在于对应的行、列、九宫格#则继续判断下一个候选数字,直到没有重复whilenuminsudo_rowornuminsudo_colornuminsudo_block:num=num%9+1#赋值sudo[row_index,col_index]=numnum=num%9+1returnsudo
这里用了求余的方法,把num限制在1到9之间。执行该方法,可以得到如下类似的基本盘:
2)随机交换,得到终盘
然后我们再多次随机交换行列,得到终盘:
#随机交换n次defrandom_sudo(sudo,times):for_inrange(times):#随机交换两行rand_row_base=random.randrange(3)*3#从0,3,6随机取一个rand_rows=random.sample(range(3),2)#从0,1,2中随机取两个数row_1=rand_row_base+rand_rows[0]row_2=rand_row_base+rand_rows[1]sudo[[row_1,row_2],:]=sudo[[row_2,row_1],:]#随机交换两列rand_col_base=random.randrange(3)*3rand_cols=random.sample(range(3),2)col_1=rand_col_base+rand_cols[0]col_2=rand_col_base+rand_cols[1]sudo[:,[col_1,col_2]]=sudo[:,[col_2,col_1]]
我随机交换50次,最终得到一个和基本盘差别很大的终盘:
3)随机抹去数字,得到数独题目
这里如果抹去数字太多,会导致题目很难解,或者容易有多个解;如果抹去数字太少,数独题目就会变得很简单。
经过一番测试,大概最多清除64个数字,最少清除14个数字,得到的题目可解。代码如下:
defget_sudo_subject(sudo,del_nums):#最少要保留17个数字,题目才可解。一共81个数字,则最多可以擦除81-17=64。max_clear_count=64#最少也要擦除14个min_clear_count=14ifdel_nums
下面是我随机抹去30个数字的效果:
4)整合代码
这里除了整合全部代码之外,我还划分了难度等级,抹去数字越多则越难。我将其划分为5个级别:入门、初级、熟练、精通、大神。