高效算法求解數獨



title: 高效算法求解數獨
date: 2019-12-26 17:55:16
tags: 數據結構與算法
categories: 數據結構與算法


背景

  之前上python課的時候,有一次實驗是求解數獨,要求時間復雜度要低;為此老師講解了一個高效的數獨算法,我覺得算法挺有意思的,寫篇博客記錄一下。


描述

首先需要知曉數獨的兩個規則:

  1. 若某個位置的值已經確定,那么,和這個位置在同一行,同一列,同一個3×3的格子,都不能填寫這個值,比如,九宮格(1,1)位置的值為2,那么,第一行,第一列,以及第一個3×3的格子里,都不能在填2了;
  2. 若某一行,或者某一列,或者某一個3×3里面,只有一個位置可能填1(假如是1),那么1一定是填寫在這個位置,因為沒有其他位置可以填它了;、

求解步驟

  1. 創建一個三維數組,假設就叫“可能值數組”,記錄數獨9×9的81個位置中,每個位置可能填寫的值,初始情況下,每個位置的可能值都是1到9,表示每個位置都可能填寫1-9中任何一個數字;

  2. 遍歷數獨的每一個位置,若某個位置已經有值,則將這個位置的可能值更新為這個值,比如,九宮格上,(1,1)的值已經確定是2了,那就將三維數組中(1,1)位置的可能值從[1-9]更新為[2],直到所有的位置更新完畢;

  3. 使用上述規則1進行剪枝:

    (1):從第一個位置開始遍歷九宮格,若當前遍歷到的位置(i,j),它的值已經知曉,那么就更新可能值數組,將第i行,第j列,以及其對應的3×3(【i/3×3 , j/3×3】就是這個3×3的第一個點)的所有位置,它們的可能值都要去除(i,j)位置的值;

    (2):若某個位置在經過上一步的剪枝后,可能值只剩下一個了,那這個位置的值就確定了,比如說,位置(1,1)的初始可能值是1到9,經過上面的一步步去除,只剩下一個3了,那這個(1,1)位置填寫的值必定就是3了。此時我們可以再次使用規則1,即第一行,第一列,以及其對應的3×3中,所有的格子的可能值不能有3;

    (3):依次遍歷每一個位置,使用上面的規則1,直到最后一格子,第一次剪枝便完成了;

  4. 使用上面的規則2進行剪枝:

    (1):統計每一行,每一列,以及每一個3×3中,每個數出現的次數,比如統計第一行每個格子的可能值,看1-9各出現幾次,若某個可能值只出現一次,那出現這個值的位置,就是填寫這個值,比如說,在第一行,3這個數字,只有(1,1)這個位置可能填寫,那(1,1)就是填3,因為其他位置的可能值當中都不包含3,也就是都不能填寫3;

    (2):根據上一步確定了某個位置的值后,那我們此時又可以使用規則1了,比如,上一步確定了(1,1)是填寫3,那么第一行,第一列,以及第一個3×3中其余的格子, 都不能在寫3了,我們從可能值數組中,將這些位置的可能,值刪除3這個數;

    (3):而此時,又可能出現上面的第3步中的(3)的情況;

  5. 規則2剪枝完畢后,數獨還沒有解決完畢,那我們只能通過枚舉每一個位置的值,來一一嘗試,直到找到最后的解決辦法:

    (1):我們在最開始創建了一個三維數組,存儲每一個位置的可能值,初始情況下,每個位置的可能值都是1-9,但是經過上面兩個規則的剪枝后,已經去除了很多;

    (2):此時我們使用DFS深度優先搜索,嘗試為每一個位置填值。經過上面的剪枝,每個位置的可能值的數量應該不一樣了,而為了減少DFS搜索的次數,我們應該從可能值最少的位置開始搜索;

    (3):遍歷9宮格,找出還未填寫值,且可能值最少的那個位置(可能有多個,找出第一個),嘗試將他的第一個可能值填寫在這個位置,然后再次調用規則1和規則2進行剪枝,剪枝完畢后,判斷當前的九宮格中,是否有不和規則的地址,比如同一行出現兩個一樣的數。若沒有不合法的地方,則再次進行一次(3),若有,表示這個位置不能填這個值,則從這個位置的可能值中再選擇另外一個;

    (4):一直使用步驟(3),直到所有的位置都確定,則表示成功解出數獨,若有某個位置,它的任何一個可能值填上去,都不能得到最終結果,那數獨就是無解的;

  6. 經過上面這些步驟,就能快速的解出數獨,因為主要通過規則1,2進行剪枝,大大減少了枚舉的次數,提升了效率;


