【原創】視訊+文字:詳解VBA解決數獨問題

在路上發表於2020-11-28

【說在前面】:

       之前,我在微信朋友圈看到一個同事發了一個狀態,說的是她在家輔導孩子做作業,一個數獨的題目,好像沒有做出來。我看了下,我也做不出來,後來仔細想了下,花了兩個多小時時間,用Python編了個程式,把那個數獨題目解出來了。隨後我就發了一個公眾號的推送,這個推送被我老婆看見了,說:“人工解數獨兩分鐘,你寫個程式花兩個多小時?!何必呢?”(學霸就是學霸,說話都這麼霸氣!)我說:“這個程式可以解答任何9×9的數獨問題。”她說:“如果換一個數獨題目,又要重新改程式碼,不太方便!而且我也不懂什麼程式設計,不會用你的程式。”我想想也是,這也是我當初準備考慮用VBA的原因,就是因為VBA依託於電子表格,出題、解答、展示結果都比較直觀,程式使用起來也比較方便。後來之所以用Python來程式設計,是因為Python處理這種大量資料計算比較方便快捷。

 

       當初用Python編寫程式,利用到了物件導向的程式設計方法,如果你不瞭解物件導向程式設計,可能對我之前寫的程式理解起來有難度。所以我想著用VBA程式設計,採用程式導向的方式編寫一個程式來解答數獨問題。

 

       這幾個週末的閒暇時間,我一直在考慮用VBA來寫一個解數獨問題的程式,採用程式導向的方式來編寫,這樣便於理解。如果程式導向都搞定了,將來在轉換到物件導向,就很容易了。我在網上查閱了很多資料,也看了很多網友寫的程式,都測試了一下,基本上都滿足不了我的需求。不是解題太慢,就是解不出來,解答一些骨灰級難度的數獨,還導致當機,系統電腦直接卡死、崩潰。有的還需要使用“猜測法”來解數獨,感覺超級不爽。

 

       好吧!既然動了用VBA解數獨的心思,那就必須得搞定。於是,想了幾天,反覆測試,編寫程式碼,終於搞定了。一般數獨可以秒解。

 

       我在網上查了很多資料和其他網友寫的程式,都沒有我這個厲害,找一個骨灰級難度的數獨,解出來也就兩分多鐘。網上一些朋友提供的數獨程式,骨灰級別難度的數獨,根本解決不了。

 

 

       以上就是一個骨灰級別難度的數獨,用的程式解出來了。嘗試了661773次,耗時207.829秒。是不是很牛X。我在網上還沒有發現有誰用VBA寫出過能這麼快解數獨的程式。

 

看視訊演示,全網最強VBA解答數獨問題!

注意:全網最強!強!!強!!!

不服來戰~!

【如果是手機觀看,建議最大化視訊,手機橫屏觀看!】

視訊地址:https://www.bilibili.com/video/BV1PZ4y1G7Zv

       怎麼樣?展示視訊看完,是不是覺得這個程式很強大?視訊裡面演示的各種難度的數獨,都是我在一個線上的數獨網站上找的,大家有興趣也可以自己試試,測試一下。

       免費線上數獨網址:

       http://www.cn.sudokupuzzle.org/

 

【一個小插曲】

       之前,我把這個數獨程式寫完了,做了展示視訊,寫了這個技術文件的草稿,給老婆看。她是一個程式小白,如果她都大概能看懂,有興趣把展示視訊看完,那說明這個技術文章寫得還是比較詳細的,因為我的題目是《詳解VBA解決數獨問題》,是“詳解”。作為完全不懂程式設計的她看後,也提了很多問題和建議,大多都是很弱的問題。我想她如果有這個問題,可能大家在看的時候,也會有同樣的問題,所以我就做了一些修改和進一步的講解。也就有了下面的:

