01 在Excel中编写和运行程序

从零学升引Excel VBA编程演习“人工智能老鼠”走迷宫(二)_行号_策略 云服务

02 编程基本操作

03 画迷宫

04 画参数表

我们学习了在Excel中用VBA编写和运行程序的全部步骤和基本语句,完成了人工智能老鼠走迷宫程序的根本部分:画一个3行3列共有9格的迷宫,建立了“动作参数表”。
还没有读过第一篇教程的同学,请先点击上面的文章标题阅读。

“动作参数表”共有9行4列,每行代表迷宫的一格,每列代表一个方向,即“上、右、下、左”四个移动方向。
动作参数表”可以完备表达迷宫中每个格的每个可能的移动方向。

对付迷宫中的某个单元格来说,有墙的方向无法移动。
在“动作参数表”中,把这个方向的值设为0;对付没有墙的方向,参数设定为1。
这样,老鼠根据“动作参数表”中的值,便可知道可以向哪个方向移动。

下图,左侧为迷宫,右侧为动作参数表:

下一步,我们的编程事情是让老鼠动起来,为此,还须要建立另一张表,称为“策略表”。
以下的标题序号接前文连续。

05 画策略表

前面我们已经画出“动作参数表”。
老鼠根据此表,无论到了哪个格子里,都能知道哪个方向可以走,哪个方向不能走。

有的格子,只有一个方向可走,有的格子,有两个或者多个方向可走。

那么,该如何选择方向呢?这是“策略表”卖力的事情。

“策略表”与“动作参数表”大小相同,都是9行4列。
行号代表迷宫的编号,列号代表“上、右、下、左” 4个方向。

“策略表”中的值是每个可移动方向的均匀概率。

例如,如果迷宫中一个格子的某个方向有墙,则向该方向移动的概率为0,以是“策略表”该方向的值填0。

如果某个格子只有一个方向可以移动,那么,向这个方向移动的概率是100%。

如果某个格子有两个方向可以移动,那么,每个方向的移动概率是1/2=50%。

如果某个格子有三个方向可以移动,那么,每个方向的移动概率是1/3=33.33%。

如果某个格子四个方向都可以移动,那么,每个方向的移动概率是1/4=25%。

这个概率值便是动作策略值。

可见,所谓“策略表”,便是取每个可移动方向的均匀概率。

下面我们编程画策略表:

我们把策略表的左上角,放在表格的第16行,第2列,我们定义两个常量代表这两个数字。
定义常量的代码如下:

Const 策略表头行 = 16 '策略表左上角行号Const 策略表头列 = 2 '策略表左上角列号

这两句代码与之前定义的迷宫常量和参数表常量放在一起,放在全体程序的最上方,如下图:

然后我们创建一个新的过程:“画策略表”,并定义内部利用的局部变量。
如下:

Sub 画策略表() '设置动作方向的均匀概率 Dim 参数之和 As Single '迷宫每个位置的动作参数之和 Dim 动作概率 As Single '迷宫每个位置动作概率(策略π) Dim 概率梯度 As Single '迷宫动作概率的增加梯度 Dim 行号 As Integer '参数表或策略表的行号 Dim 列号 As Integer '参数表或策略表的列号End Sub

接着,在Dim语句下面写“画表头”的语句,如下(代码显示不全可旁边划动):

'画策略表表头 Cells(策略表头行 - 2, 策略表头列 + 1).Value = "策略表π(动作概率)" Cells(策略表头行 - 2, 策略表头列 - 1) = "位置编号/" Cells(策略表头行 - 1, 策略表头列 - 1) = "移动方向" Cells(策略表头行 - 1, 策略表头列 + 0) = "0-上" '画策略表动作的方向 Cells(策略表头行 - 1, 策略表头列 + 1) = "1-右" '画策略表动作的方向 Cells(策略表头行 - 1, 策略表头列 + 2) = "2-下" '画策略表动作的方向 Cells(策略表头行 - 1, 策略表头列 + 3) = "3-左" '画策略表动作的方向 '画策略表边框 With Range(Cells(策略表头行, 策略表头列), Cells(策略表头行 + 8, 策略表头列 + 3)) .BorderAround LineStyle:=xlContinuous '画策略表四边单线边框 End With

