产生数独迷题

随着数独解题算法DLX的完成,产生一个数独迷题的任务就顺理成章地完成了。当然,基本的思想还是先生成终盘,然后对填好的数独题进行挖洞,每挖一个洞,就要考虑一下挖去这个洞会不会导致迷题将有多个解。假如会,这个洞就是不能挖的。事实上当一个洞被证实挖去之后会导致多解后,这个洞就注定不能被挖了。也就是说,这个算法的复杂度应该是81*O(f(n)),其中f(n)是用于解一个数独的时间复杂度。

《编程之美》里面提供了一种生成终盘的方法,即在数独中间的块(B5)上随机填入1~9,然后利用这个块行列变换到旁边的块(B4跟B6、B2跟B8)上,同样的做法将在B4、B6上填充B1跟B7,B3跟B9,从而得到一个合法的数独终盘。这种方法将可以产生 9! 种数独终盘。前面的文章我们已经讨论过了,总共存在 6.67*10^21 种数独终盘,相比之下 9! 未免也太少了吧?于是我决定另谋思路。

现在我们已经得到了解数独的方法,我的思路是,对角线上的三个块(B1,B4,B7)是可以随便填而不会造成不合法行为的,把这三个块随机填完后,再利用DLX进行求解,就可以得到一个终盘了。虽然我们知道对于同一个初始局面(B1、B4、B7被随机填好),DLX是可以得到很多个解的,但是由于算法本身的确定性,因此产生终盘的顺序也是固定的,也就是说,这种做法下,我们只能得到(9!)^3,大概是 10^14 种数独终盘。我因此产生了一个想法,把算法的结果弄得有点不是那么确定。我们知道,当我们确定性地选了一列拥有最少元素的列后,算法的思路是深搜该列下的每一行,当通过该行最终得到了一个解后,就返回结果了。为了使算法具有不确定性,我决定在选择行的时候,带上一点随机性质:随机选一个行来进行深搜。这样,我们理论上就能得到所有可能的数独了。

接下来便是挖洞。前面我们大概提到了,当我们尝试挖一个洞而该洞使数独迷题拥有多解时,该洞就不能被挖了,无论我们后面挖了哪些格子。因此,每个格子都只最多会被尝试挖一次。这样的话我们可以首先一个[0,81)的随机序列,然后按照该序列的顺序对终盘进行挖洞,直到所有的格子都被挖过,或者未填的空格已经满足要求后算法才停止。如何判断该挖去该洞后会否使数独迷题拥有多个解呢?那就是,选中某个格子后,假设该格中原先的值为i,那么我们就分别用[1,9]中除了i以外的数替换i,再对数独进行求解,如果能得到解,说明挖去该洞会使数独存在不止一个解。算法虽是这么描述,在真正实现的时候还是可以做一定的优化的。不过我通过实验发现,一般最多只能挖去59个洞,也就是说有得到的数独迷题最少有22个提示,看资料说目前为止数独迷题有唯一解的最少的提示可以达到17个,大概在选格子的顺序上是需要一点处理的。

具体实现的时候,我使用了上篇文章最后提到的那个只有30几行的dlx实现版本,原因是这样进行随机选行方便了很多,呵呵。我在代码里面做了大量的注释。不过为了保证copy时不会出现错误,我都用的英语注释。

from itertools import product
from random import shuffle
import timeit

N_Cell = 3
N = 9
GRIDS = 81

def exact_cover( X, Y ):
	X = {j:set() for j in X}
	for i, row in Y.items():
		for j in row:
			X[j].add(i)
	return X, Y

def select( X, Y, r ):
	cols = []
	for j in Y[r]:
		for i in X[j]:
			for k in Y[i]:
				if k!=j:
					X[k].remove(i)
		cols.append(X.pop(j))
	return cols

def deselect( X, Y, r, cols ):
	for j in reversed(Y[r]):
		X[j]=cols.pop()
		for i in X[j]:
			for k in Y[i]:
				if k!=j:
					X[k].add(i)

def solve( X, Y, solution=[], isRandom=False ):
	if not X:
		return list(solution)
	else:
		c = min(X, key=lambda c:len(X[c]))
		rows = list(X[c])
		#shuffling the rows results in picking a row in random
		if isRandom: 
			shuffle(rows)
		for r in rows:
			solution.append(r)
			cols = select(X, Y, r)
			if solve(X, Y, solution):
				#we don't use yield anymore, 
				#instead, when a solution found, return it
				return solution
			deselect( X, Y, r, cols )
			solution.pop()
#they are contributed mostly by: http://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html