〇、程式介面設計 這個章節,這是我新增加的一個,所以用 〇 來編號。然後再給老婆看了下,她基本上滿意了,所以我就準備釋出了。

 

       跟老婆的對話:(理工男和萌妹子的日常對話)

 

       她:你這次改的還不錯,寫的很詳細,但是我不想看,太長了,看了頭疼。

       我:那我做的展示視訊怎麼樣?看看吧!

       她:視訊配樂很不錯。感覺你這個程式還是很厲害的!

       我:這都是在你的指導下,做的修改啊!

       她:你比較像白居易。

       我:什麼意思啊?

       她:白居易在寫詩的時候,都會先讀給老婆婆聽,如果老婆婆聽得懂,覺得好,他才會發。所以他的詩都通俗易懂,而且又不失高階大氣。

       我:白居易?不知道,我只知道白雲邊。

       她:·······,(直接不理我了)

------ 語法不相容,系統已當機 ------

成功的把天聊死了 呵呵

 

 

 ----------  以上都是廢話,下面進入正題 ----------

 

       很多時候,我們覺得計算機很聰明,很厲害!其實,計算機很笨,他唯一的優點就是“快”!他能很快的處理一些複雜的,需要反覆處理的事情。

       所以,對於計算機來說,只有我們想不到的事情,沒有他做不到的事情。如果他做不到,那就是我們還沒有把問題想清楚、分析透徹,無法翻譯成程式碼,讓計算機執行。

       因此,只有瞭解計算機的執行原理,懂得計算機的思維方式,我們才能把現實中的問題,按計算機能夠理解的思路,想清楚,分析透,並翻譯成程式碼,讓計算機幫我們做。

 

       下面就向大家介紹一下VBA解數獨的思路和程式碼。

 

       剛才說了,計算機有一個很大的特點就是“快”!如何發揮它“快”的優勢?那就要利用迴圈,快速的反覆運算處理。那麼怎麼形成迴圈呢?那就找規律,要分析解決問題的核心規律,抽象成程式碼和變數,利用迴圈,快速運算處理。

 

       現在我們就來分析數獨問題:

 

       什麼是數獨?

       數獨是一種數學遊戲。數獨盤面是個九宮,每一宮又分為九個小格。玩家需要根據9×9盤面上的已知數字,推理出所有剩餘空格的數字,並滿足每一行、每一列、每一個粗線宮(3*3)內的數字均含1-9,不重複。所以又稱“九宮格”。

 

〇、程式介面設計

      

       為了使用者能夠很好的體驗這個程式,同時也為了後續的開發方便,我們需要對程式介面做一個很好的設計。

 

程式主介面:

 

 

 

       我是在WPS的電子表格裡設計的,可以看到最左邊和最上邊的數字編號,這是表格的行、列編號,是絕對定位編號,我設計的數獨九宮格,是在上面空了兩行,左邊空了一列開始繪製表格的,所以我在上面標註了 起始行號:3,起始列號:2,這就是針對我繪製的數獨九宮格第一個格子的位置,相對於電子表格編號的定位,這是相對定位,便於以後調整表格位置,不用改程式碼,只需要修改起始行號和起始列號的數字就可以了。作答區也是這個思路設計的。

       第13行,我做了一個 顯示解題過程 的開關,只需要用滑鼠點選那個小黑點,就可以自由的啟用和關閉 顯示解題過程 了,方便操作。

       這些都是互動功能,不在這次詳解的範圍內,不細說,有興趣可以看我的原始碼。

 

一、數獨區域劃分和命名:

 

      1、行、列編號

 

 

 

       根據數獨格子的樣式,我們給他命名並編號:

       行編號:Row = 1 表示第一行,以此類推,第九行 Row = 9。

       列編號:Col = 1 表示第一列,以此類推,第九列 Col = 9。

       區編號:Box = 1 表示第一個小九宮格,以此類推,第九區 Box = 9。

 

       2、點位編號:

 

 

       這是每個單元格的編號,其目的是方便找到所在點位對應的行號、列號和區號。

 

