《算法的乐趣》19.数独游戏------python

本文介绍数独游戏的规则及解决技巧,涵盖直观法与候选数法,并提供两种计算机求解方法:基于候选数的穷举算法与解空间搜索的深度优先搜索加约束传播算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数独游戏的规则与技巧

规则

9x9格子数独游戏:一般用大写字母A-I来标识行,用数字1-9来标识列,这样每个小单元格就有了坐标。
数独游戏的规则非常简单,就是在9x9=81个单元格中填入数字1-9。这81个单元格又组成3x3=9个小九宫格,要求填人的数字在每行和每列都不能有重复,同时在每个小九宫格中也不能有重复。
游戏开始时会将一些位置上的数字固定下来,这称为提示数(或起始数),根据提示数的位置和数量可以将数独游戏分成不同的难度级别。

常用技巧

解决数独问题的技巧大致可分为两类,一类是直观法,另一类是候选数法。
直观法就是不借助任何辅助工具,直接利用数独游戏的规则进行求解的方法,一般只需一只铅笔就可以直接在报纸或杂志上玩了。这种方法适合求解简单的数独游戏,对于常见于报纸和杂志上的数独题目(专业数独杂志除外),都可以轻应对,也是最能体验数独乐趣的一种方法。直观法常用的技巧包括唯一解法、基础排除法、区块排除法、唯余解法、矩形排除法、单元排除法等。这些技巧中唯一解法、基础排除法和唯余解法是基本的技巧,一般简单的数独用这三解答技巧就可以应付。除此之外,其他几种技巧都对应一种或多种稍微复杂的数独局面,当求解数独过程中遇到与之相似的局面时,应用对应的方法可以起到事半功倍的效果。

候选数法首先要根据数独题目的需要为每个没有确定的单元格建立一个候选数列表,然后根据各种排除方法,逐步排除每个单元格中不可能出现的候选数,当某个单元格对应的候选数列表中只剩下唯一候选数时,这个剩下候选数是该单元格要填的正确数字。候选数法需要一个准备过程,要为每个单元格建立候选数列表,通常在解题过程中,都是先利用直观法进行求解,直到直观法无法继续时,才使用候选数法。候选数法需要做一些简单的记录来维护选数列表,因此没有直观法那么直接,但是候选数法适合解决较为复杂的数独难题(比如有多个解的数独问题)。候选数法常用的技巧包括唯一候选数法、隐性唯一候选数法、区块删减法、数对删减法、隐性数对删碱法、三链数减法、隐性三链数删减法、矩形顶点删减法、三链列删减法、关键数删减法、关连数删减法等。

计算机求解数独问题

基于候选数方法的穷举算法
相关20格:每一个单元格所在的行、列和小九宫格中的20个单元格被称为这个单元格的相关20格。
首先为每个单元格建立候选数列表,并且对每个已经给出的提示数使用基本排除法,排除与这些提示数有关的相关20格的无效候选数;
然后利用枚举的方法对每个还没有确定的单元格进行试数,没进行一次试数,就对这个单元格的相关20格的候选数列表进行排除法维护。
重复

建立问题的数学模型

每个小单元格有两种状态,确定和不确定
当处于确定状态时,要个一个属性描述这个确定的数字;当处于不确定状态时,需要一个属性描述候选列表。
整体数据结构:一个二维矩阵表示81个单元格,再加上一个当前已经确定的单元格计数器。

算法实现

回溯法
参考链接:https://blog.youkuaiyun.com/weixin_42147487/article/details/80214185

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 6 3 0 0 \
             0 0 9 5 4 0 0 0 0 \
             0 0 4 0 0 0 0 0 2 \
             0 0 0 8 0 0 0 0 4 \
             3 7 0 0 0 0 0 0 0 \
             0 6 0 0 0 0 0 0 0 \
             2 0 5 9 0 0 0 0 0 \
             0 0 0 0 0 0 0 0 0 \
             0 0 0 0 0 0 6 7 0 "
    data = np.array(data.split(), dtype = int).reshape((9, 9))
    print(data)
    try_insert(data)
[[0 0 0 0 0 6 3 0 0]
 [0 0 9 5 4 0 0 0 0]
 [0 0 4 0 0 0 0 0 2]
 [0 0 0 8 0 0 0 0 4]
 [3 7 0 0 0 0 0 0 0]
 [0 6 0 0 0 0 0 0 0]
 [2 0 5 9 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 6 7 0]]

Finished! using time: 0.479717493057251 s
[[5 2 7 1 9 6 3 4 8]
 [8 3 9 5 4 2 7 1 6]
 [6 1 4 7 3 8 9 5 2]
 [9 5 1 8 7 3 2 6 4]
 [3 7 2 6 5 4 8 9 1]
 [4 6 8 2 1 9 5 3 7]
 [2 4 5 9 6 7 1 8 3]
 [7 9 6 3 8 1 4 2 5]
 [1 8 3 4 2 5 6 7 9]]

解空间搜索的深度优先搜索(最小代价优先)加约束传播算法来解数独。
参考链接:https://www.cnblogs.com/PyLearn/p/8034449.html#jump2

'''
    数独是一个非常有趣味性的智力游戏
    参与者需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,
    并满足每一行、每一列、每一个宫内的数字均含1-9,不重复。
'''
__author__ = 'PyLearn'
import time

def cross(A, B):
    # 例如:A = 'ABC', B = '123'
    # 则返回['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']
    return [a+b for a in A for b in B]
 
def arr_to_dict(A, B):
    # 例如:A = ['A', 'B', 'C'], B = ['1', '2', '3']
    # 则返回{'A': '1', 'B': '2', 'C': '3'}
    return dict(zip(A, B))
 
