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