二、建立數獨模型

 

       我們還是以之前我那個同事發的數獨題目為例:

 

 

       可以看到,81個格子裡面,已經有27個格子有數字了,54個格子是空的。這就是數獨題目。現在我們就要考慮,把這個題目讀取出來,在程式程式碼裡面形成數獨模型。之前我考慮用陣列來處理,但是VBA的陣列操作很麻煩,於是果斷放棄,這裡採用字串模式來處理。

 

       我們用一個巢狀迴圈,逐行讀取數獨題目中的數字,如果格子是空的,我們就用0來填充佔位,確保字串長度為81。

 

程式碼:

 

 

解釋:

 

       函式名為 GetShuDuList(Row As Integer, Col As Integer) As String (核心函式)

 

       用一個巢狀的For迴圈來讀取數獨表格的資料,第九行程式碼,這裡用了一個 IIF語句,意思就是,在指定的行號和列號定位的單元格內是空值,就填充0,否則就讀取單元格內的數字。

 

       讀取完後,用 GetShuDuList = List 返回。

 

       最後得到:

 

300500000000000346002003000003000200010840000006320980035100069091050000000900007

 

       這樣的一個字串。

       以後我們所有的操作和運算,都基於這個字串來進行。

 

       舉例:

 

 

       以新增了灰背景色的數字 3 為例。對應單元格點位編號,可以對照知道,這個3對應的點位為30點位。

 

       GetShuDuList = 30050000000000034600200300000300020……

 

       在數獨模型GetShuDuList字串中,也是對應的第30點位,即標紅的那個3。

 

       那麼,現在我們就要考慮一個重要的問題,如何通過這個點位30,算出這個點位對應在數獨表格中的行號、列號和區號。

 

       這裡需要用到一點點數學知識,就是除法的取整和取餘的問題。

 

       很明顯,這個30點位,對應的行號是4,對應的列號是3,對應的區號是4。那麼如何算呢?

 

       先算行號和列號:

 

       這個數獨是9行9列的表格,那麼我們就用點位號30除以9:

       30÷9=3 餘 3

       很容易理解了,除法結果整數部分 +1 就是行號,餘數部分就是列號。即30點位行號是4,列號是3。

 

       我們再試一下點位14看看。

       14÷9=1 餘 5

       按上面計算方法,行號是2,列號是5。沒問題,是正確的。

 

       我們再試試點位18看看。

       18÷9=2 餘 0

       按上面計算方法,行號是3,錯誤。列號是0,錯誤。

 

       我們發現,當出現9的倍數的點位號時,不正確了。

 

       稍微分析一下就可以知道,當出現9的倍數的點位號,也就是點位號除以9餘數為0時,行號就是兩個數除法運算的結果,列號就是9。

 

       於是,我們就把這個規律找出來了。那麼如何翻譯成VBA語言,讓計算機理解執行呢?

 

       在VBA中的語法,

 

       除法的運算子是 /  例如 18 / 9 = 2

       除法取整的運算子是 \ 例如 14 \ 9 = 1

       除法取餘數的運算子是 Mod 例如 14 Mod 9 = 5

 

       好,我們來寫程式碼:

 

       計算行號的函式:GetRowIndex(ShuDuIndex As Integer) As Integer

       計算列號的函式:GetColIndex(ShuDuIndex As Integer) As Integer

 

 

       這裡我們依然用到了IIF函式。

      算行號:先取模(取餘數),餘數為零,行號就是點位號除以數獨尺寸9,就得到了行號;餘數不為零,就直接取整數再 +1,就是他的行號。

      算列號:先取模(取餘數),餘數為零,列號就是數獨尺寸9,餘數不為零,就取餘數(取模),得到的就是列號。

       容易理解吧~!?

 

       下面我們就來處理計算區號的問題:

 

       這個問題我沒有找到很好的演算法,就用笨辦法來處理吧!

       計算區號的函式:GetBoxIndex(ShuDuIndex As Integer) As Integer

 

 

       以第四行程式碼為例講解:

 

       這裡用的Select Case 語法,當點位號等於1,2,3,10,11,12,19,20,21中的一個數字時,表示在第一區,返回區號1。其他以此類推。

       也就是把所有區內的點位號都羅列出來,逐個判斷這個點位在哪個區。

       也很好理解吧?~!

 

       好,現在我們已經可以通過點位號來得到這個點位所在的行號、列號和區號了,然後我們就要統計這個行、這個列、這個區記憶體在的數字了。

 

       這裡我們寫三個函式來處理:

 

       1、統計點位所在行中數字的函式(去掉0):

       GetRowList(ShuDuIndex As Integer, ShuDuList As String) As String

 

 

       第5行,第6行程式碼就是利用我們之前寫的函式,通過點位號計算出這個點位所在的行號和列號。這裡雖然用不到列號,還是寫著吧!

 

       第8行,用兩個函式的巢狀來統計數字。

       Mid函式,用來擷取整行字串。

       Mid(ShuDuList, (R - 1) * ShuDuSize + 1, ShuDuSize)

       翻譯一下:如果R = 3,表示第三行,那麼

 

       紅色部分:

       (3 - 1) * 9 + 1 = 19,表示第三行起始點位號是19。

 

       綠色部分:

       ShuDuSize = 9,這在之前的公共變數裡面已經定義了。

       整句話的意思是:在ShuDuList字串中,從第19個點位開始擷取字串,擷取9個,這樣就剛好把第三行的數字擷取出來了。

 

       GetShuDuList = 30050000000000034600200300000300020……

 

       上面的紅色部分。即:002003000

 

       Replace函式,套在Mid函式的外面,用來將擷取出來的字串中的0全部刪除。即得到:23 兩個數字字串。

 

       這是一個很基礎的小招數,這裡不深入探討,大家有興趣深入研究可以在網上查閱相關資料或檢視VBA參考手冊。

 

