您的位置:首页 > 编程语言 > Python开发

Yet Another Sudoku Solver in Python

2012-08-31 19:56 459 查看
#coding:utf8
import itertools

'''
前面一堆Exception是为了在数独无法进行下去的时候直接跳出来的

Number_Counter 是用来查找唯一数的

Candidate_Counter 是用来查找 N链数的'''

class MyException(BaseException):
error_message = ""

def __init__(self):
super(MyException,self).__init__(self.error_message)

class DeepException(BaseException):
def __init__(self,Max_Recursion_Depth,(i,j,v)):
self.error_message = "Error raised when trying to guess (%d, %d) with %d"%(i, j, v)
self.error_message += "\n\t\t reach Max_Recursion_Depth which is %d"%Max_Recursion_Depth
self.error_message += '\n\t\t change it into a bigger one to avoid this error'
super(DeepException,self).__init__()

class FillingException(MyException):
error_message = "Error raised when trying to fill a number"
def __init__(self,(i,j,v)):
self.error_message = "Error raised when trying to fill (%d, %d) with %d"%(i, j, v)
super(FillingException,self).__init__()

class NoNumberSuitEx(MyException):
def __init__(self,i,j):
self.error_message = "Error raised when trying to guess (%d, %d)"%(i, j)
self.error_message += "\n\t\tNot any number suits here"
super(NoNumberSuitEx,self).__init__()

class WrongSudokuEx(MyException):
def __init__(self,i,fun,numbers):
self.error_message = "Error raised because this sudoku is wrong"
self.error_message += "\n\t\tSee, all numbers in %dth %s are following"%(i,fun.__name__)
self.error_message += '\n\t\t%s'%str(numbers)
self.error_message += "\n\t\tThis may result from wrong guess before"
super(WrongSudokuEx,self).__init__()

def disable(fun):
'''装饰器,用来取消某些函数的作用
调试的时候可能希望暂时去掉某些搜索方法'''
def wrapper(*argv):
return set()
return wrapper

class Number_Counter(dict):
def __init__(self):
for i in range(1,10):
self[str(i)] = []

def extend(self,index,values):
if values:
for v in values:
self[str(v)].append(index)

def find_only(self):
for key,item in self.items():
if len(item) == 1:
return item[0],eval(key)
else:
return None,None

def show(self):
for key,item in self.items():
print key,":",items

class Candidate_Counter(dict):
def __init__(self,List = None):
super(Candidate_Counter,self).__init__()
if List:
self.extend(List)

def extend(self, List):
if List:
for i, j, candidate in List:
candidate = list(candidate)
candidate.sort()
candidate = [str(c) for c in candidate]
candidate = ''.join(candidate)
if candidate in self:
self[candidate].append((i,j))
else:
self[candidate] = [(i,j)]

def links(self):
for key,items in self.items():
if len(key) == len(items) > 1:
#print key,items
yield items

class Index(dict):
'''把一些简单的约束封装到类Index里面处理
其中比较重要的罗列如下'''

#所有的索引号
all = set((i,j) for i in range(9) for j in range(9))

def row(self,*argv):
'''某一行的所有索引号
若输入值是单个值i,则返回第i行所有索引号
若输入值是两个值i,j,则返回(i,j)所在的行的所有索引号'''
if len(argv) == 1:
i = argv[0]
elif len(argv) == 2:
i = argv[0]
else:
raise BaseException("传入row的数据类型错误")
return set((i, j) for j in range(9))

def column(self,*argv):
if len(argv) == 1:
j = argv[0]
elif len(argv) == 2:
j = argv[1]
else:
raise BaseException("传入column的数据类型错误")
return set((i, j) for i in range(9))

def box(self,*argv):
if len(argv) == 2:
i, j = argv
x, y = i/3*3, j/3*3
elif len(argv) == 1:
i = argv[0]
x, y = i%3*3, i/3*3
else:
raise BaseException("传入box的数据类型错误")
return set((x+m, y+n) for m in range(3) for n in range(3))

def candidates_in(self,index_range):
assert index_range
for (m, n) in index_range & self.unknown:
key = hash((m, n))
yield (m, n, self.possible[key])

