1. 程式人生 > 其它 >數獨小專案開篇:DFS解決數獨難題

數獨小專案開篇:DFS解決數獨難題

技術標籤:演算法小課堂演算法pythonalgorithm

數獨小專案開篇:DFS解決數獨難題

前言

  這周小刀是挺忙的,週末加班,哎,誰不是996呢?(打工魂燃燒吧~

  這次我們來講講一個小專案——九宮格數獨

  有很多關於這個問題的益智遊戲,但是它的解法其實也可以用我們之前講的 DFS 來解決。

  不過我這次講的是個小專案,那自然會較為完整一些。在DFS演算法實現數獨的基礎上,利用影象處理來將一張數獨題目的圖片(可以從軟體上截圖獲取)進行資料讀取:即給我一張數獨的圖片,我就能給出這道數獨題目的解法。

在這裡插入圖片描述
  比如我們拿到上面的這張圖,我們可以利用軟體將它轉化為陣列,這樣就可以執行既定的數獨求解演算法,得到解。

  這其中涉及了影象處理,模板匹配等技術,當然我們的核心是數獨求解,別給我整些花裡胡哨的

  這個專案的靈感來自於我去年在GITHUB上看到的一個專案,那個博主也完成了類似的工作,還寫了UI介面,使得整個專案很完整,Nice

  接下來我們也分部分來講解這個小專案的完成過程吧

CSDN (゜-゜)つロ 乾杯

DFS解決數獨問題思路

  其實數獨問題很適合來進行DFS的全域性解空間搜尋,只要有解,那我們就可以找得到,因為最直接的做法就是暴力求解我嘗試所有的可能數字組合,總會get到答案,會有那麼一天!

  首先我們來拆解問題:

  給定一個數獨題目,有些位置有數,有些位置沒有數,沒有數的位置需要我們填進數字。而九宮格數獨的規則是:每行每列,每一個格子(包含九個數)內,都是數字1-9,且只出現一次。

在這裡插入圖片描述
  按照我們之前講的思路:我們DFS的起點肯定是第一個待填的格子,迴圈1-9,判斷是否可以在這個位置填下,如果可以,假設我填了數字5,更新一些狀態變數,然後遞迴到下一個待填的位置,繼續迴圈數字,找合適的填下,演算法結束的標誌就是所有待填的格子都被我填完了。

  這個問題其實核心操作很少,主要是一些資料讀取的過程和狀態變數的設定。


程式碼實現細節

  首先我們需要一個儲存待填格子行列座標的陣列:

point=[] # 待填入數字的格的座標

  然後我們有三個判斷規則,即每行每列每個宮格都要沒有重複,我們來建立狀態變數:

M=9
row=[[False for _ in range(M)] for _ in range(M)]
col=[[False for _ in range(M)] for _ in range(M)]
Mat=[[[False for _ in range(M)]for _ in range(3)] for k in range(3)]

  這裡row,col都是M*M的陣列(M只要大於9都可以),表示每行可能出現的各種數字標記,一共就9行,每行9個數字,所以是 9 * 9的陣列,而宮格是9個,一共三行三列,每個宮格9個數字,所以是3 * 3 * 9的陣列。

  假設我們的輸入格式是下面這樣,即待填位置的數用0表示:

8 0 0 0 0 0 0 0 0
0 0 3 6 0 0 0 0 0
0 7 0 0 9 0 2 0 0
0 5 0 0 0 7 0 0 0
0 0 0 0 4 5 7 0 0
0 0 0 1 0 0 0 3 0
0 0 1 0 0 0 0 6 8
0 0 8 5 0 0 0 1 0
0 9 0 0 0 0 4 0 0

  我們可以先讀取資料然後初始化狀態變數

Sudoku=[] # 儲存資料
for i in range(9):
    Sudoku.append(list(map(int,input().split())))
for i in range(9):
    for j in range(9):
        num=Sudoku[i][j]
        if num!=0:
            row[i][num]=True
            col[j][num]=True
            Mat[i//3][j//3][num]=True
        else:
            point.append((i,j))

  這裡i,j表示行列座標,當位於i行j列的數不是0,即已經填入的數,我們更新其所在行列在當前數字下的狀態變數,九宮格這裡注意要整除3,得到所在九宮格的正確行列數。

  如果是0,則儲存其行列位置到point陣列,以便後續使用

  還記得上次在斐波那契數列講到的DFS該注意的幾點嘛?(小刀打開了之前的碼稿……

在這裡插入圖片描述

  大致三個點:

  1. 第一個是搜尋策略,這是核心演算法,自不用多說
  2. 第二個是迴圈的前後參量傳遞,即我走過的路我不走了,或者通過一條失敗的路pass掉其他n條類似的路直接不走,節省時間空間。
  3. 第三個是退出條件,沒有退出條件,就是無限迴圈,Happy Ending!

  接下來便是DFS的核心函數了,大家可以按照上面講過的思路比對程式碼的實現,就會容易理解很多。

def dfs(num):
    """
    Function: DFS for Sudoku
    Arg:
        num(int):the number of boxes that need to be solved
    """
    nonlocal Sudoku, row, col, Mat, flag, point
    if flag:
        return
    # ending condition
    if num == -1:  # 填完全部
        # 列印結果
        flag = True
        return
    # recursion step
    for c in range(1, 10):
        x, y = point[num]
        if (row[x][c] == False) and (col[y][c] == False) and (Mat[x//3][y//3][c] == False):
            # set state
            row[x][c] = col[y][c] = Mat[x//3][y//3][c] = True
            Sudoku[x][y] = c

            dfs(num-1)
            # clear state
            row[x][c] = col[y][c] = Mat[x//3][y//3][c] = False
            Sudoku[x][y] = 0

  這裡遞迴的變數是待填格子的數量當格子數量為-1時,即表示已經填完所有的格子,解答完畢。令flag標誌為True,直接退出還在進行的其他遞迴狀態。

  再理一下思路:如果還沒填完,則讀取point裡面儲存的當前第num個格子的行列位置,遍歷數字1-9,如果當前行,當前列,當前宮格都還沒出現過該數字,則表示可以填下,令該位置資料為該數字,令num-1,繼續遞迴。

  記得退出該數字的遞迴時,要清除選定該數字所標記的狀態變數和Sudoku陣列資料,才不會影響到其他遞迴狀態喔~

樣例測試

  我們來看剛才的那組資料的測試結果:

>>> Sudoku()
8 0 0 0 0 0 0 0 0
0 0 3 6 0 0 0 0 0
0 7 0 0 9 0 2 0 0
0 5 0 0 0 7 0 0 0
0 0 0 0 4 5 7 0 0
0 0 0 1 0 0 0 3 0
0 0 1 0 0 0 0 6 8
0 0 8 5 0 0 0 1 0
0 9 0 0 0 0 4 0 0

60 boxes to solve: 
8 1 2 | 7 5 3 | 6 4 9 
9 4 3 | 6 8 2 | 1 7 5 
6 7 5 | 4 9 1 | 2 8 3
---------------------
1 5 4 | 2 3 7 | 8 9 6 
3 6 9 | 8 4 5 | 7 2 1 
2 8 7 | 1 6 9 | 5 3 4 
---------------------
5 2 1 | 9 7 4 | 3 6 8 
4 3 8 | 5 2 6 | 9 1 7 
7 9 6 | 3 1 8 | 4 5 2 
Used 0.29929 s

  差不多用了0.3s,還是可以接受的。

  再來看我們引言用到的圖片的數獨:

在這裡插入圖片描述

>>> Sudoku()
58 boxes to solve: 
4 9 2 | 8 6 3 | 7 1 5 
3 7 8 | 1 4 5 | 6 2 9 
5 6 1 | 2 7 9 | 8 4 3 
---------------------
7 1 5 | 6 9 2 | 4 3 8 
2 4 3 | 7 1 8 | 9 5 6 
9 8 6 | 3 5 4 | 1 7 2 
---------------------
6 5 4 | 9 2 1 | 3 8 7 
1 3 7 | 5 8 6 | 2 9 4 
8 2 9 | 4 3 7 | 5 6 1 
Used 0.06116 s

  這道題看來對計算機來講比較好做,只用了0.06s,反正我是做不出來(