Mid函式介紹:

 

 
Replace函式介紹:

 

 

       2、統計點位所在列中數字的函式(去掉0):

       GetColList(ShuDuIndex As Integer, ShuDuList As String) As String

 

 

       這裡無法用Mid函式一次性來擷取,因為他跳行了,不是連續的,所以,我們只能用For迴圈來擷取,For迴圈的步長Setp = ShuDuSize 即步長為9,通過迴圈擷取,這樣以來,我們就可以得到整列的數字,然後執行第10行程式碼,去掉裡面的0。

 

       3、統計點位所在區中數字的函式(去掉0):

       GetBoxList(ShuDuIndex As Integer, ShuDuList As String) As String

 

 
       方法跟上面類似,只是我們這裡專門為獲取區塊內的數字寫了一個函式:JoinBoxList(BoxIndex As Integer, ShuDuList As String) As String

 

 

       同樣是通過Mid函式來擷取,最後用Replace函式來去掉裡面的0。

 

       最後的結果:

 

 

      以標綠的單元格3行,4列,2區為例,在VBE本地視窗可以看到,

      23:就是紅框框第三行裡的數字,刪掉了0。

      58319:就是橙色框框第四列裡的數字,刪掉了0。

      53:就是藍色框框第二區裡的數字,刪掉了0。

      程式計算沒有問題。

 

       好,現在我們可以通過

       GetShuDuList(Row As Integer, Col As Integer) As String 得到數獨模型列表

       GetRowList(ShuDuIndex As Integer, ShuDuList As String) As String 得到點位所在行中的數字列表。

       GetColList(ShuDuIndex As Integer, ShuDuList As String) As String 得到點位所在列中的數字列表。

       GetBoxList(ShuDuIndex As Integer, ShuDuList As String) As String 得到點位所在區中的數字列表。

 

       下面我就就要統計空表格所對應的行號、列號、區號和可能填入的數字列表。

 

       我們寫一個函式來處理:

       CanPointInfo(ShuDuList As String) As Variant (核心函式)

 

 

       解釋:

       第3行:ReDim CanPointList(1 To 1) As Variant

       定義一個一維可變陣列 CanPointList,用來存放每個空點位的相關資訊。

 

       第5行、第6行。定義一個ArrId = 1,用於表示陣列下標,最後進行遞增,以達到擴充陣列的目的。

 

       第7行,定義一個一維陣列Point,用來存放空點位的相關資訊。

 

       第10行,做一個For迴圈,從1迴圈到81,遍歷整個陣列模型列表。

 

       第11行,迴圈過程中,利用Mid函式,逐個取出陣列模型列表中的數字,如果等於0,則表示數獨中這個點位是空的。

 

       第12-15行,通過之前寫的函式得到行號,列號,區號。

       Point(1) 存放行號資訊

       Point(2) 存放列號資訊

       Point(3) 存放區號資訊

       Point(4) 存放行可能填入的數字資訊,這裡先讓他為空,後續再處理。

 

       第17-23行,定義三個字串變數,通過之前寫的函式,得到點位所在行、所在列、所在區的數字資訊。

 

       第25-32行,通過一個迴圈語句,從1迴圈到9,然後比對行中的數字,列中的數字,區中的數字,如果都沒有出現,表示這個數字是可能填入的數字,然後將可能填入的數字壓入Point(4)裡面。

 

       第34行,把整理好的Point存入擴充後的CanPointList裡。

 