然后,我们向策略表中填数据

实现方法是根据“动作参数表”,从第0行开始,首先对该行所有列的值求和,赋给变量“参数之和”。

然后用该行每一列的值除以“参数之和”,得到该方向的动作概率。

一样平常来说,“策略表”中可以直接填写动作概率值。
但是,为了后面编程方便,我们把动作概率值转成阶梯概率值,再填写。

举例来说,假设某一格子有三个方向可以移动,那么,每个方向的动作概率为1/3=0.3333。
如下表所示:

0-上

1-右

2-下

3-左

0

0.3333333

0.3333333

0.3333333

而“阶梯概率”,是把当前列的动作概率值与左边所有列的动作概率值累加。
例如,上表中第2列的值是0.3333,其左边两列的值分别为0和0.3333,三者累加和为0.6666。
由于累加值像阶梯一样逐渐上升,以是称为阶梯概率。
如下表:

0-上

1-右

2-下

3-左

0

0.3333333

0.666667

1

下图为阶梯概率的示意图:

下面是向策略表中填写阶梯概率的代码:

'填写策略表参数 For 行号 = 0 To 8 '策略表行号从0到8循环 Cells(策略表头行 + 行号, 策略表头列 - 1) = 行号 '在策略表左侧写迷宫编号 阶梯概率 = 0 '策略表每行的阶梯概率初值为0 For 列号 = 0 To 3 '策略表列号从0到3循环 '求参数表中每行的参数之和 参数之和 = Application.Sum(Range(Cells(参数表头行 + 行号, 参数表头列), Cells(参数表头行 + 行号, 参数表头列 + 3))) 动作概率 = Cells(参数表头行 + 行号, 参数表头列 + 列号) / 参数之和 '求单元格每个方向的动作概率 阶梯概率 = 阶梯概率 + 动作概率 Cells(策略表头行 + 行号, 策略表头列 + 列号) = 阶梯概率 '将阶梯概率填入策略表 Next 列号 Next 行号

程序的主体构造依然是两个嵌套在一起的For循环语句,外圈的For循环是策略表的行号,从0到8,共9行。
内圈的For循环是策略表的列号,从0到3,分别代表四个方向,即:0-上,1-右,2-下,3-左。

在循环体中,先调用Excel中的内部函数Sum()打算出当前行中的『参数之和』。

然后分别取每一列的参数值除以参数之和,得到『动作概率』。

末了分别按列累加『动作概率』得到『阶梯概率』,填写到策略表中。
结果如下图右下方表格:

有了策略表,就可以让老鼠按这个策略动起来了。

下面学一个新的编程方法:编写自定义函数。

06 自定义函数

举个例子,y=sin(x) 是一个三角函数。
我们任意给定一个x值,三角函数能打算出对应的y值。

比如,x=30(度),打算出:y=0.5

下图是用三角函数打算器打算sin(30)得出的结果:

所谓自定义函数,相称于编写这个打算器的某一个打算功能。

首先,定义一个函数,定义函数的方法如下:

Function 函数名(参数1 As 类型 , 参数2 As 类型 , ...... ) As 类型

[打算过程]

函数名=打算结果

End Function

第一个词Function表示要定义一个函数。

空一格后写函数名,可以用英文,也可以用中文。
函数名后跟一对括号。

括号里写函数的参数。
参数可以没有,也可以有一个或多个参数,多个参数用英文的逗号『,』隔开。

参数与变量一样,可以用英文,也可以用中文,参数名后面接As指定参数的类型。
参数的类型与变量的类型相同。

参数写完后,以括号结束。
括号外还有一个As用于指定函数返回值的类型。

例如:

Function 平方(数值 As Single) As Single

... ...

End Function