@property
def range_gen(self):
return (self.row,self.column,self.box)

@property
def index_range_gen(self):
for i in range(9):
for fun in self.range_gen:
yield fun(i)

def ruled(self,i,j):
return (self.row(i) | self.column(j) | self.box(i,j)) - set([(i,j)])

def __init__(self):
'''初始化一些成员
self.all                  所有索引 (i, j) 并保存在表
self.known = []           记录所有已知索引的表
self.unknown = self.all   记录所有未知索引的表
self.possible             是一个字典,记录所有未知索引处可以填入的数字
key, item = hash((i, j)), set(range(1,10))'''

self.known = set()
self.unknown = self.all.copy()

self.possible = {}
for t in self.unknown:
self.possible[hash(t)] = set(range(1, 10))

'''make_known(i, j, value) 填入数据
每次填入的时候,自动维护 known, unknown, 和 possible'''
def make_known(self,i,j,value):
key = hash((i,j))

assert (i, j ) in self.unknown
assert key in self.possible

self.unknown.remove((i,j))
self.known.add((i, j, value))
del self.possible[key]

#remove value from all relative blanks which are unknown
updated = set()
for index in self.ruled(i,j) & self.unknown:
key = hash(index)
if value in self.possible[key]:
updated.add(index)
self.possible[key].discard(value)
return updated

def show_possible(self):
for i in range(9):
for j in range(9):
if (i,j) in self.unknown:
print str(self.possible[hash((i,j))]).ljust(15),
else:
print str([]).ljust(15),
print ""

class Sudoku(list):

numbers = set(range(1,10))#shared by all Sudoku

def __init__(self,data):
'''记录这个类的递归深度'''
if not hasattr(data,"level"):
self.level = 0
else:
self.level = data.level + 1

'''初始化成员 indexs, 它是类 index 的一个对象'''
self.indexs = Index()

'''从输入参数 data 中初始化自身和 indexs'''
for i in range(9):
self.append([])
for j in range(9):
self[i].append(0)

for i, j in self.indexs.all:
if data[i][j]:
self.setitem(i, j, data[i][j])

def setitem(self,i,j,value):
'''调用 setitem 函数填入数据
将自动调用 self.indexs.make_known 维护 self.indexs'''
if self[i][j]:
raise FillingException((i,j,value))

self[i][j] = value
updated = self.indexs.make_known(i,j,value)

return updated

def find_unique(self,search_range = None):
'''生成器: 唯余解法

唯余解法就是某宫格可以添入的数已经排除了8个,那么这个宫格的数字就只能添入那个
4000
没有出现的数字
也就是根据 possible 的长度为 1 来判断该宫格中只能填入 possible 中剩下的数

此外,注意到也有一种解法称作“唯一解法”
比如,当某行已填数字的宫格达到8个,那么该行剩余宫格能填的数字就只剩下那个还没出现过的数字了
因为当某行已填数字的宫格达到8个时,剩余宫格中的 possible 集合中必然最多只能有一个数字
所以本方法涵盖了唯一解法'''
if search_range is None:
search_range = self.indexs.unknown

for i,j in search_range & self.indexs.unknown:
key = hash((i, j))
if len(self.indexs.possible[key]) == 1:
yield i,j,list(self.indexs.possible[key])[0]

def find_only(self, search_range):
'''隐性唯一候选数法
根据 search_range 的不同,对行、列或者九宫格进行隐性唯一候选数法搜索

隐性唯一候选数法:
当某个数字在某一列各宫格的候选数中只出现一次时
那么这个数字就是这一列的唯一候选数了
这个宫格的值就可以确定为该数字
这是因为,按照数独游戏的规则要求每一列都应该包含数字1~9
而其它宫格的候选数都不含有该数
则该数不可能出现在其它的宫格,那么就只能出现在这个宫格了
对于唯一候选数出现行,九宫格的情况,处理方法完全相同'''

numbers = Number_Counter()
for i, j in search_range & self.indexs.unknown:
key = hash((i, j))
numbers.extend((i, j), self.indexs.possible[key])

index, value = numbers.find_only()
if index:
yield index[0], index[1], value