例如:

 

 

       標灰的單元格,點位號是29,通過計算可以得到他的行號、列號、區號。

       即:

       Point(1) = 4,在第4行。

       Point(2) = 2,在第2列。

       Point(3) = 4,在第4區。

 

       然後統計行、列、區的數字:

       RowNumList = “32”

       ColNumList = “139”

       BoxNumList = “316”

 

       然後執行25-32行程式碼:

       數字1:在ColNumList、BoxNumList  中都存在,捨去;

       數字2:在RowNumList 中存在,捨去;

       數字3:在RowNumList、ColNumList、BoxNumList 中都存在,捨去;

       數字4:都不存在,可以保留。執行第30行程式碼;

       數字5:都不存在,可以保留。執行第30行程式碼;

       數字6:在BoxNumList 中存在,捨去;

       數字7:都不存在,可以保留。執行第30行程式碼;

       數字8:都不存在,可以保留。執行第30行程式碼;

       數字9:在ColNumList中存在,捨去。

 

       迴圈結束後,Point(4) = “4578”

 

       最後整理一下:

       Point(1) = 4

       Point(2) = 2

       Point(3) = 4

       Point(4) = “4578”

 

       即:4行3列這個單元格對應的區位號是4,這個單元格可能填入的數字是 4578中的任何一個。

 

       然後執行第33-35行程式碼:

       ReDim Preserve CanPointList(1 To ArrId) As Variant

 

       保留原有陣列內的資訊,擴充陣列,並存入剛才得到的Point資訊,然後CanPointList陣列下標自增,以便於後續擴充,便於填入新的資料。

 

       當第10行到第37行程式碼迴圈執行完後,那麼這個數獨的所有點位都遍歷完了,並且把所有空單元格的資訊都已經統計出來,並存入了CanPointList陣列中了,這時,CanPointList陣列就變成了一個二維陣列。

 

 

       好了,現在我們通過

       GetShuDuList 函式得到了整個陣列模型列表

       CanPointInfo 函式得到了每個空點位的資訊和可能填入的陣列資訊。

 

       下面我們我們就要考慮,如果我們從CanPointInfo拿出一個點位資訊,填入到GetShuDuList 列表中,然後,我們就要檢查我們填入的這個數字,在這個點位上的行中、列中,區中是否存在,如果不存在,我們就可以填入,如果存在,就不符合數獨規則,就要退出來,換另外一個可能填入的數字,並恢復之前嘗試的數字,然後進行下一輪,繼續嘗試。

 

       這個檢查的過程,我們依然需要寫函式來實現。

 

       這裡,我們寫一個Check函式,用來檢查填入數字是否合法。

 

 