所需計算

  1. 已知位置(i,j),則這個位置所在的3*3,其第一個點是(i/3×3 , j/3×3),i/3×3表示先作除法,去除了小數部分,再乘3,就是3的倍數了;
  2. 已知位置(i,j),如何計算這個位置屬於第幾個3×3,那就是(i/3×3 + j/3),每個3*3都占3行,且3列,i/3得到這個位置在第幾個3行,j/3得到這個位置在第幾個3列,每三行有三個3×3,所以i/3×3 + j/3就可以得到這個位置在第幾個3×3;

代碼

因為是為了完成python實驗,所以代碼是用python寫的:

# 此類用來表示搜索時,需要搜索的一個位置
# x,y為此位置的坐標,size為此位置的可能值的數量
class Node:
    def __init__(self, x, y, size):
        self.x = x
        self.y = y
        self.size = size

# 讀取方式2,讀取top95
def read_way2():
    # 從文件中讀取初始數獨
    value = [[0] * 10 for i in range(10)]
    # 讀取文件2,3
    s = infile.readline();
    if s == '':
        return
    for i in range(9):
        for j in range(9):
            value[i][j] = int(s[i * 9 + j])
    return value

# 初始化函數
def init():
    value = read_way2()   # 讀取top95
    # 初始化possibleValue,若當前位置有值,則其可能值就是這個值本身
    # 若沒有值,則初始的可能值就是1-9
    possibleValue = [[[] for j in range(10)] for i in range(10)]
    for i in range(9):
        for j in range(9):
            if value[i][j] != 0:
                possibleValue[i][j] = [value[i][j]]
            else:
                possibleValue[i][j] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

    return possibleValue

#####################################################################################################################

# 根據規則1進行剪枝
# 遍歷所有的位置,找到已經確定的位置進行剪枝
def pruningByRule1(possibleValue):
    for i in range(9):
        for j in range(9):
            if len(possibleValue[i][j]) == 1:
                removeValueByRule1(i, j, possibleValue)    # 以當前位置為起點,移除重復的可能值


# 在規則1剪枝中,將同一區域中,已經確定的數移除
# 以(i,j)位置為起點,移除重復的可能值
def removeValueByRule1(i, j, possibleValue):
    # 與當前值在同一行或同一列的位置,可能值減去當前位置的值
    for k in range(9):
        # 從第i行中的可能值列表中,移除當前值
        confirmOneValueInRule1(i, k, possibleValue[i][j][0], possibleValue)
        # 從第i列中的可能值列表中,移除當前值
        confirmOneValueInRule1(k, j, possibleValue[i][j][0], possibleValue)

    # 與當前值在同3*3的位置,可能值減去當前位置的值
    for k in range(int(i / 3) * 3, int(i / 3) * 3 + 3):
        for l in range(int(j / 3) * 3, int(j / 3)* 3 + 3):
            confirmOneValueInRule1(k, l, possibleValue[i][j][0], possibleValue)

# 移除某個位置的可能值,並在移除后判斷能否得到確定值
def confirmOneValueInRule1(i, j, num, possibleValue):
    if len(possibleValue[i][j]) == 1:
        return
    # 從當前位置的可能值中,移除已經確定的數
    if num in possibleValue[i][j]:
        possibleValue[i][j].remove(num)
    # 判斷移除后,當前位置能否確定
    if len(possibleValue[i][j]) == 1:
        # 若當前位置確定,則以當前位置為基准進行移除操作
        removeValueByRule1(i, j, possibleValue)


###########################################################################################