def find_only_and_fill(self,search_range = None):
'''调用 find_only 查找满足隐性唯一候选数法的宫格并填入数据'''
updated = set()

if search_range is None:
for fun in self.indexs.range_gen:
for i in range(9):
search_range = fun(i)

updated |= self.find_only_and_fill(search_range)
else:
'''这里才真正调用 find_only '''
for i, j, v in set(self.find_only(search_range)):
updated |= self.setitem(i, j, v)

if updated:
'''如果更新了某些宫格'''
self.find_unique_and_fill(search_range & self.indexs.unknown)

return updated

def find_unique_and_fill(self,search_range = None):
updated = set()
for i, j ,v in set(self.find_unique(search_range)):
updated |= self.setitem(i, j, v)
return updated

def find_fill(self,search_range):
return self.find_unique_and_fill() and self.find_only_and_fill()

def chain(self,search_range):
updated = set()

if not search_range:
return updated

all_items = self.indexs.candidates_in(search_range)
if not all_items:
return updated

candidate_counter = Candidate_Counter(all_items)

for indexs in candidate_counter.links():
for i, j in indexs:
key_link = hash((i, j))
values = self.indexs.possible[key_link]
for m, n in search_range & self.indexs.unknown - set(indexs):
key = hash((m, n))
for v in values:
if v in self.indexs.possible[key]:
updated.add((m, n))
self.indexs.possible[key].remove(v)
if updated:
if not self.find_only_and_fill(search_range):
self.find_unique_and_fill(search_range)
return updated

def show(self):
for line in self:
print line
print ""

def fill_sure(self):
while True:

for fun in self.indexs.range_gen:
for i in range(9):
search_range = fun(i)
self.chain(search_range)

if not self.find_unique_and_fill() and not self.find_only_and_fill():
break

def solved(self):
if not self.indexs.unknown and self.check():
setattr(self,"solved",lambda :True)
return True
else:
return False

def guesser(self):
shortest = ()
number = 9
index = ()
for i,j in self.indexs.unknown:
key = hash((i,j))
possible = self.indexs.possible[key]
if len(possible) < number:
index = (i, j)
shortest = possible
number = len(possible)

i, j = index
for v in shortest:
yield (i,j,v)

raise NoNumberSuitEx(i+1, j+1)

def solve(self):

#self.fill_sure()

if self.solved():
return self

for i,j,v in self.guesser():

new_puzzle = Sudoku(self)
new_puzzle.setitem(i,j,v)

try:
ans = new_puzzle.solve()
return ans
except MyException,e:
print e
continue

def check(self):
for gen in self.indexs.range_gen:
for i in range(9):
numbers = set((self[m]
for m, n in gen(i)))
if numbers != self.numbers:
raise WrongSudokuEx(i+1,gen,numbers)
return True

def stream_to_data(istream):
data = [eval(c) for c in istream if c in "0123456789"]
data = [data[9*i:9*(i+1)] for i in range(9)]
return data

def data_to_stream(data):
string = []
for line in data:
line = [str(n) for n in line]
string.append("".join(line))
return "".join(string)

if __name__ == "__main__":

from time import time
counter = 0
#with open("./sudoku.txt","r") as handle:
#    handle = open("./sudoku.txt",'r')
#    istream_set = handle.readlines()[0:100]
#    handle.close()
#    for istream in istream_set:
istream = "450890000000000000008700090607005030090020040040900102070006300000000000000048016"
istream = istream.replace(".","0")
data = stream_to_data(istream)
START = time()
istream = istream.replace("\r\n","").replace(" ","")
#print len(istream)
print 'Q:\t',istream
s = Sudoku(data)
y = s.solve()
if "0" not in str(y):
print "Succeed, time used: ", (time() - START)*1e3, "ms :",y.check()
pass
else:
print "%d  Fail   , time used: "%counter, (time() - START)*1e3, "ms:"
print 'A:\t',data_to_stream(y)
print ""

'''
Q:    450890000000000000008700090607005030090020040040900102070006300000000000000048016
Succeed, time used:  12.7029418945 ms : True
A:    452891673769534821318762495627415938891623547543987162274156389186379254935248716
'''
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息