解釋:

       第3-6行,判斷嘗試填入的數字是否為0,如果是0,則表示還未賦值給嘗試的點位,直接返回False,表示資料非法。

 

       第9行,這裡是通過點位資訊裡的行號和列號計算出數獨模型列表的索引位置,為了程式的簡潔,我這裡寫了一個函式來處理。

 

 

       這個演算法很容易理解啊,不多說。

 

       第11-17行,通過之前寫的函式,獲取點位所在行、所在列、所在區中的數字列表。

 

       第19-25行,跟之前的演算法一樣,通過InStr函式來判斷嘗試的數字是否存在,如果不存在,就都等於0,則返回True,表示這個數字可以填入,否則,返回False,表示這個數字非法。

 

       好,到這裡,我們就要考慮如何從CanPointInfo 陣列中取一個點位資訊,存入GetShuDuList 列表中,並用Check函式來檢查了。

 

       從CanPointInfo 陣列中取點位資訊,有很多方法,從前面取也可以,從後面取也可以,從中間取,也可以。這裡為了滿足將來遞迴演算法的要求,我們從最後一個來取,這樣取也不會打亂前面陣列結構資訊。因為從前面取或從中間取點位資訊,CanPointInfo 陣列結構會發生變化。

 

       那麼如何從最後一個陣列元素取資訊呢?在別的程式語言裡面有專門的函式,像Python中,他自帶一個pop()方法,可以取出陣列最後一個元素,取出後,並將原陣列中最後一個元素刪除。

 

       而VBA則沒有這個方法,所以,我們又得自己寫函式來實現了。

 

       首先,我們寫一個得到陣列最後一個元素的函式。

       GetArrLast(Arr As Variant) As Variant

 

 

    很簡單的一個函式,一句話搞定。

       用UBound方法得到陣列的最大下標,然後取出,並返回。

 

       再寫一個刪除陣列最後一個元素的函式。

       DelArrLast(Arr As Variant) As Variant

 

 

 

解釋:

       第11行,通過LBound得到陣列的最小下標,通過UBound得到陣列最大下標,然後ReDim Preserve 重新定義動態陣列,並保留陣列原始資訊,讓最大下標減1。就等於把最後一個元素刪除了,然後再返回。

 

       這裡需要注意,當陣列只剩一個元素的時候,最小下標和最大下標都是1,如果最大下標再減1,這個陣列就會在重新定義時報錯。我當時在除錯程式的時候,這個地方報錯,我查了很久才查出來是這裡的問題,後來我就用了一個條件判斷語句來處理這個問題。

 

       第6-10行,如果最大下標等於1,我們就給這個陣列的頭兩個元素賦值為0。

       就相當於

       Point(1) = 0

       Point(2) = 0

 

       也就是說,這個點位資訊的行號為0,列號為0。正常情況下,點位資訊的行號和列號不可能為0,這裡我們強行的賦值為0,以後在遞迴的時候,就可以以此為判斷依據,如果行號等於0,那麼這個遞迴就要結束了,可以作為遞迴終止的判斷,防止發生死迴圈,導致系統崩潰。

 

       現在我們完成了從陣列最後一個元素取值的函式,也完成了刪除陣列最後一個元素的函式。下面我們就要考慮,如果取出來後,嘗試不行,我們又要恢復陣列原始狀態,我們還要考慮在陣列最後增加元素資訊的方法。Python中,他自帶一個append()方法,可以很容易的實現。而VBA沒有,所以,我們還是得寫函式來完成。

 

       函式名稱:AddArrLast(ArrSub As Variant, Arr As Variant) As Variant

 

 

       同樣用到了ReDim Preserve,保留陣列原始資訊,擴充陣列的方法。之前已經講過了,這裡不再重複。

 

       接下來,我們考慮如何把我們準備填入的數字放到數獨模型列表中的問題。

 

       如果是陣列,就很容易處理,但是之前我們考慮過,用陣列雖然很好處理這個問題,但是處理其他問題就比較麻煩,所以,最後我們這裡的數獨模型列表並沒有用陣列,而是用的字串。現在我們就要考慮如何把我們需要填入的數字,替換到數獨模型列表中的數字。

 

       一說替換,大家可能馬上會想到用Replace方法。但是,這是不行的,因為我們需要填入的數字在數獨模型列表中都是以0來填充的,如果用Replace來替換,你到底是要替換哪個0呢?你不說清楚,計算機是不知道的,他會把所有的0都替換掉。當然,你也可以定位,但是定位後用Replace替換,他會把定位前的字元都刪除掉,不知道這個函式他們是怎麼設計的,為什麼要這麼操作,也不是很清楚。所以,這也是不行的。

 

       於是,我們這裡考慮用Left()和Right()函式來處理。

       我們寫一個函式。

       ReplaceMid(Str As String, RepStr As Variant, ShuDuIndex As Integer) As String

 

 

       如果我們需要替換掉第10個數字,那麼我們用Left取出左邊的9個數字,Right取出第10個之後的所有的數字,然後左邊的數字拼接上替換的數字,再拼接上右邊的數字,就相當於把指定位置的數字替換掉了。很好理解吧?而且這樣也比較簡單高效。

 