我们定义了一个叫做“平方”的函数,个中有一个参数,叫做“数值”,类型是浮点型。
函数的返回值类型也是浮点型。

当我们在编程环境中输入函数的定义后,按回车键,会自动涌现End Function

,代表函数的结束。

我们在两句之间输入函数的内容,即打算这个“数值”的平方。
如下:

Function 平方(数值 As Single) As Single Dim 结果 As Single '定义一个浮点型变量 结果 = 数值 数值 '打算数值的平方 平方 = 结果 '把打算结果赋值给函数名End Function

这个函数很大略,功能是打算任意给天命值的平方。

在函数内部,首先定义一个变量“结果”,是浮点型变量。

然后把“数值”和“数值”相乘打算平方,并把打算结果负值给“结果”。

末了把“结果”赋值给函数名“平方”。
这一句必须写,否则,调用函数者将得不到返回的打算结果。

写好的函数须要在另一个过程或函数中调用,函数调用格式是:

吸收者=函数名(参数1,参数2,......)

个中吸收者可以是变量,也可以是某个输出工具,如单元格。

调用的函数名必须与自定义的函数名完备一样。

括号里的参数数量、参数类型和前后顺序必须与定义函数时的参数完备一样,否则会出错。
参数可以是变量,也可以直接写数值。
变量名称可以与函数定义的参数名称不同。

下面我们写一个测试过程,调用“平方”函数。
如下:

Sub 测试() Cells(2, 1) = 平方(2) '打算2的平方,并填入单元格 Cells(2, 2) = 平方(3) '打算3的平方,并填入单元格 Cells(2, 2) = 平方(4.6) '打算4.6的平方,并填入单元格End Sub

请把光标移到“Sub 测试()”过程内部,按F5键实行,打算结果如下:

4

9

21.15999985

可以看出,编写自定义函数的好处是,可以多次调用函数,打算不同的值,避免重复写代码。

除了自定义函数以外,前面先容过的Sub过程,同样可以利用参数,定义和调用方法与Function函数完备同等。
后面我们会编写带参数的Sub过程。

07 策略选择函数

根据画好的策略表,老鼠可以进行策略选择,以确定下一步的移动方向。

策略选择的办法很大略,首先产生一个0~1之间的随机数,根据随机数的值查策略表,随机数落在哪个范围区间,就选择该区间所对应的列作为移动的方向。

比如,假设某行的策略表的值如下所示:

0-上

1-右

2-下

3-左

0

0.3333333

0.666667

1

如果得到一个随机数是0.5,根据上表,0.5处于0.3333和0.6666之间,我们统一向取右边界取值,取0.6666对应的方向,即取第2列,方向为“下”。

策略选择的程序是一个自定义函数,名为“策略选择”,包含一个参数“位置编号”。
这个参数是老鼠在迷宫中确当前单元格编号,也是参数表或策略表确当前行号。
函数代码如下:

Function 策略选择(位置编号 As Integer) As Integer '按策略表选择指定行号的动作方向 Dim 随机数 As Single '浮点型变量:随机数 Dim 策略值 As Single '浮点型变量:策略表中的值 Dim 策略表列号 As Integer '策略表列号,取值:0、1、2、3 Randomize (Timer) '初始化随机数 随机数 = Rnd() '取得一个0~1之间的随机数 '按照策略表中的动作概率确定移动方向 For 策略表列号 = 0 To 3 '依次指定策略表的四个方向 策略值 = Cells(策略表头行 + 位置编号, 策略表头列 + 策略表列号) '从策略表取出指定行列的策略值 If 随机数 <= 策略值 Then '如果随机数小于即是该策略值 策略选择 = 策略表列号 '返回策略表确当前列号 Cells(策略表头行 + 位置编号, 策略表头列 + 策略表列号).Font.ColorIndex = 7 '策略表当选中的参数变粉色 Exit For '跳出循环 End If Next 策略表列号 End Function

函数在内部产生一个0~1之间的随机数,然后根据参数传入的指定行,依次判断每一列的策略值,如果随机数小于即是某列的策略值,则把该列号返回给函数调用者,并跳出循环,函数结束。

