VBA:完美解决数独问题----全网最强

笔记 同时被 2 个专栏收录
2 篇文章 0 订阅
1 篇文章 0 订阅

 

【说在前面】:

       之前,我在微信朋友圈看到一个同事发了一个状态,说的是她在家辅导孩子做作业,一个数独的题目,好像没有做出来。我看了下,我也做不出来,后来仔细想了下,花了两个多小时时间,用Python编了个程序,把那个数独题目解出来了。随后我就发了一个公众号的推送,这个推送被我老婆看见了,说:“人工解数独两分钟,你写个程序花两个多小时?!何必呢?”我说:“这个程序可以解答任何9×9的数独问题。”她说:“如果换一个数独题目,又要重新改代码,不太方便!而且我也不懂什么编程,不会用你的程序。”我想想也是,这也是我当初准备考虑用VBA的原因,就是因为VBA依托于电子表格,出题、解答、展示结果都比较直观,程序使用起来也比较方便。后来之所以用Python来编程,是因为Python处理这种大量数据计算比较方便快捷。

       当初用Python编写程序,利用到了面向对象的编程方法,如果你不了解面向对象编程,可能对我之前写的程序理解起来有难度。所以我想着用VBA编程,采用面向过程的方式编写一个程序来解答数独问题。

       这几个周末的闲暇时间,我一直在考虑用VBA来写一个解数独问题的程序,采用面向过程的方式来编写,这样便于理解。如果面向过程都搞定了,将来在转换到面向对象,就很容易了。我在网上查阅了很多资料,也看了很多网友写的程序,都测试了一下,基本上都满足不了我的需求。不是解题太慢,就是解不出来,解答一些骨灰级难度的数独,还导致死机,系统电脑直接卡死、崩溃。有的还需要使用“猜测法”来解数独,感觉超级不爽。

       好吧!既然动了用VBA解数独的心思,那就必须得搞定。于是,想了几天,反复测试,编写代码,终于搞定了。一般数独可以秒解。

       我在网上查了很多资料和其他网友写的程序,都没有我这个厉害,找一个骨灰级难度的数独,解出来也就两分多钟。网上一些朋友提供的数独程序,骨灰级别难度的数独,根本解决不了。

       以上就是一个骨灰级别难度的数独,用的程序解出来了。尝试了661773次,耗时207.829秒。是不是很牛X。我在网上还没有发现有谁用VBA写出过能这么快解数独的程序。

看视频演示,全网最强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 + 1ShuDuSize)

       翻译一下:如果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行语句,弹出此题无解信息。

       好啦,现在整个程序就写完了。

       当然,为了程序有更好的交互性,还可以在里面加入更多的人机交互的内容,比如如何绘制表格,如何给表格添加灰色背景,如何将出题区的内容复制到作答区等等,这不是这篇文章的重点,这里不详细介绍。

       后面附上数独源文件,大家可以看源代码了解。

       下载地址:VBA:解决数独问题,源码。

链接:https://pan.baidu.com/s/1KwKXaU0ipKCryriImP9zLQ 
提取码:zd4x 
 

也可扫码下载:

       就讲到这里吧!

 

 

  • 3
    点赞
  • 4
    评论
  • 14
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 4 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:精致技术 设计师:CSDN官方博客 返回首页

打赏作者

追梦-在路上

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值