詳解:

 

 

       好了,到此,我們就要考慮在空單元格填入可能的數字並檢查合法性的問題了。

       這個問題很複雜,所以,必須得寫個函式來處理。

 

       TryInPoint(Point As Variant, ShuDuList As String, CanPointList As Variant) (核心函式)

 

 

解釋:

       第4行:取出點位資訊中可能填入數字的列表。

 

       第8行:迴圈取出可能填入的數字,每次取一個。

 

       第9行:將取出來準備填入的數字放入Point(5)中。

 

       第10行:利用我們剛才寫的Check函式判斷合法性。

 

       第11行:如果是合法的,就替換掉數獨模型列表中對應的數字。

 

       第12行:判斷CanPointList陣列是否已經到了最後一個,如果到了最後一個,即行號、列號等於 0 的時候,就呼叫 ShowOkShuDu函式,顯示正確結果。

       ShowOkShuDu這個函式我們還沒寫。後面再寫!

 

       第16-18行:再從CanPointList中取出最後一個陣列元素。

 

       第20行:開始再次呼叫TryInPoint函式,遞迴嘗試。

 

       第22-25行:如果嘗試出現非法資料,就恢復上一輪的操作。

 

       到這裡為止。我們就把數獨問題的核心演算法寫完了。剩下就是考慮如何把正確的數獨結果顯示出來了。

 

       寫一個顯示數獨結果的函式。

       ShowOkShuDu(ShuDuList As String) (核心函式)

 

 

       就是利用迴圈來展示,展示完後,執行第14行程式碼,彈出對話方塊,提示數獨問題解答完成!

 

       然後執行第15行程式碼,終止整個程式執行。

       很容易理解,不多講。

 

       現在,數獨問題,已經可以解決了。考慮到程式的強壯性和智慧化。我們還需要考慮一些其他的問題。

 

       比如,在出題過程中,是否已經存在同行、同列或一個區中存在重複的數字,即數獨題目有誤。這個我們在作答前必須要檢查一個題目是否有問題。

 

       寫一個檢查數獨題目是否正確的函式。

       CheckShuDuOk(ShuDuList As String)

 

 

解釋:

       第8行:應為數獨一共有9行、9列、9區,所以,我們只需要迴圈九次,就可以檢查所有的行、列、區了。

 

       第9行:得到一行中的所有數字,並去掉0。

 

       第12行:從1-9這九個數字,依次和行中的數字進行比較。如果等於0,則表示沒有這個數字,嘗試下一個;如果不等於0,說明裡面存在,但是這樣是不是就可以判斷這個數字合法呢?不一定,因為你不知道里面到底是存在一個還是存在多個?存在一個,合法,存在多個,不合法,這是一個核心問題!!!

 

       這個問題怎麼弄?

 

       用InStr和InStrRev這兩個函式就可以搞定。

 

