算法實踐——舞蹈鏈(Dancing Links)算法求解數獨


在“跳躍的舞者,舞蹈鏈(Dancing Links)算法——求解精確覆蓋問題”一文中介紹了舞蹈鏈(Dancing Links)算法求解精確覆蓋問題。

本文介紹該算法的實際運用,利用舞蹈鏈(Dancing Links)算法求解數獨

 

在前文中可知,舞蹈鏈(Dancing Links)算法在求解精確覆蓋問題時效率驚人。

那利用舞蹈鏈(Dancing Links)算法求解數獨問題,實際上就是下面一個流程

1、把數獨問題轉換為精確覆蓋問題

2、設計出數據矩陣

3、用舞蹈鏈(Dancing Links)算法求解該精確覆蓋問題

4、把該精確覆蓋問題的解轉換為數獨的解

 

首先看看數獨問題(9*9的方格)的規則

1、每個格子只能填一個數字

2、每行每個數字只能填一遍

3、每列每個數字只能填一遍

4、每宮每個數字只能填一遍(宮的概念,參看“算法實踐——數獨的基本解法”)

 

那現在就是利用這個規則把數獨問題轉換為精確覆蓋問題

可是,直觀上面的規則,發現比較難以轉換為精確覆蓋問題。因此,把上面的表述換個說法

1、每個格子只能填一個數字

2、每行1-9的這9個數字都得填一遍(也就意味着每個數字只能填一遍)

3、每列1-9的這9個數字都得填一遍

4、每宮1-9的這9個數字都得填一遍

 

這樣理解的話,數獨問題轉換為精確覆蓋問題就相對簡單多了。關鍵就是如何構造精確覆蓋問題中的矩陣

 

我們把矩陣的每個列都定義成一個約束條件。

 

第1列定義成:(1,1)填了一個數字

第2列定義成:(1,2)填了一個數字

……

第9列定義成:(1,9)填了一個數字

第10列定義成:(2,1)填了一個數字

……

第18列定義成:(2,9)填了一個數字

……

第81列定義成:(9,9)填了一個數字

至此,用第1-81列完成了約束條件1:每個格子只能填一個數字

第N列(1≤N≤81)定義成:(X,Y)填了一個數字。

N、X、Y之間的關系是:X=INT((N-1)/9)+1;Y=((N-1) Mod 9)+1;N=(X-1)×9+Y

 

 

第82列定義成:在第1行填了數字1

第83列定義成:在第1行填了數字2

……

第90列定義成:在第1行填了數字9

第91列定義成:在第2行填了數字1

……

第99列定義成:在第2行填了數字9

……

第162列定義成:在第9行填了數字9

至此,用第82-162列(共81列)完成了約束條件2:每行1-9的這9個數字都得填一遍

第N列(82≤N≤162)定義成:在第X行填了數字Y。

N、X、Y之間的關系是:X=INT((N-81-1)/9)+1;Y=((N-81-1) Mod 9)+1;N=(X-1)×9+Y+81

 

 

第163列定義成:在第1列填了數字1

第164列定義成:在第1列填了數字2

……

第171列定義成:在第1列填了數字9

第172列定義成:在第2列填了數字1

……

第180列定義成:在第2列填了數字9

……

第243列定義成:在第9列填了數字9

至此,用第163-243列(共81列)完成了約束條件3:每列1-9的這9個數字都得填一遍

第N列(163≤N≤243)定義成:在第X列填了數字Y。

N、X、Y之間的關系是:X=INT((N-162-1)/9)+1;Y=((N-162-1) Mod 9)+1;N=(X-1)×9+Y+162

 

 

第244列定義成:在第1宮填了數字1

第245列定義成:在第1宮填了數字2

……

第252列定義成:在第1宮填了數字9

第253列定義成:在第2宮填了數字1

……

第261列定義成:在第2宮填了數字9

……

第324列定義成:在第9宮填了數字9

至此,用第244-324列(共81列)完成了約束條件4:每宮1-9的這9個數字都得填一遍

第N列(244≤N≤324)定義成:在第X宮填了數字Y。

N、X、Y之間的關系是:X=INT((N-243-1)/9)+1;Y=((N-243-1) Mod 9)+1;N=(X-1)×9+Y+243

 

至此,用了324列完成了數獨的四個約束條件,矩陣的列定義完成

那接下來,就是把數獨轉換為矩陣

數獨問題中,每個格子分兩種情況。有數字的格子、沒數字的格子。

 

有數字的格子

以例子來說明,在(4,2)中填的是7

把(4,2)中填的是7,解釋成4個約束條件

