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


【說在前面】:

       之前,我在微信朋友圈看到一個同事發了一個狀態,說的是她在家輔導孩子做作業,一個數獨的題目,好像沒有做出來。我看了下,我也做不出來,后來仔細想了下,花了兩個多小時時間,用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 

 

 


免責聲明!

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



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