解數獨(Python)


0.目錄

1.介紹

2.一些通用函數

3.全局變量(宏變量)

4.數獨預處理(約束傳播)

5.解數獨(深度優先搜索+最小代價優先)

6.主函數

7.總代碼

1.介紹

數獨是一個非常有趣味性的智力游戲,數獨起源於18世紀初瑞士數學家歐拉等人研究的拉丁方陣(Latin Square)。
參與者需要根據9×9盤面上的已知數字,推理出所有剩余空格的數字,並滿足每一行、每一列、每一個宮內的數字均含1-9,不重復。
一個數獨謎題是由81個方塊組成的網格。大部分愛好者把列標為1-9,把行標為A-I,把9個方塊的一組(列,行,或者方框)稱為一個單元,把處於同一單元的方塊稱為對等方塊。謎題中有些方塊是空白的,其他的填入了數字。
每個方塊都屬於3個單元,有20個對等方塊。
當每個單元的方塊填入了1到9的一個排列時,謎題就解決了。
本文采用解空間搜索的深度優先搜索(最小代價優先)加約束傳播算法來解數獨。
代碼總體分為五個部分:
1.通用函數
2.全局變量(宏變量)
3.數獨預處理(約束傳播)
4.解數獨(深度優先搜索+最小代價優先)
5.主函數

2.一些通用函數

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()

cross函數:輸出A、B交叉組合而成的字符串
arr_to_dict函數:將數組形式的數獨轉化為字典形式的數獨
str_to_arr函數:將字符串形式的數獨轉化為數組形式的數獨
show_str_sudoku函數:解析字符串形式的數獨並顯示
show_dict_sudoku函數:解析字典形式的數獨並顯示

3.全局變量(宏變量)

用Python按如下方式來實現單元、對等方塊、方塊的概念:

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)

squares代表81個元素編號
unitlist代表27個不能出現重復數字的單元
units表示某個元素編號以及與之對應的3個單元列表
peers表示某個元素編號以及與之相關的20個元素編號

4.數獨預處理(約束傳播)

初始數獨的樣子:

以下是簡單的預處理函數:

# 一.數獨預處理
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, '')

    return values

共三個函數。values[key]代表在key這個位置上的可能取值。
parse_sudoku函數:預處理的入口函數
assign函數:從values[key]中刪除除了value以外的所有值
eliminate函數:從values[key]中刪除值num
處理完后的數獨為:

以上只是簡單的進行的數獨的預處理。
但是其實根據數獨的規則,我們可以得到以下兩條原則:
(1).如果一個方塊只有一個可能值,把這個值從方塊的對等方塊(的可能值)中排除;
(2).如果一個單元只有一個可能位置來放某個值,就把值放那。
於是我們根據這個策略可以改寫eliminate函數:

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

於是數獨的預處理結果變為了:

這樣是不是就把問題規模一下子簡化了很多。

5.解數獨(深度優先搜索+最小代價優先)

因為沒有規定數獨只有唯一解,所以以下程序實際上求解了數獨的所有解。

# 二.解數獨
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

solve_sudoku函數:是真正的解數獨的入口,將數獨預處理完畢的結果拋給search_sudoku函數求解
search_sudoku函數:是一個遞歸函數,采用的代價函數是選擇可能值數目最少的方塊,然后進行深度優先搜索遍歷。
some_result函數:是在深度優先搜索的結果中找出滿足條件的數獨返回。如果想要所有解,那么可以改成返回一個解的列表。
如果想要程序更快,那么就可以只找一個解。可以在深度優先搜索的循環代碼中,返回找到的滿足條件的解即可。

6.主函數

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))

解出來數獨的結果為:

7.總代碼

'''
    數獨是一個非常有趣味性的智力游戲
    參與者需要根據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))


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM