随着数独解题算法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 ~