1、在(4,2)中填了一個數字。

2、在第4行填了數字7

3、在第2列填了數字7

4、在第4宮填了數字7(坐標(X,Y)到宮N的公式為:N=INT((X-1)/3)×3+INT((Y-1)/3)+1)

 

那么這4個條件,分別轉換成矩陣對應的列為

1、在(4,2)中填了一個數字。對應的列N=(4-1)×9+2=29

2、在第4行填了數字7。對應的列N=(4-1)×9+7+81=115

3、在第2列填了數字7。對應的列N=(2-1)×9+7+162=178

4、在第4宮填了數字7。對應的列N=(4-1)×9+7+243=277

 

於是,(4,2)中填的是7,轉成矩陣的一行就是,第29、115、178、277列是1,其余列是0。把這1行插入到矩陣中去。

 

沒數字的格子

還是舉例說明,在(5,8)中沒有數字

把(5,8)中沒有數字轉換成

(5,8)中填的是1,轉成矩陣的一行就是,第44、118、226、289列是1,其余列是0。

(5,8)中填的是2,轉成矩陣的一行就是,第44、119、227、290列是1,其余列是0。

(5,8)中填的是3,轉成矩陣的一行就是,第44、120、228、291列是1,其余列是0。

(5,8)中填的是4,轉成矩陣的一行就是,第44、121、229、292列是1,其余列是0。

(5,8)中填的是5,轉成矩陣的一行就是,第44、122、230、293列是1,其余列是0。

(5,8)中填的是6,轉成矩陣的一行就是,第44、123、231、294列是1,其余列是0。

(5,8)中填的是7,轉成矩陣的一行就是,第44、124、232、295列是1,其余列是0。

(5,8)中填的是8,轉成矩陣的一行就是,第44、125、233、296列是1,其余列是0。

(5,8)中填的是9,轉成矩陣的一行就是,第44、126、234、297列是1,其余列是0。

把這9行插入到矩陣中。由於這9行的第44列都是1(不會有其他行的44列會是1),也就是說這9行中必只有1行(有且只有1行)選中(精確覆蓋問題的定義,每列只能有1個1),是最后解的一部分。這就保證了最后解在(5,8)中只有1個數字。

 

這樣,從數獨的格子依次轉換成行(1行或者9行)插入到矩陣中。完成了數獨問題到精確覆蓋問題的轉換。

接下來求解精確覆蓋問題就交給舞蹈鏈(Dancing Links)算法,詳情參看“跳躍的舞者,舞蹈鏈(Dancing Links)算法——求解精確覆蓋問題

 


Public  Interface  I_Sudoku
    Function SetLine( ByVal Row As  Integer, ByVal  ParamArray Value() As  Integer) As  Boolean
    Function Calculate() As  String
    Sub Init()
End  Interface

Public  Class  clsSudokuDLX
    Implements  I_Sudoku

    Private _Dance As  clsDancingLinks

    Private _Num(80) As  Integer

    Public  Sub  New()
        Init()
    End  Sub

    Public  Function SetLine(Row As  Integer, ParamArray Value() As  Integer) As  Boolean  Implements  I_Sudoku.SetLine
        Dim I As  Integer
        For I = 0 To IIf(Value.Length < 10, Value.Length - 1, 8)
            _Num(Row * 9 - 9 + I) = Value(I)
        Next
        Return  True
    End  Function

    Public  Function Calculate() As  String  Implements  I_Sudoku.Calculate
        Dim I As  Integer, J As  Integer
        Dim X As  Integer, Y As  Integer
        Dim Index As  New  List( Of  Integer), Value As  New  List( Of  Integer)
        Dim C1 As  Integer, C2 As  Integer, C3 As  Integer, C4 As  Integer
        For I = 0 To 80
            X = Int(I / 9)
            Y = I Mod 9
            If _Num(I) > 0 Then
                C1 = X * 9 + Y + 1
                C2 = X * 9 + _Num(I) + 81
                C3 = Y * 9 + _Num(I) + 162
                C4 = (Int(X / 3) * 3 + Int(Y / 3)) * 9 + _Num(I) + 243
                _Dance.AppendLineByIndex(C1, C2, C3, C4)
                Index.Add(I)
                Value.Add(_Num(I))
            Else
                C1 = X * 9 + Y + 1
                C2 = X * 9 + 1 + 81
                C3 = Y * 9 + 1 + 162
                C4 = (Int(X / 3) * 3 + Int(Y / 3)) * 9 + 1 + 243
                _Dance.AppendLineByIndex(C1, C2, C3, C4)
                Index.Add(I)
                Value.Add(1)

                For J = 2 To 9
                    _Dance.AppendLineByIndex(C1, C2 + J - 1, C3 + J - 1, C4 + J - 1)
                    Index.Add(I)
                    Value.Add(J)
                Next
            End  If
        Next

        Dim P() As  Integer = _Dance.Dance

        For I = 0 To 80
            _Num(Index.Item(P(I) - 1)) = Value.Item(P(I) - 1)
        Next

        Dim V As  String = ""
        For I = 0 To 80
            V = V & _Num(I) & "  "
            If I Mod 9 = 8 Then V &= vbNewLine
        Next
        Return V

    End  Function

    Public  Sub Init() Implements  I_Sudoku.Init
        _Dance = New  clsDancingLinks(324)
        Dim I As  Integer
        For I = 0 To 80
            _Num(I) = 0
        Next
    End  Sub