def str_to_arr(str_sudoku):
    # 传入:str_sudoku = '4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......'
    # 返回['4', '.', '.', '.', '.', '.', '8', ... , '.', '.']
    return [c for c in str_sudoku if c in cols or c in '0.']
 
def show_str_sudoku(str_sudoku):
    # 解析字符串形式的数独并展示
    for i, value in enumerate(str_sudoku):
        if i%3 == 0 and i%9 != 0:
            print('|', end=' ')
        print(value, end=' ')
        if (i+1)%9 == 0:
            print()
        if i == 26 or i == 53:
            print('------+-------+------')

def show_dict_sudoku(dict_sudoku):
    # 解析字典形式的数独并展示
    width = 1 + max(len(dict_sudoku[s]) for s in squares)
    line = '+'.join(['-' * (width * 3)] * 3)
    for r in rows:
        print(''.join(dict_sudoku[r + c].center(width) + ('|' if c in '36' else '') for c in cols))
        if r in 'CF': 
            print(line)
    print()

cols = '123456789'
rows = 'ABCDEFGHI'
# squares表示 9*9个元素编号:['A1', 'A2', 'A3', ... , 'I8', 'I9']
squares = cross(rows, cols)
# unitlist表示 3*9个单元列表:
unitlist = ([cross(rows, c) for c in cols] + [cross(r, cols) for r in rows] + [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')])
# units表示某个元素编号:与之相关的3个单元列表
units = dict((s, [u for u in unitlist if s in u]) for s in squares)
# peers表示某个元素编号:与之相关的20个元素编号
peers = dict((s, set(sum(units[s], []))-set([s])) for s in squares)
 
# 一.数独预处理
def parse_sudoku(str_sudoku):
    # values代表各位置上可能的取值:{'A1': '123456789', 'A2': '123456789', ... , 'I8': '123456789', 'I9': '123456789'}
    values = dict((s, cols) for s in squares)
    # arr_sudoku为数组形式, dict_sudoku为字典形式, 均为81位
    arr_sudoku = str_to_arr(str_sudoku)
    dict_sudoku = arr_to_dict(squares, arr_sudoku)# {'A1': '4', 'A2': '.', ... , 'I8': '.', 'I9': '.'}
 
    for key, value in dict_sudoku.items():
        if value in cols and not assign(values, key, value):
            return False
 
    return values
 
def assign(values, key, value):
    # 从values[key]中删除除了value以外的所有值,因为value是唯一的值
    # 如果在过程中发现矛盾,则返回False
    other_values = values[key].replace(value, '')
    if all(eliminate(values, key, num) for num in other_values):
        return values
    else:
        return False
 
def eliminate(values, key, num):
    # 从values[key]中删除值num,因为num是不可能的
    if num not in values[key]:
        return values
    values[key] = values[key].replace(num, '')
 
    # 这里采用了约束传播
    # 1.如果一个方块只有一个可能值,把这个值从方块的对等方块(的可能值)中排除。
    if len(values[key]) == 0:
        return False
    elif len(values[key]) == 1:
        only_value = values[key]
        # 从与之相关的20个元素中删除only_value
        if not all(eliminate(values, peer, only_value) for peer in peers[key]):
            return False
 
    # 2.如果一个单元只有一个可能位置来放某个值,就把值放那。
    for unit in units[key]:
        dplaces = [s for s in unit if num in values[s]]
        if len(dplaces) == 0:
            return False
        elif len(dplaces) == 1:
            only_key = dplaces[0]
            if not assign(values, only_key, num):
                return False
    return values


# 二.解数独
def solve_sudoku(str_sudoku):
    return search_sudoku(parse_sudoku(str_sudoku))
 
def search_sudoku(values):
    if values is False:
        return False
    if all(len(values[s]) == 1 for s in squares):
        return values
 
    # 选择可能值数目最少的方块, 进行深度优先搜索
    n, key = min((len(values[key]), key) for key in squares if len(values[key]) > 1)
    return some_result(search_sudoku(assign(values.copy(), key, num)) for num in values[key])
 
def some_result(values):
    for result in values:
        if result:
            return result
    return False
 
if __name__ == '__main__':
    # str_sudoku为字符串形式, 为81位
    str_sudoku = ['4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......']
    # str_sudoku = ['4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......',
    #               '003020600900305001001806400008102900700000008006708200002609500800203009005010300',
    #               '.....6....59.....82....8....45........3........6..3.54...325..6..................']
 
    for sudoku in str_sudoku:
        start = time.clock()
        solve_result = solve_sudoku(sudoku)
        end = time.clock()
        print('初始数独为:')
        show_str_sudoku(sudoku)
        print('解为:')
        show_dict_sudoku(solve_result)
        print("求解数独运行时间为: %f s" % (end - start))
初始数独为:
4 . . | . . . | 8 . 5 
. 3 . | . . . | . . . 
. . . | 7 . . | . . . 
------+-------+------
. 2 . | . . . | . 6 . 
. . . | . 8 . | 4 . . 
. . . | . 1 . | . . . 
------+-------+------
. . . | 6 . 3 | . 7 . 
5 . . | 2 . . | . . . 
1 . 4 | . . . | . . . 
解为:
4 1 7 |3 6 9 |8 2 5 
6 3 2 |1 5 8 |9 4 7 
9 5 8 |7 2 4 |3 1 6 
------+------+------
8 2 5 |4 3 7 |1 6 9 
7 9 1 |5 8 6 |4 3 2 
3 4 6 |9 1 2 |7 5 8 
------+------+------
2 8 9 |6 4 3 |5 7 1 
5 7 3 |2 9 1 |6 8 4 
1 6 4 |8 7 5 |2 9 3 

求解数独运行时间为: 0.010437 s

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值