#helper function 
#given index of row, colunm and the ans in the corresponding cell
#return colunms that should be one in this context
def get_ones( r, c, n ):
	b = (r//N_Cell)*N_Cell + (c//N_Cell)
	ones = [("rc", (r, c)), ("rn", (r, n)),
			("cn", (c, n)), ("bn", (b, n))]
	return ones

def puzzle_generator():
	#identifier for columns are static, we have our index from 0
	#when("rc",(r,c)) has a one, it means sudoku grid (r,c) has been filled
	#when("rn",(r,n)) has a one, it means sudoku line r already has a number n
	#when("cn",(c,n)) has a one, it means sudoku column c already has a number n
	#when("bn",(b,n)) has a one, it means sudoku block b already has a number n
	X_init=([("rc", rc) for rc in product(range(N), range(N))]+
			[("rn", rn) for rn in product(range(N), range(1, N+1))]+
			[("cn", cn) for cn in product(range(N), range(1, N+1))]+
			[("bn", bn) for bn in product(range(N), range(1, N+1))])

	#Y and Y_final records lines (r,c,n) has which columns as one
	#where Y is for solving the randomly generated puzzle
	#while Y_final is for recording the final answer,
	#if there is a Y_final[(r,c,n)], that means grid[r][c]has been filled with n
	Y = dict()
	Y_final = dict()	

	#puzzle we are going to generate, initially all 0
	puzzle = [ ([0]*N) for i in range(N) ]

	for i in range(N_Cell):
		init = range(1, N+1)
		#generate a random sequence from 1~9 and fill them one by one to the blocks
		#lines are added to Y and Y_final in correspondance, prepare for diggin cells
		shuffle(init)
		for j in range(N_Cell):
			for k in range(N_Cell):
				r = i*N_Cell+j
				c = i*N_Cell+k
				n = init[j*N_Cell+k]
				puzzle[r][c]=n
				Y[(r,c,n)]=list(get_ones(r,c,n))
				Y_final[(r,c,n)] = list(get_ones(r,c,n))

		#other unfilled cells are 0, there are more than one possibilities for them
		#which means we have Y[(r,c,i)](i=1~9)
		for j in range(N_Cell):
			for k in range(2*N_Cell):
				r = (6+i*N_Cell+j)%N
				c = (i*N_Cell+k)%N
				for n in range( 1, N+1 ):
					Y[(r,c,n)]=list(get_ones(r, c, n))

	#convert it to a exact_cover problem and solve it
	#the final answers are added to Y_final
	X, Y = exact_cover(X_init, Y)
	solution = solve(X, Y, isRandom=True)
	for (r, c, n) in solution:
		puzzle[r][c]=n
		Y_final[(r,c,n)]=list(get_ones(r, c, n))
	
	#begin digging, we have no investigation on how many cells should be digged for a specific difficulty
	#so here we made it 60 in temporary, that's, we have 21 hints
	#but running result shows that we can hardly have 60 cells digged successfully, most of the time 50+
	empty = 60
	done = 0
	tries = 0
	#dig the cells in a random order
	seq = range(GRIDS)
	shuffle(seq)
	while done<empty and tries<GRIDS:
		#main idea: try each cell(r,c,n) where cell(r,c) is filled with n
		#pop (r,c,n) from Y_final, replace it with other (r,c,i)(i!=n)
		r, c = divmod(seq[tries], N)
		tries += 1
		n = puzzle[r][c]
		for i in range(1, N+1):
			Y_final[(r,c,i)]=get_ones(r,c,i)
		Y_final.pop((r,c,n))
		#this is a new exact_cover problem
		#we are replace the initial n in cell(r,c) with other i
		#to see if there is an answer for it
		X,Y_final=exact_cover(X_init, Y_final)
		#if not, that means we can't get an answer from it,
		#we can safely delete the cell, that is, puzzle[r][c]=0
		if( not solve(X,Y_final) ):
			puzzle[r][c]=0
			done += 1
		#if yes, that means this cell can be filled with other number and 
		#still we get a legal sudoku, the cell can't be deleted
		#so Y_final[(r,c,i)] should be pop 
		else:
			for i in range(1,n)+range(n+1, N+1):
				Y_final.pop((r,c,i))
		#finally, the initially deleted line, (r,c,n) should be push in 
		Y_final[(r,c,n)]=get_ones(r,c,n)
	print 'empty:', done
	return puzzle

if __name__ == '__main__':

	puzzle = puzzle_generator()
	for row in range(len(puzzle)):
		print puzzle[row]


好了,接下来我要去研究一下pygame了,先把基础界面做出来,再考虑数独的难度、人工解法及提示。come on ~

你可能感兴趣的:(产生数独迷题)