利用python,求解数独

4 篇文章 0 订阅
1 篇文章 0 订阅
import numpy as np
import time
time1 = time.time()
'''
    整体灵感就是
    1 求出每个数字为0的位置可以填的数,并将其位置和能填的数分别以key和value的方式
      存储到字典里面
    2 将字典里的数据按照所能填写的数据的多少进行排序,先在能填的数少的里面选取一个
      进行填写
    3 将填写的过程记录到列表里面,这个地方暂时想到的就是用列表记录填写过程
    4 更新1、2步,若出现某一步可填写的数据为空,说明之前某一步的选择有问题,回溯回
      去,更换数值,然后回到步骤1
    5 当所有的数都填完后,退出循环
'''


def nine(data):
    nine_block = np.zeros([3,3,3,3], dtype = int)
    for i in range(3):
        for j in range(3):
            nine_block[i,j] = data[3*i:3*(i+1),3*j:3*(j+1)]
    return nine_block

def num_set(data, nine_block):
    pick_set = {}
    for i in range(9):
        for j in range(9):
            if data[i,j] == 0:
                pick_set[str(i)+str(j)] = set(np.array(range(10))) - \
                (set(data[i,:]) | set(data[:,j]) | \
                set(nine_block[i//3,j//3].ravel()))
    return pick_set


def try_insert(data):    

    insert_step = []
    while True:
        
        pick_set = num_set(data, nine(data))
        if len(pick_set) == 0: break
        pick_sort = sorted(pick_set.items(), key = lambda x:len(x[1]))
        item_min = pick_sort[0]
        key = item_min[0]
        value = list(item_min[1])
        insert_step.append((key, value))
        if len(value) != 0:
            data[int(key[0]), int(key[1])] = value[0]
        else:
            insert_step.pop()
            for i in range(len(insert_step)):
                huishuo = insert_step.pop()
                key = huishuo[0]
                insert_num = huishuo[1]
                if len(insert_num) == 1:
                    data[int(key[0]), int(key[1])] = 0
                else:
                    data[int(key[0]), int(key[1])] = insert_num[1]
                    insert_step.append((key, insert_num[1:]))
                    break
    tiem2 = time.time()
    print('\nFinished! using time:', tiem2-time1, 's')
    print(data)    
   
if __name__ == '__main__':
    data =  "0 0 0 0 0 0 0 6 0 \
             0 0 0 0 0 4 7 0 5 \
             5 0 0 0 0 0 1 0 4 \
             1 0 0 0 0 2 4 0 0 \
             0 0 8 0 7 0 0 0 0 \
             0 3 0 6 0 0 0 0 0 \
             2 0 0 0 0 9 0 0 1 \
             0 0 6 0 8 0 0 0 0 \
             0 7 0 3 0 0 0 0 0 "
    data = np.array(data.split(), dtype = int).reshape((9, 9))
    print(data)
    try_insert(data)

别不多说,代码先行,运行结果如下

      用时1.91秒,还算能看,其实很早就想做点什么,但碍于能力有限,一直拖着,没有实践。刚开始考虑用excel实现求解数独来着,奈何同样涉及到VBA,而在下对VBA只是略懂皮毛,只能作罢。

      至于学习Python的过程,其实在很早的时候,就听同窗加挚友洪同学提及过Python,当时第一感觉和帕金森音略同,故而与之戏称帕金森云云,真正开始学习Python其实要追溯到17年12月份,到写此文时也有近6个月的时间,不知不觉,小半年了。进度之缓慢,说来实在惭愧。

      好了,回到正文,其实基本的思想在代码里面也有所提及,下面就着重讲解下各部分代码在干嘛吧!

      numpy这个东西很好用,里面提供了数组的各种花样操作,对于初学者来说,降低了入门难度,故而我很喜欢^_^。而time模块在这里其实只起到了计算程序运行时间的作用,就不再说了。

      首先,定义函数nine(data),这个函数的主要功能是将9×9的数据,变成3×3的块,于是在事先,就先利用了numpy模块生成了这样的块,这也就是下面这行代码所干的事。

nine_block = np.zeros([3,3,3,3], dtype = int)

紧接着,将原来的数据按块填进已生成的nine_block中,就是两个for循环,就实现了这个需求。

for i in range(3):
        for j in range(3):
            nine_block[i,j] = data[3*i:3*(i+1),3*j:3*(j+1)]

至于这里的赋值为什么是这样来赋值,其实很容易理解,拿所要填的第一块来说,也就是nine_block[0,0],是不是希望将data里面的前三行前三列数据填过来,这个时候那就是data[0:3, 0:3]; 再看水平方向的第二块,是不是就是data[0:3, 3:6],以此类推。那么跟nine_block的索引i, j结合起来就不难看出这个赋值表达式意义了。

        接着,定义函数num_set(data, nine_block),此函数的作用为,将data中所有为0的位置的可填数找到,并存储到一个字典里面,其中这个字典的key为0值的行索引和列索引,这里用str(i)+str(j)实现,而该位置可填的数究竟怎么确定的,其实,这就要回到数独的玩法上了,因为在填数时玩家必须保证该位置所属的行、列以及块里面没有这个数的时候,否则就违反了游戏规则,那么这里的可填数就使用了这个规则,就是在(1 ~ 9)的数字集合删去已经在相关位置出现的数,那么这样就找到了该位置可填的数的集合。在这里主要用到了集合的子、交、并、补概念。

pick_set = {}
for i in range(9):
    for j in range(9):
        if data[i,j] == 0:
            pick_set[str(i)+str(j)] = set(np.array(range(10))) - \
            (set(data[i,:]) | set(data[:,j]) | \
            set(nine_block[i//3,j//3].ravel()))

可能你会注意到在这里的相应的所属块取出来后,可能无法直接用set(nine_block[i//3,j//3]),但我把这个块状的数据ravel()后,就是拉平后,可以使用了,至于内在原因,还在摸索当中。

这个函数的返回值是长这样的^_^

                                                                

比如说'00':{3,4,7,8,9},这就表明此时在data中位于第1行,第1列的位置,其可填的数为3,4,7,8和9。其他的也是如此。

        最后定义的函数为try_insert(data),实现了数据的插入,步骤分为两步,即插和改。由于对递归实现的方法不甚理解,所以就不班门弄斧了,这里使用的是利用列表来记录每一次的插入。插入一个值,同时在插入下一个值之前,就更新了我们的可选字典,这样以便快速验证插入数值的正确性。那么究竟怎么判断我首次插入的值是否准确呢,其实这个地方就是深度优先的概念了,就是说,一条路走到黑,直到走不通为止,因为但凡我前面的数填的不对,走到后面就会发现,在某一个位置就会出现没有可填数的情况,但怎么告诉程序说前面填错了呢?思考三分钟...

        这个时候我们的记录插入情况的列表就派上用场了,可以根据插入情况进行回溯,怎么个回溯法呢?简单来说就是由后往前找,在第一个分叉的地方,换另外的数,并把之前走不通的数舍去,下面就一个例子进行说明,假如下面是我的某一次的插入过程

                                                                

在程序里面默认的插入方法是:当有多个选择时,先插入可选数集合中的第1个数。在如上的这个插入过程当中,发现,在进行对索引值为'55'位置的数进行插入时,竟然发现此处没有可插入数,那么,问题来了,必然是前面出错了,可是错误出在哪呢?无从得知,只知道错误出现了,那么就此例而言,错误会出现在索引值为'45', '75', '01'这些可填值只有1个的位置上吗,显然不是,因为可填数只有1个,那我只能填这个了,暂且称之为被动正确(临时想到的),那么这个错误很有可能是出在诸如索引值为'00', '21'这样的可填数不唯一的地方,可是怎么回去呢,思考三分钟...

        一言以蔽之 改,如何改,不卖关子了,就是把'70', '10', '62'这些被动正确的地方数重新变为0,然后在'00'这个位置把插入数不选9,选3,然后往下走,如果选3之后后面走通了,那么皆大欢喜,但是,万一走不通怎么办,如果又走不通,说明不是'00'处的问题,而是问题还在前面,如'21', '40'这样的位置,但如何回溯到更前一步呢?即然在前面情况不变下,'00'处选9走不通,那何不在首次回溯时,就暂时在'00'处的可选值中将9丢弃,这样在'00'处选3的情况下,如果还走不通,那么我们再次进行回溯时,就不会还是找'00'了,而是会找到'21'的位置。如此循环,直到所有的值都填上为止。

        代码就不再贴了,前面可能没有说,在选值进行插入时,我提前将可选集合按照可填数的多少进行了排序,先填可选数少的,这样可以大大提高程序运行效率。^_^


                
  • 10
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Python求解数独(Sudoku)通常会使用回溯算法或启发式搜索策略。以下是一个简单的概述: 1. **回溯算法**:这是一种递归的方法,从第一个空格开始,尝试填入数字1到9,然后检查每一行、每一列和每一个宫格(3x3的小九宫格)是否都包含这9个数字一次。如果发现冲突,则回溯到上一步,尝试下一个数字。这个过程一直持续到找到一个合法的数独解决方案或者所有可能的数字都尝试过仍无解。 2. **启发式搜索**:如使用A*搜索,会优先填充那些规则更明确的位置(如行、列、宫格已完全填满的区域),并在搜索过程中维护一个启发式函数(如最小剩余数法),帮助找到最快接近解决方案的状态。 在Python中,可以使用`numpy`库来创建和操作二维数组,`pandas`处理数据结构,还有自定义的数据结构(如`dict`或`set`)来存储候选数字。下面是一个简单的回溯算法示例: ```python import numpy as np def solve_sudoku(board): empty_cells = find_empty_cells(board) if not empty_cells: return True # 解决了,返回True row, col = empty_cells for num in range(1, 10): if is_safe(board, row, col, num): board[row][col] = num if solve_sudoku(board): return True board[row][col] = 0 # 回溯:移除刚才填入的数 return False # 辅助函数 def find_empty_cells(board): for i in range(9): for j in range(9): if board[i][j] == 0: return (i, j) # 返回第一个空格的坐标 return None def is_safe(board, row, col, num): # 检查行、列、宫格 return all(board[row, j] != num for j in range(9)) and \ all(board[i, col] != num for i in range(row, row + 3, 3)) and \ all(board[i, j] != num for j in range(col, col + 3, 3)) # 示例: board = np.array([[5, 3, 0, 0, 7, 0, 0, 0, 0], [6, 0, 0, 1, 9, 5, 0, 0, 0], [0, 9, 8, 0, 0, 0, 0, 6, 0], [8, 0, 0, 0, 6, 0, 0, 0, 3], [4, 0, 0, 8, 0, 3, 0, 0, 1], [7, 0, 0, 0, 2, 0, 0, 0, 6], [0, 6, 0, 0, 0, 0, 2, 8, 0], [0, 0, 0, 4, 1, 9, 0, 0, 5], [0, 0, 0, 0, 8, 0, 0, 7, 9]]) if solve_sudoku(board): print("Solution:") print(board) else: print("No solution exists.") ```
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值