End  Class

上面的代碼給出了clsSudokuDLX的代碼,通過調用clsDancingLinks類來求解數獨。I_Sudoku接口沒什么特殊意義,僅僅是為了測試方便而已。clsDancingLinks類的代碼這里就不貼了,在“跳躍的舞者,舞蹈鏈(Dancing Links)算法——求解精確覆蓋問題”里有

 

對三個數獨問題求解來對比算法的效率

先看看三個數獨

數獨一:簡單的數獨

image

 

數獨二:有點難度的數獨

image

 

數獨三:高難度的數獨。雖然和數獨二比較僅僅差了一個數字的位置,但是求解的難度提高了不止一個等級。

image

 

 

時間效率的對比

 

我們分別對三個數獨進行百次的求解,剔除明顯異於平均的時間,再取平均值

 

暴力破解法

數獨一:0.114毫秒

數獨二:0.238毫秒

數獨三:15.706毫秒

 

舞蹈鏈(Dancing Links)算法

數獨一:876.161毫秒(這個差距有點大,近乎7700倍,和我的期望值差距比較大)

數獨二:775839.5毫秒=775秒840毫秒≈12.93分鍾(我只做了三次測試,第一次等了5分鍾,沒結果,就退出了;第二次777348毫秒;第三次774331毫秒)

數獨三:只做了一次測試,時間約40分鍾,還沒結果,就退出了

 

從上面的測試結果來看,舞蹈鏈(Dancing Links)算法從時間效率的角度來看,是完敗於暴力破解法的

 

空間效率的對比

 

暴力破解法

數獨一:在求解的過程中,一路唯一數單元格到底,沒有緩存數據。故空間占用81字節。

數獨二:在求解的過程中,最多向棧Q緩存了12步的數據。故占用空間81+12*81=972字節

數獨三:在求解的過程中,最多向棧Q緩存了21步的數據。故占用空間81+21*81=1782字節

 

舞蹈鏈(Dancing Links)算法

數獨一:題目提供了32個數字,則矩陣一共有32*1+(81-32)*9=473行,每行4個節點,則一共有473*4+324+1=2217個節點。每個節點6個分量,則一共要13302個字節。另程序在每行額外提供2個字節緩存相關信息。故一共要11844+473*2=14248字節

數獨二:題目提供了21個數字,則矩陣一共有21*1+(81-21)*9=561行,每行4個節點,則一共有561*4+324+1=2569個節點。每個節點6個分量,則一共要15414個字節。另程序在每行額外提供2個字節緩存相關信息。故一共要15414+561*2=16536字節

數獨三:題目提供了21個數字,則矩陣一共有21*1+(81-21)*9=561行,每行4個節點,則一共有561*4+324+1=2569個節點。每個節點6個分量,則一共要15414個字節。另程序在每行額外提供2個字節緩存相關信息。故一共要15414+561*2=16536字節

 

以上的分析,舞蹈鏈(Dancing Links)算法從空間效率的角度來看,是完敗於暴力破解法的

 

做分析做到這,結果出乎我的意料。雖然我估計舞蹈鏈(Dancing Links)算法不見得優於暴力破解法,但沒想到差距會那么大。不過,也可以理解,舞蹈鏈(Dancing Links)算法僅僅是利用了十字雙向循環鏈的數據結構解決了緩存和回溯的難題,但本質上還是回溯法,還是暴力破解法。

 

 

針對該數獨問題進行優化舞蹈鏈(Dancing Links)算法

 