08 求取下一步的行号和列号

函数“策略选择()”可以得到下一步的移动方向,接着我们须要根据这个方向求出老鼠移动后的位置,即在迷宫中的新的行号和列号。

我们创建一个Sub()过程实现这个功能。
过程名字叫“下一步位置”,这个过程与自定义函数Function()一样带有参数。

过程“下一步位置”的定义如下:

Sub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer)End Sub

这个过程的括号里有三个参数:“当前方向”、“当前行”、“当前列”,数据类型都是整型(Integer)。
但是,在“当前行”和“当前列”前面,还有一个特殊的词“ByRef”,它的意思是“传地址”。

“传地址”是一种分外的参数通报办法,可以大略理解为参数的值可以“双向传送”。

如果定义参数时,参数名称之前不写“ByRef”,如本例第一个参数“当前方向”前面就没有“ByRef”,那么这个参数只能单向传送数据,即只能从调用者向过程或函数内部通报,而不能反向通报。

反之,如果参数名称之前写了“ByRef”,比如第二和第三个参数“当前行”和“当前列”,那么这个参数可以双向传送数据,既可以从调用者向过程或函数内部通报,又可以反向从过程或函数向调用者通报数据。

下面看这个过程的实现办法。
假设当前老鼠在迷宫编号为4的位置,如下图,它可以向三个方向移动:右、下和左。

按照迷宫内部的行列号,如图中赤色编号所示,老鼠所在的“当前行”为1,“当前行”为1。

如果下一步的方向是“左”,则新的“当前列”即是原“当前列”-1,即1-1=0;

如果下一步的方向是“右”,则新的“当前列”即是原“当前列”+1,即1+1=2;

如果下一步的方向是“下”,则新的“当前行”即是原“当前行”+1,即1+1=2;

当然,如果老鼠可以向上移动的话,则新的“当前行”即是原“当前行”-1,即1-1=0;

下面的代码即实现以长进程:

Sub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer)'打算出下一步的行号和列号(迷宫内)并返回 Select Case 当前方向 Case 0 '"0=上" 当前行 = 当前行 - 1 '新的“当前行”的值会返回给调用者 Case 1 '"1=右" 当前列 = 当前列 + 1 '新的“当前列”的值会返回给调用者 Case 2 '"2=下" 当前行 = 当前行 + 1 '新的“当前行”的值会返回给调用者 Case 3 '"3=左" 当前列 = 当前列 - 1 '新的“当前列”的值会返回给调用者 End Select End Sub

这段代码利用了一个新的语句:Select Case语句。

Select Case语句与If语句一样都是分支语句。
常日,当一件事情只有两种选择时,比如非黑即白,常用If语句分别对黑和白进行处理。

如果一件事情的选择多于两种,那么利用Select Case语句会更方便。

利用方法如下:

Select Case 条件表达式

Case 情形表达式1

[程序代码1]

Case 情形表达式2

[程序代码2]

Case 情形表达式3

... ...

Case Else

[程序代码n]

End Select

程序的实行过程是,当“条件表达式”的值即是“情形表达式1”时,即实行[程序代码1],当“条件表达式”的值即是“情形表达式2”时,即实行[程序代码2],... ...,以此类推。
如果,所有情形都不知足,则实行Case Else后面的[程序代码n]。

09 老鼠随机行走

首先,我们学习一个新的循环语句Do While ... Loop循环,格式如下:

Do While 逻辑表达式

[程序代码1]

( Exit Do ) '跳出循环,此句为选用

[程序代码2]

Loop

之前我们学过For循环语句,个中的循环次数是固定的,每当达到次数后,循环就会结束。
但是,有时候我们并不知道详细的循环次数,这时就要用Do While循环语句。

程序实行时,首先判断“逻辑表达式”是否为“真”。
在VBA中,所有非零的值都代表“真”,即是0的值为“假”。