InStr 函式介紹:

 

 InStrRev函式介紹:

 

 

       比如:我這裡有一個字串:”351658”

       當我們檢查到3時,發現不等於0,則表示裡面有3。然後我們在看是否只有一個。

 

       InStr(”351658”, ”3”) = 1

       從左往右找,3出現在第一個位置。

 

       InStrRev(”351658”, ”3”) = 1

       從右往左找,3也出現在第一個位置。

 

       此時InStr和InStrRev的值相等,說明裡面就只有一個3,合法。

 

       當我們檢查到5時,發現不等於0,則表示裡面有5。然後我們在看是否只有一個。

       InStr(”351658”, ”5”) = 2

 

       從左往右找,5出現在第二個位置。

       InStrRev(”351658”, ”5”) = 5

 

       從右往左找,5出現在第五個位置。

       此時InStr和InStrRev的值不相等,說明裡面至少有兩個5,非法。

 

       好理解吧?!

 

       以下檢查列中的數字和檢查區中的數字,方法類似,不在重複講解。

 

       還有一種錯誤:就是我們在出題的時候,輸入錯誤,輸入了一個字母,或者輸入了大於9或小於1的數字,這都是不符合數獨遊戲規則的,我們也要將他判斷出來。

 

       於是,我們還得寫一個判斷是不是數字的函式。

 

 

       很簡單,一句話搞定,通過IsNumeric函式來處理。然後用再判斷是否大於9或小於1。

 

       還有一種情況就是,數獨出題區是滿的,沒有空單元格了,這也是一種錯誤。所以,我們對GetShuDuList函式做些許修改。

 

 

       解釋:

       增加第11行至18行程式碼,用來判斷輸入非法的字元和大於9小於1的數字。

       增加第24行至27行,用來判斷是不是滿格數獨。

 

       好,現在我們把這個數獨問題的程式基本上寫完了,然後我們需要一個主程式來讓他執行起來。

 

       主程式-程式入口

 

       ShuDuKu() (核心函式)

 

 

解釋:

       第2-3行:定義一個變數,賦值“此題無解”。當沒有得到正確結果時,就會執行第24行程式碼,彈出“此題無解”的對話方塊。

 

       第4行:公共變數賦值,表示數獨尺寸為9X9的數獨。

 

       第5-8行:得到出題區的起始行、列號和作答區的起始行、列號。

 

       第11行:呼叫GetShuDuList函式,得到數獨模型列表。

 

       第12行:呼叫CheckOkShuDu函式,檢查出題是否正確。

 

       第15行:呼叫CanPointInfo函式,得到空單元格的點位資訊和可能存入的數字資訊。

 

       第18行:從CanPointList陣列中取出最後一個陣列元素,複製給點位資訊陣列。

 

       第20行:呼叫DelArrLast函式,刪除CanPointList陣列的最後一個元素。

 

       第22行:呼叫TryInPoint函式,開始嘗試填入數字並檢查合法性。

 

       如果嘗試完成,得到正確結果,在TryInPoint函式裡面呼叫了ShowOkShuDu函式,最後有一個End語句,終止了整個程式,所以第24行就不會執行。

 

       如果嘗試完成,沒有得到正確結果,則在TryInPoint函式中不會呼叫ShowOkShuDu函式,也就不會執行ShowOkShuDu函式中的End語句,程式會這行主程式的第24行語句,彈出此題無解資訊。

 

       好啦,現在整個程式就寫完了。

 

       當然,為了程式有更好的互動性,還可以在裡面加入更多的人機互動的內容,比如如何繪製表格,如何給表格新增灰色背景,如何將出題區的內容複製到作答區等等,這不是這篇文章的重點,這裡不詳細介紹。

 

       後面附上數獨原始檔,大家可以看原始碼瞭解。

連結:https://pan.baidu.com/s/1KwKXaU0ipKCryriImP9zLQ 
提取碼:zd4x 

 

 

相關文章