回顧前文“跳躍的舞者,舞蹈鏈(Dancing Links)算法——求解精確覆蓋問題”,可以發現,在Dance(K)函數調用的時候,是直接調用_Head.Right來獲得未求解列。由於精確覆蓋問題是要求每個列都要覆蓋到,因此,在算法中調用未求解列的先后順序那就不是最重要了。假如,現在有兩個未求解列C1和C2,C1列有8個元素,C2列有4個元素。最壞的情況,從C1列求解,需要調用8次Dance(K+1),而從C2列求解,需要調用4次Dance(K+1)。感覺上從C2列求解比從C1列求解效率要高些。因此,在Dance(K)函數調用的時候,先找尋列元素最少的未求解列,再依次求解,可能效率會高點。我們把這個稱之為改進的舞蹈鏈(Improve Dancing Links)算法

 

給每個列首元素(除卻Head元素)添加一個Count分量,表示這個列首所在列的其他元素的個數。

因此,在原算法的基礎上,把C1=Head.Right改成獲得Count分量最少的列首元素。代碼貼於下方,修改的部分用紅色標示


Public  Class  clsDancingLinksImprove
    Private Left() As  Integer, Right() As  Integer, Up() As  Integer, Down() As  Integer
    Private Row() As  Integer, Col() As  Integer

    Private _Head As  Integer

    Private _Rows As  Integer, _Cols As  Integer, _NodeCount As  Integer
    Private Count() As Integer

    Private Ans() As  Integer

    Public  Sub  New( ByVal Cols As  Integer)
        ReDim Left(Cols), Right(Cols), Up(Cols), Down(Cols), Row(Cols), Col(Cols), Ans(Cols)
        ReDim Count(Cols)
        Dim I As  Integer

        Up(0) = 0
        Down(0) = 0
        Right(0) = 1
        Left(0) = Cols

        For I = 1 To Cols
            Up(I) = I
            Down(I) = I
            Left(I) = I - 1
            Right(I) = I + 1
            Col(I) = I
            Row(I) = 0

            Count(I) = 0
        Next

        Right(Cols) = 0

        _Rows = 0
        _Cols = Cols
        _NodeCount = Cols
        _Head = 0
    End  Sub

    Public  Sub AppendLine( ByVal  ParamArray Value() As  Integer)
        _Rows += 1
        If Value.Length = 0 Then  Exit Sub

        Dim I As  Integer, K As  Integer = 0

        For I = 0 To Value.Length - 1
            If Value(I) = 1 Then
                _NodeCount += 1
                ReDim  Preserve Left(_NodeCount)
                ReDim  Preserve Right(_NodeCount)
                ReDim  Preserve Up(_NodeCount)
                ReDim  Preserve Down(_NodeCount)
                ReDim  Preserve Row(_NodeCount)
                ReDim  Preserve Col(_NodeCount)
                ReDim  Preserve Ans(_NodeCount)
                If K = 0 Then
                    Left(_NodeCount) = _NodeCount
                    Right(_NodeCount) = _NodeCount
                    K = 1
                Else
                    Left(_NodeCount) = _NodeCount - 1
                    Right(_NodeCount) = Right(_NodeCount - 1)
                    Left(Right(_NodeCount - 1)) = _NodeCount
                    Right(_NodeCount - 1) = _NodeCount
                End  If

                Down(_NodeCount) = I + 1
                Up(_NodeCount) = Up(I + 1)
                Down(Up(I + 1)) = _NodeCount
                Up(I + 1) = _NodeCount

                Row(_NodeCount) = _Rows
                Col(_NodeCount) = I + 1

                Count(I + 1) += 1
            End  If
        Next

    End  Sub

    Public  Sub AppendLineByIndex( ByVal  ParamArray Index() As  Integer)
        _Rows += 1
        If Index.Length = 0 Then  Exit Sub

        Dim I As  Integer, K As  Integer = 0

        ReDim  Preserve Left(_NodeCount + Index.Length)
        ReDim  Preserve Right(_NodeCount + Index.Length)
        ReDim  Preserve Up(_NodeCount + Index.Length)
        ReDim  Preserve Down(_NodeCount + Index.Length)
        ReDim  Preserve Row(_NodeCount + Index.Length)
        ReDim  Preserve Col(_NodeCount + Index.Length)
        ReDim  Preserve Ans(_NodeCount + Index.Length)

        For I = 0 To Index.Length - 1

            _NodeCount += 1

            If I = 0 Then
                Left(_NodeCount) = _NodeCount
                Right(_NodeCount) = _NodeCount
            Else
                Left(_NodeCount) = _NodeCount - 1
                Right(_NodeCount) = Right(_NodeCount - 1)
                Left(Right(_NodeCount - 1)) = _NodeCount
                Right(_NodeCount - 1) = _NodeCount
            End  If

            Down(_NodeCount) = Index(I)
            Up(_NodeCount) = Up(Index(I))
            Down(Up(Index(I))) = _NodeCount
            Up(Index(I)) = _NodeCount

            Row(_NodeCount) = _Rows
            Col(_NodeCount) = Index(I)

            Count(Index(I)) += 1
        Next
    End  Sub

    Public  Function Dance() As  Integer()
        Return IIf(Dance(0) = True, Ans, Nothing)
    End  Function

    Private  Function Dance( ByVal K As  Integer) As  Boolean
        If (Right(_Head) = _Head) Then
            ReDim  Preserve Ans(K - 1)
            Return  True
        End  If

        Dim P As Integer, C1 As Integer
        P = Right(_Head)
        C1 = P
        Do While P <> _Head
            If Count(P) < Count(C1) Then C1 = P
            P = Right(P)
        Loop

        If Count(C1) < 1 Then Return False

        RemoveCol(C1)

        Dim I As  Integer, J As  Integer

        I = Down(C1)
        Do  While I <> C1
            Ans(K) = Row(I)

            J = Right(I)
            Do  While J <> I
                RemoveCol(Col(J))
                J = Right(J)
            Loop

            If Dance(K + 1) Then  Return  True

            J = Left(I)
            Do  While J <> I
                ResumeCol(Col(J))
                J = Left(J)
            Loop

            I = Down(I)
        Loop

        ResumeCol(C1)
        Return  False
    End  Function

    Public  Sub RemoveCol( ByVal ColIndex As  Integer)

        Left(Right(ColIndex)) = Left(ColIndex)
        Right(Left(ColIndex)) = Right(ColIndex)

        Dim I As  Integer, J As  Integer

        I = Down(ColIndex)
        Do  While I <> ColIndex
            J = Right(I)
            Do  While J <> I
                Up(Down(J)) = Up(J)
                Down(Up(J)) = Down(J)

                Count(Col(J)) -= 1

                J = Right(J)
            Loop

            I = Down(I)
        Loop

    End  Sub

    Public  Sub ResumeCol( ByVal ColIndex As  Integer)

        Left(Right(ColIndex)) = ColIndex
        Right(Left(ColIndex)) = ColIndex

        Dim I As  Integer, J As  Integer

        I = Up(ColIndex)

        Do  While (I <> ColIndex)
            J = Right(I)
            Do  While J <> I
                Up(Down(J)) = J
                Down(Up(J)) = J

                Count(Col(J)) += 1

                J = Right(J)
            Loop
            I = Up(I)
        Loop

    End  Sub