如果“逻辑表达式”为“真”,则实行循环体中的[程序代码1];如果为“假”,则不循环,直接跳到Loop后面,实行下面程序。

如果程序中写有“Exit Do”,实行到此句时,结束循环,跳到Loop后面,实行下面的程序。

如果,没有“Exit Do”,则连续实行[程序代码2];

实行到“Loop”时,程序跳回到Do While语句,再次判断“逻辑表达式”是否为“真”。
如果为真,则连续实行循环体内的程序;如果为“假”,则结束循环,跳到Loop后面,实行下面的程序。

Do循环语句还有一种写法,把While判断放到Loop后面,格式如下:

Do

[程序代码1]

( Exit Do ) '跳出循环,此句为选用

[程序代码2]

Loop While 逻辑表达式

利用规则与Do While循环完备同等,唯一差异是Do循环先实行循环体里的程序代码,后判断逻辑表达式是否为“真”。
如果是“真”,则跳回到Do语句重新循环;如果为“假”,则结束循环,实行下面的语句。

下面我们看让老鼠随机行走的程序代码:

Sub 随机行走() Dim 老鼠行号 As Integer '老鼠在迷宫中的行号 Dim 老鼠列号 As Integer '老鼠在迷宫中的列号 Dim 当前编号 As Integer '老鼠在迷宫中的序号,也是参数表和策略表的行号。
Dim 表列号 As Integer '策略表的列号,对应下一步的移动方向 '设定老鼠的起始位置 老鼠行号 = 迷宫头行 老鼠列号 = 迷宫头列 Do While True '无条件循环 延时 (0.1) '延时0.1秒 Cells(老鼠行号, 老鼠列号) = "" '删除当前位置的“老鼠”,改为空字符 当前编号 = (老鼠行号 - 迷宫头行) 3 + (老鼠列号 - 迷宫头列) '根据行列号打算老当前位置的编号 表列号 = 策略选择(当前编号) '调用“策略选择”函数得到下一步的方向 下一步位置 表列号, 老鼠行号, 老鼠列号 '调用“下一步位置”过程取得下一步的行号和列号 If Cells(老鼠行号, 老鼠列号) = "食品" Then '如果下一步的单元格中有“食品” Cells(老鼠行号, 老鼠列号) = "老鼠" '不才一步单元格中填“老鼠” Cells(迷宫头行 + 2, 迷宫头列 + 2).Font.ColorIndex = 5 '字体颜色改为蓝色 延时 (0.5) '延时0.5秒 Exit Do '目标达成,退出循环 End If '老鼠还没有碰着食品 Cells(老鼠行号, 老鼠列号) = "老鼠" '不才一步单元格中填“老鼠” Cells(老鼠行号, 老鼠列号).Font.ColorIndex = 1 '字体颜色设为玄色 LoopEnd Sub

这个Sub过程名为“随机行走”,程序的每一行都有注释,我们就不对每一句都阐明了。

程序的主体是 Do While True 开头的一个无限循环,个中“True”代表“真”,以是这个循环的条件永久知足,会一贯循环。

个中调用了一个“延时”过程,用来等待指定的韶光,单位是秒。
如果不该用延时,老鼠会运动得太快,肉眼难以识别。
这个“延时”过程也是自定义的,代码放在后面。

让老鼠产生移动效果的方法很大略,首先删除迷宫中当前位置的老鼠,即把老鼠当前所在的单元格写为空字符“”。

然后打算老鼠在迷宫中的编号,亦即参数表和策略表确当前行号。

根据当前行号,调用“策略选择”函数,得到下一步的方向,也便是策略表的“表列号”。

然后调用“下一步位置 ”过程,得到下一步的行号和列号。

把稳,定义过程“Sub 下一步位置()”时,括号里有三个参数。
调用它时也须要给定三个参数,但是不用加括号,调用语句是:

下一步位置 表列号, 老鼠行号, 老鼠列号