# 根據規則2剪枝,判斷同一個區域每個值可能出現的次數
# 若某個值可能出現的位置只有一個,表示這個值就在此位置
def pruningByRule2(possibleValue):
    # 統計第i行,數字j可能值出現了幾次
    countX = [[0] * 10 for i in range(12)]
    # 統計第i列,數字j可能值出現了幾次
    countY = [[0] * 10 for i in range(12)]
    # 統計第i個3*3,數字j可能值出現了幾次
    countZ = [[0] * 10 for i in range(12)]

    # 統計各個區域可能值出現的次數
    for i in range(9):
        for j in range(9):
            for num in possibleValue[i][j]:
                countX[i][num] += 1
                countY[j][num] += 1
                countZ[i // 3 * 3 + j // 3][num] += 1

    # 判斷哪些數字只出現了一次, 若只出現了一次的數字
    # 表示這個數字就是那個位置的答案
    for i in range(9):
        for j in range(1,10):
            # 若第i行數字j只出現了一次
            if countX[i][j] == 1:
                for k in range(9):  # 遍歷第i行的每一列,判斷這個唯一值出現在哪
                    confirmValueInRule2(i, k, j, possibleValue)

            # 若第i列數字j只出現了一次
            if countY[i][j] == 1:
                for k in range(9):  # 遍歷第i列的每一列,判斷這個唯一值出現在哪
                    confirmValueInRule2(k, i, j, possibleValue)

            # 若第i個3 * 3中,數字j的可能值只有一個
            if countZ[i][j] == 1:
                # 遍歷第i個3*3的所有位置,判斷這個唯一值出現在哪
                for k in range(i//3*3, i//3*3+3):
                    for l in range(i%3*3, i%3*3+3):
                        confirmValueInRule2(k, l, j, possibleValue)


# 判斷當前位置是否包含某個數,包含則為此位置的答案
def confirmValueInRule2(i, j, singleNum, possibleValue):
    # 若當前位置已經確定值了, 直接返回
    if len(possibleValue[i][j]) ==1:
        return
    # 若當前位置包含唯一可能值,則這個位置的確定值就是它
    if singleNum in possibleValue[i][j]:
            possibleValue[i][j] = [singleNum]
            # 重新調用規則1
            removeValueByRule1(i, j, possibleValue)

###########################################################################################

# 遞歸搜索
def searchForPruning(node, possibleValue):
    # 若沒有需要填值的點了,表示搜索結束,答案已出
    if node is None:
        return possibleValue

    # 獲取當前位置的x,y坐標
    x = node[0]
    y = node[1]
    for num in possibleValue[x][y]:
        # 復制一份當前狀態
        tempPossibleValue = copy.deepcopy(possibleValue)
        # 更新數據
        tempPossibleValue[x][y] = [num]
        # 調用規則1,2
        removeValueByRule1(x, y, tempPossibleValue)
        pruningByRule2(tempPossibleValue)

        # 調用規則1,2后,判斷當前結果是否合法,若合法,則進行遞歸下一層
        if judge_result(tempPossibleValue):
            # 遞歸求解
            tempPossibleValue = searchForPruning(get_lowest_node(tempPossibleValue), tempPossibleValue)
            # 判斷遞歸結果,若結果有返回值,則表示求解成功
            if tempPossibleValue is not None:
                return tempPossibleValue

# 獲取當前可能值最小的位置
def get_lowest_node(possibleValue):
    minn = 100
    node = None
    for i in range(9):
        for j in range(9):
            # 若當前位置沒有確定值,並且可能值的數量更少,則更新記錄,
            if 1 < len(possibleValue[i][j]) < minn:
                minn = len(possibleValue[i][j])
                node = (i, j)
    return node


# 判斷某個位置是否可以放某個值
def judge_result(possibleValue):
    # 標記某個數字是否出現
    countX = [[False] * 10 for i in range(12)]
    countY = [[False] * 10 for i in range(12)]
    countZ = [[False] * 10 for i in range(12)]

    # 統計各個區域可能值出現的次數
    for i in range(9):
        for j in range(9):
            if len(possibleValue[i][j]) == 1:
                # 若當前狀態不合法,返回false
                if countX[i][possibleValue[i][j][0]] or countY[j][possibleValue[i][j][0]] or countZ[i // 3 * 3 + j // 3][possibleValue[i][j][0]]:
                    return False
                # 若合法,則標記已經確定的數字
                countX[i][possibleValue[i][j][0]] = True
                countY[j][possibleValue[i][j][0]] = True
                countZ[i // 3 * 3 + j // 3][possibleValue[i][j][0]] = True
    return True


# 判斷某個位置是否可以放某個值
def judge_now_number(possibleValue, i, j, num):
    # 判斷num在這一行和這一列是否被使用
    for k in range(9):
        if len(possibleValue[i][k]) == 1 and possibleValue[i][k][0] == num:
            return False
        if len(possibleValue[k][j]) == 1 and possibleValue[k][j][0] == num:
            return False
    # 判斷num在這個3*3是否被使用
    for k in range(int(i / 3) * 3, int(i / 3) * 3 + 3):
        for l in range(int(j / 3) * 3, int(j / 3) * 3 + 3):
            if len(possibleValue[k][l]) == 1 and possibleValue[k][l][0] == num:
                return False
    return True

###########################################################################################


# 輸出展示可能值列表
def display(possibleValue):
    for i in range(9):
        for j in range(9):
            print(possibleValue[i][j], end="---")
        print()
    print()

###########################################################################################

# 主函數
def main():
    start = time.time()
    c = 0
    # 主邏輯
    while True:
        # 調用初始化函數
        possibleValue = init()
        # 調用規則1剪枝
        pruningByRule1(possibleValue)
        # 調用規則2剪枝
        pruningByRule2(possibleValue)
        # display(possibleValue)

        possibleValue = searchForPruning(get_lowest_node(possibleValue), possibleValue)
        # 判斷是否有解
        if possibleValue is not None:
            display(possibleValue)
        else:
            print("無解")

        if not judge_result(possibleValue):
            print("結果異常")

        c += 1
        if c >= 90:
            break
    end = time.time()
    print(end - start)


# 讀取本地存儲文件
infile = open("D:/top95.txt")
main()

擴展

  數獨求解的算法,上面這種並不是最快的,還有一種叫做舞蹈鏈(Dancing Links)的算法,效率更高,有興趣的可以了解一下;


免責聲明!

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



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