End  Class

這樣修改后還有一個好處是,如果Count分量最少的列首元素的Count分量是0的話,那么說明當前無解(說明沒有行能覆蓋該列首元素所在的列),直接返回False,省掉調用Dance(K+1)的步驟。

 

看看改進的舞蹈鏈(Improve Dancing Links)算法的效率

 

時間效率

數獨一:6.31毫秒

數獨二:8.50毫秒

數獨三:11.34毫秒

 

空間效率

數獨一:題目提供了32個數字,則矩陣一共有32*1+(81-32)*9=473行,每行4個節點,則一共有473*4+324+1=2217個節點。每個節點6個分量,則一共要13302個字節。另程序在每行額外提供2個字節緩存相關信息,每個列要增加Count分量。故一共要13302+473*2+325=14573字節

數獨二:題目提供了21個數字,則矩陣一共有21*1+(81-21)*9=561行,每行4個節點,則一共有561*4+324+1=2569個節點。每個節點6個分量,則一共要15414個字節。另程序在每行額外提供2個字節緩存相關信息。每個列要增加Count分量。故一共要15414+561*2+325=16861字節

數獨三:題目提供了21個數字,則矩陣一共有21*1+(81-21)*9=561行,每行4個節點,則一共有561*4+324+1=2569個節點。每個節點6個分量,則一共要15414個字節。另程序在每行額外提供2個字節緩存相關信息。每個列要增加Count分量。故一共要15414+561*2+325=16861字節

 

以上的分析,改進的舞蹈鏈(Improve Dancing Lonks)算法在空間效率上還是完敗。但是時間效率上從看,在低難度的數獨問題上,雖然和暴力破解法還是有差距,但是差距比沒有優化前要小了很多;在高難度的數度問題上,時間效率比暴力破解法要高。

 

 

舞蹈鏈(Dancing Links)算法針對數獨問題還能再優化。我們來看看第二個優化的方向

 

