美文网首页
解数独算法

解数独算法

作者: _n_n_ | 来源:发表于2018-06-08 21:57 被阅读0次

    昨天在Ubuntu18.04上打开自带的数独游戏,宿舍几个人一起玩了很久,今天整理了一下玩的过程,研究出算法并写成程序,以自动求解数独游戏,具体实现步骤已在代码注释中详细说明。

    代码已发布在我的github上:
    HelloGithubHaHa


    
    '''计算机自动解9x9数独游戏,输入题目后进行推断,若无法继续推断,则进行猜测,若猜测错误,则进行回溯,直到成功解出结果'''
    '''题目信息保存在9x9的数组中,推断信息以set集合的形式存储于cheet_with_infer数组中'''
    '''有两个主要的数据结构,一个是元素全为int的cheet_without_infer,一个是元素为int或set的cheet_with_infer'''
    
    # 需要用到深拷贝
    import copy
    
    def print_cheet_without_infer(cheet):
        '''打印没有推断信息的数组'''
        print()
        for line in cheet:
            for num in line:
                print('{:^3}'.format(num), end='')
            print()
    
    def print_cheet_without_infer_to_input_again(cheet):
        '''以与输入相同的格式,打印没有推断信息的数组,便于手动猜测后再次输入'''
        print()
        for line in cheet:
            print(','.join([str(x) for x in line]).replace('-1', ' '))
    
    def print_cheet_with_infer(cheet):
        '''打印包含推断信息的数组'''
        print()
        for line in cheet:
            for infer in line:
                print('{:^15}'.format(str(infer).replace(' ', '')), end='')
            print()
    
    def infer(cheet, cheet_origin):
        '''
        cheet为不包含推断信息的数组,cheet_origin为包含推断信息的数组
        根据基本规则进行推断,每一个数所在行、列以及九宫格不能包含相同的数字
        若数组中的所有元素都已经推断出,则返回2
        若数组中的某个元素在此次函数调用中可以明确推断出,则返回1
        若不能推断出任何未知元素,则返回0
        若有错(某一位置的推断信息为空set集合),说明不满足任何数独的基本规则,返回-1
        '''
        finish = True # 如果数组中的所有元素都已经推断出,则finish为True
        for i in range(9):
            for j in range(9):
                if cheet_origin[i][j] == -1:
                    finish = False # 数组中尚有元素未推断出,finish为False
                    infer_set = set(range(1, 10)) # 初始化推断信息
                    # 通过行和列不能包含相同的数字进行推断
                    for k in range(9):
                        if k != j and cheet_origin[i][k] != -1:
                            infer_set.discard(cheet[i][k])
                        if k != i and cheet_origin[k][j] != -1:
                            infer_set.discard(cheet[k][j])
                    # 通过九宫格内不能包含相同的数字进行推断
                    x = j // 3 * 3
                    y = i // 3 * 3
                    for m in range(y, y + 3):
                        for n in range(x, x + 3):
                            if (m != i or n != j) and cheet_origin[m][n] != -1:
                                infer_set.discard(cheet[m][n])
                    # 判断此位置的推断信息,据此控制程序流程
                    if len(infer_set) == 1:
                        cheet[i][j] = infer_set.pop()
                        cheet_origin[i][j] = cheet[i][j]
                        return 1
                    elif len(infer_set) == 0:
                        return -1
                    else:
                        cheet[i][j] = infer_set
        if finish:
            return 2
        else:
            return 0
    
    def power_set(s):
        '''求一个集合的所有非空真子集(二进制法),为了进行速度优化,需要在返回前对其进行排序'''
        set_list = []
        l = list(s)
        n = len(l)
        # 非空,所以下界为1;真子集,所以上界是2的n次方
        for i in range(1, 2**n - 1):
            combo = set()
            for j in range(n):
                if (i >> j) % 2 == 1:
                    combo.add(l[j])
            set_list.append(combo)
        set_list = sorted(set_list, key=lambda x: len(x))
        return set_list
    
    def deep_infer(cheet, cheet_origin):
        '''
        根据推断信息进行推断,去除某些位置的可能性数字
        对于每一行、每一列、每一九宫格内的推断信息,
        先求出这些推断信息的并集,再求出此并集的所有非空真子集,对于每一个非空真子集,
        若推断信息中有 小于等于此真子集大小 数目的超集,并且对于其他推断信息,此真子集与其的交集均为空(不包含此真子集中的元素),
        则这些位置的推断信息可置为此真子集
        如某一行中有四个待推断元素,推断信息为:{1,3,6}, {1,3,6}, {1,4}, {1,4}
        在对其并集的非空真子集的遍历过程中,发现集合{3,6}的超集有{1,3,6}和{1,3,6},并且其他推断信息{1,4}与此集合{3,6}的交集为空,
        因此,可以将两个推断信息{1,3,6}置为{3,6}
        若不能推断出任何未知元素,则返回2
        若数组中的某个元素在此次函数调用中可以明确推断出,则返回1
        若推断信息在此次函数调用中有修改,则返回0
        '''
        change = False # 推断信息是否有修改的标志
    
        # 根据每一行已有的推断信息进行推断
        for i in range(9):
            # 求并集
            union_set = set()
            for j in range(9):
                if isinstance(cheet[i][j], set):
                    union_set = union_set.union(cheet[i][j])
            # 遍历根据长度排序的所有非空真子集
            for s in power_set(union_set):
                contain_set_list = [] # 保存列号
                finish = True # 若此行中的所有列上的推断信息的长度都小于等于集合s的长度,则可以提前退出,finish为True
                satisfy = True # 若除了集合s的超集以外,此行还含有推断信息与集合s的交集不为空的位置,则不能进行推断,satisfy为False
                for j in range(9):
                    if isinstance(cheet[i][j], set):
                        if len(cheet[i][j]) > len(s):
                            finish = False
                        if s.issubset(cheet[i][j]):
                            contain_set_list.append(j)
                        elif len(s.intersection(cheet[i][j])) > 0:
                            satisfy = False
                if finish:
                    break
                if not satisfy:
                    continue
                if len(contain_set_list) <= len(s) and len(contain_set_list) > 0:
                    # 若s长度为1,则表明已经明确推断出此位置的数字,于是将set中的元素提取出来放到此位置上
                    if len(s) == 1:
                        j = contain_set_list[0]
                        cheet[i][j] = s.pop()
                        cheet_origin[i][j] = cheet[i][j]
                        return 1
                    for j in contain_set_list:
                        if len(cheet[i][j]) > len(s):
                            cheet[i][j] = s.copy()
                            change = True
        # 根据每一列已有的推断信息进行推断,和前面类似
        for j in range(9):
            union_set = set()
            for i in range(9):
                if isinstance(cheet[i][j], set):
                    union_set = union_set.union(cheet[i][j])
            for s in power_set(union_set):
                contain_set_list = []
                finish = True
                satisfy = True
                for i in range(9):
                    if isinstance(cheet[i][j], set):
                        if len(cheet[i][j]) > len(s):
                            finish = False
                        if s.issubset(cheet[i][j]):
                            contain_set_list.append(i)
                        elif len(s.intersection(cheet[i][j])) > 0:
                            satisfy = False
                if finish:
                    break
                if not satisfy:
                    continue
                if len(contain_set_list) <= len(s) and len(contain_set_list) > 0:
                    if len(s) == 1:
                        i = contain_set_list[0]
                        cheet[i][j] = s.pop()
                        cheet_origin[i][j] = cheet[i][j]
                        return 1
                    for i in contain_set_list:
                        if len(cheet[i][j]) > len(s):
                            cheet[i][j] = s.copy()
                            change = True
        # 根据每一九宫格已有的推断信息进行推断,和前面类似
        for m in range(3):
            for n in range(3):
                union_set = set()
                for i in range(m * 3, m * 3 + 3):
                    for j in range(n * 3, n * 3 + 3):
                        if isinstance(cheet[i][j], set):
                            union_set = union_set.union(cheet[i][j])
                for s in power_set(union_set):
                    contain_set_list = []
                    finish = True
                    satisfy = True
                    for i in range(m * 3, m * 3 + 3):
                        for j in range(n * 3, n * 3 + 3):
                            if isinstance(cheet[i][j], set):
                                if len(cheet[i][j]) > len(s):
                                    finish = False
                                if s.issubset(cheet[i][j]):
                                    contain_set_list.append((i, j))
                                elif len(s.intersection(cheet[i][j])) > 0:
                                    satisfy = False
                    if finish:
                        break
                    if not satisfy:
                        continue
                    if len(contain_set_list) <= len(s) and len(contain_set_list) > 0:
                        if len(s) == 1:
                            i, j = contain_set_list[0]
                            cheet[i][j] = s.pop()
                            cheet_origin[i][j] = cheet[i][j]
                            return 1
                        for i, j in contain_set_list:
                            if len(cheet[i][j]) > len(s):
                                cheet[i][j] = s.copy()
                                change = True
    
        if not change:
            return 2
        else:
            return 0
    
    def find_min_to_guess(cheet_with_infer):
        '''找到并返回一个推断信息的长度最小的位置'''
        min_length = 10 # 初始化最小长度标记
        for i in range(9):
            for j in range(9):
                if isinstance(cheet_with_infer[i][j], set):
                    length = len(cheet_with_infer[i][j])
                    if min_length > length:
                        min_length = length
                        m = i
                        n = j
        return m, n
    
    def start(cheet_with_infer, cheet_without_infer):
        '''开始进行推断,若推断成功,则返回True,否则返回False'''
        while True:
            print_cheet_without_infer(cheet_without_infer)
            res = infer(cheet_with_infer, cheet_without_infer)
            if res == 1:
                continue
            elif res == 2:
                return True
            elif res == -1:
                return False
            while True:
                res = deep_infer(cheet_with_infer, cheet_without_infer)
                if res == 1:
                    break
                elif res == 2:
                    # 无法继续进行推断,于是在推断信息长度最小的位置进行假设,然后继续进行推断(递归),若猜测错误,则进行回溯
                    print_cheet_without_infer_to_input_again(cheet_without_infer)
                    print_cheet_with_infer(cheet_with_infer)
                    print('Can not continue refer! Begin guess!')
                    i, j = find_min_to_guess(cheet_with_infer)
                    for num in cheet_with_infer[i][j]:
                        new_cheet_with_infer = copy.deepcopy(cheet_with_infer)
                        new_cheet_without_infer = copy.deepcopy(cheet_without_infer)
                        new_cheet_with_infer[i][j] = num
                        new_cheet_without_infer[i][j] = num
                        res = start(new_cheet_with_infer, new_cheet_without_infer)
                        if res:
                            cheet_with_infer.clear()
                            cheet_with_infer.extend(new_cheet_with_infer)
                            cheet_without_infer.clear()
                            cheet_without_infer.extend(new_cheet_without_infer)
                            return True
                        else:
                            print('Guess error, begin next guess!')
    
    
    # 用户通过图形界面输入题目信息,程序通过图形界面展示结果
    import tkinter
    import tkinter.messagebox
    
    def infer_handler():
        # 没有推断信息的数组
        cheet_without_infer = []
        for labels in all_labels:
            nums = []
            for label in labels:
                if label['text']:
                    nums.append(int(label['text']))
                else:
                    nums.append(-1)
            cheet_without_infer.append(nums)
    
        # 有推断信息的数组,推断信息在推断的过程中加入
        cheet_with_infer = copy.deepcopy(cheet_without_infer)
    
        # 开始推断
        res = start(cheet_with_infer, cheet_without_infer)
        if not res:
            tkinter.messagebox.showerror('题目信息有误,无法进行推断!')
    
        for i in range(9):
            for j in range(9):
                all_labels[i][j]['text'] = str(cheet_without_infer[i][j])
    
    def clear_handler():
        for labels in all_labels:
            for label in labels:
                label['text'] = ''
    
    def change_bg(source, origin_color):
        for labels in all_labels:
            for label in labels:
                label['bg'] = origin_color
        source['bg'] = 'skyblue'
    
    def keyboard_handler(event):
        if event.char.isdigit() or event.keycode == 8:
            for labels in all_labels:
                for label in labels:
                    if label['bg'] == 'skyblue':
                        if event.keycode == 8:
                            label['text'] = ''
                        else:
                            label['text'] = event.char
    
    root = tkinter.Tk()
    root.resizable(False, False)
    root.title('数独解题器')
    root.bind_all('<Any-KeyPress>', keyboard_handler)
    # 九宫格面板
    top_frame = tkinter.Frame(root)
    top_frame.pack()
    all_labels = []
    for i in range(9):
        row_labels = []
        for j in range(9):
            label = tkinter.Label(top_frame, font=('', 18), width=2, height=1, relief=tkinter.RAISED)
            origin_color = label['bg']
            label.bind('<Button-1>', lambda event, label=label:change_bg(label, origin_color))
            label.grid(row=i, column=j)
            row_labels.append(label)
        all_labels.append(row_labels)
    # 底部按钮面板
    bottom_frame = tkinter.Frame(root)
    bottom_frame.pack()
    infer_button = tkinter.Button(bottom_frame, text='推断', font=('', 18), command=infer_handler)
    infer_button.pack(side=tkinter.LEFT, padx=5, pady=5)
    clear_button = tkinter.Button(bottom_frame, text='清空', font=('', 18), command=clear_handler)
    clear_button.pack(side=tkinter.LEFT, padx=5, pady=5)
    root.mainloop()
    
    

    相关文章

      网友评论

          本文标题:解数独算法

          本文链接:https://www.haomeiwen.com/subject/kvlzsftx.html