前面说过,后两个参数“老鼠行号”和“老鼠列号”利用的是“ByRef”传地址模式,意味着数据是双向通报的。
也便是说,调用过程时,“老鼠行号”和“老鼠列号”的值是当前的值,调用过程后,“老鼠行号”和“老鼠列号”被授予了新的值,即下一步的行号和列号。

以是接着用『Cells(老鼠行号, 老鼠列号) = "老鼠"』就可以把老鼠画在新的位置上。

反复如此循环就实现了老鼠的不断随机移动。

不过,去世循环是不好的,须要设置一个退出机制。

程序中的If 语句即完成退出功能。
If语句判断老鼠是否移动到有“食品”的单元格中,如果是“食品”,则把该单元格写成蓝色的“老鼠”,任务完成,用Exit Do语句退出循环,程序结束。

其他情形下,即下一步的单元格中没有“食品”,则“老鼠”写成玄色,并连续循环。

下面是延时过程的代码:

Sub 延时(T As Single) Dim time1 As Single time1 = Timer '取得当前韶光 Do DoEvents '等待 Loop While Timer - time1 < T '韶光差知足条件则退出循环End Sub

末了编写一个“主程序”:

Sub 主程序() Range(Cells(迷宫头行, 迷宫头列), Cells(迷宫头行 + 4, 迷宫头列 + 3)).Clear '清空迷宫区域 初始化 '调用“初始化”过程 随机行走 '调用“随机行走”过程,随机走迷宫 End Sub

主程序只有三句话,首先清空一块迷宫区域,然后调用“初始化”过程,末了调用“随机行走”过程。

我们把“主程序”放到代码区域的最上方,是在定义常量的后面的第一个Sub过程。

请把以上的所有代码录入或复制到编程窗口中,为了避免丢失内容造成程序运行缺点,我把程序所需所有过程和函数的定义部分罗列如下,大家自行把文章中的代码填进去即可:

'以下定义常量Const 迷宫头行 = 4 '迷宫左上角的行号Const 迷宫头列 = 2 '迷宫左上角的列号Const 参数表头行 = 4 '动作参数表左上角行号Const 参数表头列 = 8 '动作参数表左上角列号Const 策略表头行 = 16 '策略表左上角行号Const 策略表头列 = 2 '策略表左上角列号Sub 主程序() Range(Cells(迷宫头行, 迷宫头列), Cells(迷宫头行 + 4, 迷宫头列 + 3)).Clear '清空迷宫区域 初始化 '调用“初始化”过程 随机行走 '调用“随机行走”过程,随机走迷宫 End SubSub 初始化() Dim 列号 As Integer '把变量 列号 定义为整型 For 列号 = 1 To 20 '列号从1到20循环 Cells(1, 列号) = 列号 '循环体,向指定单元格写入数据 Next 列号 '每循环一次列号加1,大于20退却撤退出循环 Range(Cells(2, 1), Cells(26, 12)).Clear '清空指定矩形区域 画迷宫 '调用“画迷宫()”过程 画动作参数表 '调用“画动作参数表()”过程 画策略表 '调用“画策略表()”过程End SubSub 画迷宫() ... ...End SubSub 画动作参数表() '画动作参数表(θ) ... ...End SubSub 画策略表() '设置动作方向的均匀概率 ... ... End SubFunction 策略选择(位置编号 As Integer) As Integer '按策略表选择指定行号的动作方向 ... ... End FunctionSub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer) ... ... End SubSub 随机行走() ... ...End SubSub 延时(T As Single) ... ...End Sub

下面运行这段程序:将光标移到“主程序”过程内部,按F5功能键运行程序,就可以看到老鼠在迷宫中随机移动,像没头的苍蝇一样,直到移动到写有“食品”的单元格中时,程序结束。

至此,我们编出了一只“傻老鼠”,会随机乱动,然而,它不会学习履历也不会吸取教训。

下一篇文章 ,我们将授予老鼠“强化学习”的能力,这样它就会逐步记住走哪条路能更快“吃”到食品,逐渐学会走最短的路径达成目标。

敬请期待,有问题请留言。