在求解精確覆蓋問題中,返回的答案實際上是行的集合,集合的一個特性是無序性。也就意味着,如果答案是唯一的話,改變行在矩陣中的順序,不影響最后答案的輸出,無論這行換到什么位置,最后的答案始終包含着這行(如果答案不是唯一的,也沒啥太大的影響)。也就是說,行的順序不影響最終答案的求解。

 

我們就從這個方向入手

在構造矩陣的時候,先遍歷數獨的格子,先把有數字的格子轉換為行,插入到矩陣中。很顯然,這些行一定會被選中(想想看么,原問題中(4,2)填的是7,如果該行沒選中,結果出現了(4,2)填的是9,那不是一件很搞笑的事么)。

 

由於是精確覆蓋問題,每列只能有1個1,而上面的插入的幾行一定會被選中。那么,在接下來插入的行如果和上面的行相沖的話(兩個行有相同的列有1),那么,后插入的行是個無效的行(肯定不會被選中)。這些無效的行插入到矩陣中,雖然不會影響最終的結果,但是肯定影響求解的效率(空間和時間都有所損耗),而這樣的無效行其實有不少。

 

我們要采用特殊的手法,來避免這些無效的行插入到矩陣中。分兩步走

1、先遍歷數獨的格子,把那些有數字的格子轉換為行,插入到矩陣中。在插入的同時,把包含1的列的列首元素的Count分量設置為-1(起到后面判別的作用)。

由於這些行一定能被選中,是答案的一部分,那么把這些行的行號置入到答案列表中,並把這些列的列首元素從水平雙向鏈中移除(手動移除比調用RemoveCol方法快)

2、在遍歷沒有數字的格子,轉換為若干行(1個格子9行)插入到矩陣中。在插入到矩陣的時候,判斷包含1的列的列首元素的Count分量。如果是-1,說明新插入的行和第1步中的某些行相沖,是個無效行,沒有必要插入到矩陣中;如果不是-1,說明是個有效行,插入到矩陣中。

經過這個優化,能大大減少矩陣的規模(列不變,行減少了不少),我們稱之為數獨的舞蹈鏈(Sudoku Dancing Links)算法

 

 
Public  Class  clsSudokuDLXBySudoku
    Implements  I_Sudoku

    Private _Dance As  clsDancingLinksSudoku

    Private _Num(80) As  Integer

    Public  Sub  New()
        Init()
    End  Sub

    Public  Function SetLine(Row As  Integer, ParamArray Value() As  Integer) As  Boolean  Implements  I_Sudoku.SetLine
        Dim I As  Integer
        For I = 0 To IIf(Value.Length < 10, Value.Length - 1, 8)
            _Num(Row * 9 - 9 + I) = Value(I)
        Next
        Return  True
    End  Function

    Public  Function Calculate() As  String  Implements  I_Sudoku.Calculate
        Dim I As  Integer, J As  Integer
        Dim X As  Integer, Y As  Integer
        Dim Index As  New  List( Of  Integer), Value As  New  List( Of  Integer)
        Dim C1 As  Integer, C2 As  Integer, C3 As  Integer, C4 As  Integer
        For I = 0 To 80
            X = Int(I / 9)
            Y = I Mod 9
            If _Num(I) > 0 Then
                C1 = X * 9 + Y + 1
                C2 = X * 9 + _Num(I) + 81
                C3 = Y * 9 + _Num(I) + 162
                C4 = (Int(X / 3) * 3 + Int(Y / 3)) * 9 + _Num(I) + 243
                _Dance.AppendLineByIndex(C1, C2, C3, C4)
                Index.Add(I)
                Value.Add(_Num(I))
            End  If
        Next

        _Dance.CompleteInsertMustSelectRow()

        For I = 0 To 80
            X = Int(I / 9)
            Y = I Mod 9
            If _Num(I) = 0 Then
                C1 = X * 9 + Y + 1
                C2 = X * 9 + 1 + 81
                C3 = Y * 9 + 1 + 162
                C4 = (Int(X / 3) * 3 + Int(Y / 3)) * 9 + 1 + 243
                If _Dance.AppendLineByIndex(C1, C2, C3, C4) = True  Then
                    Index.Add(I)
                    Value.Add(1)
                End  If

                For J = 2 To 9
                    If _Dance.AppendLineByIndex(C1, C2 + J - 1, C3 + J - 1, C4 + J - 1) = True  Then
                        Index.Add(I)
                        Value.Add(J)
                    End  If
                Next
            End  If
        Next

        Dim P() As  Integer = _Dance.Dance

        For I = 0 To 80
            _Num(Index.Item(P(I) - 1)) = Value.Item(P(I) - 1)
        Next

        Dim V As  String = ""
        For I = 0 To 80
            V = V & _Num(I) & "  "
            If I Mod 9 = 8 Then V &= vbNewLine
        Next
        Return V

    End  Function

    Public  Sub Init() Implements  I_Sudoku.Init
        _Dance = New  clsDancingLinksSudoku(324)
        Dim I As  Integer
        For I = 0 To 80
            _Num(I) = 0
        Next
    End  Sub
