《AI之矛》(1)【數獨Agent】

Github倉庫地址git

學習是爲了尋找解決問題的答案,若脫離了問題只爲知曉而進行的打call,那麼隨時間流逝所沉澱下來的,估計就只有「重在參與」的虛幻存在感了,自學的人就更應善於發現可供解決的問題。爲了入門AI,定個小目標,解決數獨問題。github

1、問題描述

圖片描述

一個9*9的方格中,部分方格已預先填入數字,目的是按照以下規則將空白方格填上1-9中的一個:數組

  1. 每一個方格中填且僅填一個數字,數字取值範圍1-9
  2. 每行九個方格爲單元來看,1-9每一個數字都要出現,且僅出現一次
  3. 每列九個方格爲單元來看,1-9每一個數字都要出現,且僅出現一次
  4. 3*3九個方格爲單元來看,1-9每一個數字都要出現,且僅出現一次
描述問題是解決問題的第一步(將問題轉化爲程序所能理解的數據模型,才能作進一步有效地思考)

1.1 問題記錄方式

  1. 從左到右從上到下,以一個字符串的方式記下全部方格中的內容,有數字記數字,空白記做點(.),如:..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3..
  2. 以字典的方式記錄,將每行標記爲ABCDEFGHI,每列標記爲123456789,字典的key值爲標記的單元格描述,如:A1,G4等;字典的value值爲方格中的記錄:有數字記數字,空白記做點(.)
{
  'A1': '.'
  'A2': '.',
  'A3': '3',
  'A4': '.',
  'A5': '2',
  ...
  'I9': '.'
}
字符串方式,記錄簡潔佔用空間小,但處理起來比較麻煩;字典方式,方便查找處理,但記錄空間較大。

因此,咱們以字符串方式記錄存儲,以字典方式進行運算求解。那麼在運算求解前須要對記錄方式的轉換多線程

1.2 字典方式記錄

1.2.1 全部key值數組

rows = 'ABCDEFGHI'
cols = '123456789'

def cross(a, b):
    return [s+t for s in a for t in b]

boxes = cross(rows, cols)

1.2.2 規則單元

row_units = [cross(r, cols) for r in rows]
column_units = [cross(rows, c) for c in cols]
square_units = [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')]
unitlist = row_units + column_units + square_units

1.2.3 指定單元格所屬規則單元

units = dict((s, [u for u in unitlist if s in u]) for s in boxes)
peers = dict((s, set(sum(units[s],[]))-set([s])) for s in boxes)

1.3 記錄方式轉換

def grid_values(grid):
    return dict(zip(boxes, grid))

zip() 將可迭代的對象做爲參數,把對象中對應的元素打包成一個個元組,而後返回由這些元組組成的列表app

2、策略1:過濾淘汰

首先明確一個概念:性能

  • 規則單元:一個方格所屬的水平行、垂直列以及3*3方陣的全部方格
  • 規則同胞:一個方格的規則單元中除了本身的其餘方格

若是沒有任何限制,每一個方格可填入的數字能夠是123456789中的任何一個,而根據數獨遊戲規則,預先填入數字的方格會限制,該方格的規則單元中,其餘待填數方格的數字取值範圍。因此顯而易見的解決策略,即是根據限制規則,縮小取值範圍。學習

開始進行過濾淘汰以前,咱們須要的初始數獨方格字典中,表明空方格的(.)用可取值的數字範圍替換,初始範圍爲123456789spa

def grid_values(grid):
    valueLst = []
    digits = '123456789'
    for item in grid:
        if item == '.':
            valueLst.append(digits)
        elif item in digits:
            valueLst.append(item)
    return dict(zip(boxes, valueLst))

如此得到的初始數獨方格字典爲:線程

{
    'A1': '123456789',
    'A2': '123456789',
    'A3': '3',
    'A4': '123456789'
    'A5': '2',
    ...
    'I9': '123456789'
}
過濾淘汰策略:找到已肯定的數獨方格,再依次遍歷這些方格的規則同胞方格,從待肯定方格的取值範圍中,把已肯定的數字去掉,以縮小取值範圍。
def eliminate(values):
    solvedBoxes = [box for box in values.keys() if len(values[box]) == 1]
    for box in solvedBoxes:
        value = values[box]
        for peer in peers[box]:
            values[peer] = values[peer].replace(value, '')
    return values

過濾淘汰策略,是在規則單元上進行取值範圍縮小的。這隻覆蓋了數獨遊戲規則的一部分,而數獨規則還包括:
每一個最小規則單元中九個方格中的數字123456789僅出現一次。特別說明一下,最小規則單元
單行的九個方格,單列的九個方格,或3*3的九個方格,也能夠說一個規則單元包含了三個最小規則單元。code

3、策略2:惟一可選

根據最小規則單元,進一步縮小規則同胞中待填數的取值範圍,便引出了第二條規則:惟一可選策略

圖片描述

若是最小規則單元中,只有一個方格出現了某個數字,那麼這個方格就該填這個數字
def only_choice(values):
    for unit in unitlist:
        for digit in '123456789':
            places = [box for box in unit if digit in values[box]]
            if len(places) == 1:
                values[places[0]] = digit
    return values

交替使用過濾淘汰策略惟一可選策略即可將數獨問題中,全部待填數方格的取值範圍縮減至最小,但因爲這兩種策略循環使用的終止條件,是再也不有新肯定的填數方格出現,因此這並不充分能解決全部數獨問題。

def reduce_puzzle(values):
    stalled = False
    while not stalled:
        solved_values_before = len([box for box in values.keys() if len(values[box]) == 1])
        values = eliminate(values)
        values = only_choice(values)
        solved_values_after = len([box for box in values.keys() if len(values[box]) == 1])
        stalled = solved_values_before == solved_values_after
        if len([box for box in values.keys() if len(values[box]) == 0]):
            return False
    return values

4、策略3:約束搜索

對於方格預設數字比較多的數獨問題,或許能夠直接經過上述縮減取值範圍的方法解決。但當所給預設數字方格比較少時,在完成取值範圍縮小後,必然還會有一些取值不肯定的方格存在。如此問題的求解,就須要從多個可選值的方格中,分別假定其中一個進行搜索。

而此處針對進一步的搜索,有兩個問題須要考慮:

  1. 如何選取搜索起點方格?
  2. 肯定哪一種搜索策略:深度優先搜索,廣度優先搜索?

關於第一個問題,不管選擇哪一個方格起始搜索,對於可否解決問題來講並不存在差別。而從求解過程的性能和效率來考慮,就有了差異。而在思考第二個問題以前,還須要明確一點:數獨問題的解是否惟一?顯然若是預設的方格過多且彼此矛盾,問題必然無解,而預設的方格過少,勢必也會存在多個知足規則的解。因此爲了優先求得一個肯定解,咱們採起深度優先搜索,而如果求可能的全部解,多線程進行廣度優先搜索,能夠得到較好的時間複雜度,但卻須要暫存許多中間信息。

def search(values):
    values = reduce_puzzle(values)
    if values is False:
        return False
    if all(len(values[s]) == 1 for s in boxes):
        return values
    n,s = min((len(values[s]), s) for s in boxes if len(values[s]) > 1)
    for value in values[s]:
        new_values = values.copy()
        new_values[s] = value
        attemp = search(new_values)
        if attemp:
            return attemp

如此數獨問題得解,但能解決速度問題的程序就能成爲AI麼?

相關文章
相關標籤/搜索