Yet Another Sudoku Solver in Python

#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个,那么这个宫格的数字就只能添入那个没有出现的数字
        也就是根据 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][n] 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
''' 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值