End  Class

Public  Class  clsDancingLinksSudoku
    Private Left() As  Integer, Right() As  Integer, Up() As  Integer, Down() As  Integer
    Private Row() As  Integer, Col() As  Integer

    Private _Head As  Integer

    Private _Rows As  Integer, _Cols As  Integer, _NodeCount As  Integer
    Private Count() As  Integer

    Private Ans() As  Integer

    Private _HadInsertMustSelectRow As  Integer

    Public  Sub  New( ByVal Cols As  Integer)
        ReDim Left(Cols), Right(Cols), Up(Cols), Down(Cols), Row(Cols), Col(Cols), Ans(Cols)
        ReDim Count(Cols)
        Dim I As  Integer

        Up(0) = 0
        Down(0) = 0
        Right(0) = 1
        Left(0) = Cols

        For I = 1 To Cols
            Up(I) = I
            Down(I) = I
            Left(I) = I - 1
            Right(I) = I + 1
            Col(I) = I
            Row(I) = 0

            Count(I) = 0
        Next

        Right(Cols) = 0

        _Rows = 0
        _Cols = Cols
        _NodeCount = Cols
        _Head = 0

        _HadInsertMustSelectRow = 0
    End  Sub

    Public  Function AppendLine( ByVal  ParamArray Value() As  Integer) As  Boolean
        Dim V As  New  List( Of  Integer)

        Dim I As  Integer
        For I = 0 To Value.Length - 1
            If Value(I) = 1 Then V.Add(I + 1)
        Next

        Return AppendLineByIndex(V.ToArray)
    End  Function

    Public  Function AppendLineByIndex( ByVal  ParamArray Index() As  Integer) As  Boolean
        Dim I As  Integer, K As  Integer = 0

        If _HadInsertMustSelectRow > 0 Then
            If Index.Length = 0 Then
                _Rows += 1
                Return  True
            Else
                For I = 0 To Index.Length - 1
                    If Count(Index(I)) = -1 Then  Return  False
                Next
            End  If
        Else
            If Index.Length = 0 Then  Return  False
        End  If

        _Rows += 1

        ReDim  Preserve Left(_NodeCount + Index.Length)
        ReDim  Preserve Right(_NodeCount + Index.Length)
        ReDim  Preserve Up(_NodeCount + Index.Length)
        ReDim  Preserve Down(_NodeCount + Index.Length)
        ReDim  Preserve Row(_NodeCount + Index.Length)
        ReDim  Preserve Col(_NodeCount + Index.Length)
        ReDim  Preserve Ans(_NodeCount + Index.Length)

        For I = 0 To Index.Length - 1

            _NodeCount += 1

            If I = 0 Then
                Left(_NodeCount) = _NodeCount
                Right(_NodeCount) = _NodeCount
            Else
                Left(_NodeCount) = _NodeCount - 1
                Right(_NodeCount) = Right(_NodeCount - 1)
                Left(Right(_NodeCount - 1)) = _NodeCount
                Right(_NodeCount - 1) = _NodeCount
            End  If

            Down(_NodeCount) = Index(I)
            Up(_NodeCount) = Up(Index(I))
            Down(Up(Index(I))) = _NodeCount
            Up(Index(I)) = _NodeCount

            Row(_NodeCount) = _Rows
            Col(_NodeCount) = Index(I)

            If _HadInsertMustSelectRow > 0 Then
                Count(Index(I)) += 1
            Else
                Count(Index(I)) = -1
            End  If
        Next

        Return  True
    End  Function

    Public  Sub CompleteInsertMustSelectRow()
        Dim I As  Integer

        For I = 1 To _Cols
            If Count(I) = -1 Then
                Left(Right(I)) = Left(I)
                Right(Left(I)) = Right(I)
            End  If
        Next

        For I = 1 To _Rows
            Ans(I - 1) = I
        Next

        _HadInsertMustSelectRow = _Rows
    End  Sub

    Public  Function Dance() As  Integer()
        Return IIf(Dance(_HadInsertMustSelectRow) = True, Ans, Nothing)
    End  Function

    Private  Function Dance( ByVal K As  Integer) As  Boolean

        If (Right(_Head) = _Head) Then
            ReDim  Preserve Ans(K - 1)
            Return  True
        End  If

        Dim P As  Integer, C1 As  Integer
        P = Right(_Head)
        C1 = P
        Do  While P <> _Head
            If Count(P) < Count(C1) Then C1 = P
            P = Right(P)
        Loop

        If Count(C1) < 1 Then  Return  False

        RemoveCol(C1)

        Dim I As  Integer, J As  Integer

        I = Down(C1)
        Do  While I <> C1
            Ans(K) = Row(I)

            J = Right(I)
            Do  While J <> I
                RemoveCol(Col(J))
                J = Right(J)
            Loop

            If Dance(K + 1) Then  Return  True

            J = Left(I)
            Do  While J <> I
                ResumeCol(Col(J))
                J = Left(J)
            Loop

            I = Down(I)
        Loop

        ResumeCol(C1)
        Return  False
    End  Function

    Public  Sub RemoveCol( ByVal ColIndex As  Integer)

        Left(Right(ColIndex)) = Left(ColIndex)
        Right(Left(ColIndex)) = Right(ColIndex)

        Dim I As  Integer, J As  Integer

        I = Down(ColIndex)
        Do  While I <> ColIndex
            J = Right(I)
            Do  While J <> I
                Up(Down(J)) = Up(J)
                Down(Up(J)) = Down(J)

                Count(Col(J)) -= 1

                J = Right(J)
            Loop

            I = Down(I)
        Loop

    End  Sub

    Public  Sub ResumeCol( ByVal ColIndex As  Integer)

        Left(Right(ColIndex)) = ColIndex
        Right(Left(ColIndex)) = ColIndex

        Dim I As  Integer, J As  Integer

        I = Up(ColIndex)

        Do  While (I <> ColIndex)
            J = Right(I)
            Do  While J <> I
                Up(Down(J)) = J
                Down(Up(J)) = J

                Count(Col(J)) += 1

                J = Right(J)
            Loop
            I = Up(I)
        Loop

    End  Sub
End  Class

通過_HadInsertMustSelectRow來決定插入行的數據。如果_HadInsertMustSelectRow=0說明當前插入行為必選行,設置Count(J)的值為-1。如果_HadInsertMustSelectRow>0說明當前行為可選行,判斷和之前的必選行是否有沖突,如果有,說明本行必不可選,不比再插入到矩陣里了。通過CompleteInsertMustSelectRow方法來修改_HadInsertMustSelectRow參數,來決定行的性質

 

數獨的舞蹈鏈(Sudoku Dancing Links)算法的效率

 

時間效率

數獨一:1.31毫秒

數獨二:2.81毫秒

數獨三:5.56毫秒

 

空間效率

數獨一:矩陣一共有164行,每行4個節點,則一共有164*4+324+1=981個節點。每個節點6個分量,則一共要5886個字節。另程序在每行額外提供2個字節緩存相關信息,每個列要增加Count分量。故一共要5886+164*2+325=6539字節

數獨二:矩陣一共有276行,每行4個節點,則一共有276*4+324+1=1429個節點。每個節點6個分量,則一共要8574個字節。另程序在每行額外提供2個字節緩存相關信息。每個列要增加Count分量。故一共要8574+276*2+325=9451字節

數獨三:矩陣一共有275行,每行4個節點,則一共有275*4+324+1=1425個節點。每個節點6個分量,則一共要8550個字節。另程序在每行額外提供2個字節緩存相關信息。每個列要增加Count分量。故一共要8550+275*2+325=9425字節

 

可以看出,數獨的舞蹈鏈(Sudoku Dancing Links)算法改進的舞蹈鏈算法(Improve Dancing Links)算法,無論是時間效率上還是空間效率上都有了很大的改進。但是和暴力破解法相比,在簡單的數獨問題上,時間和空間都不占優勢,在高難度的數獨問題上,數獨的舞蹈鏈算法還是在時間上占有一點優勢的。這還是說明了一點,舞蹈鏈(Dancing Links)算法本質上也是暴力破解法,只是利用巧妙的數據結構實現了高效的緩存和回溯。

 

以上是我對用舞蹈鏈(Dancing Links)算法求解數獨問題的分析,經過兩次優化后,數獨的舞蹈鏈(Sudoku Dancing Links)算法已經達到比較好的效果。但能不能再優化呢?能優化到全面超越暴力破解法?目前我沒有看出其他的優化方向,如有,望網友不吝賜教,大家共同進步。

 

最后,給出三個數獨的解

數獨一:

image

 

數獨二:

image

 

數獨三:

image


免責聲明!

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



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