1. Minimax 极小化极大算法
搜索策略中的一种(本质零和博弈),旨在让最差的情况得情况出现机会最小,比如让对手赢的机会。往往自身作为树中的max方,选择其子节点中含有value值最大的节点;而作为树中的min方则相反(对手),选择其子节点中含有value值最小的节点。下面上图说明会好理解一点
假设一个有限层数的搜索树如下,我们作为先手方max
max方一般用正方形表示,而min方一般用圆形表示,最底层叶子节点上的数字代表用评估函数计算出的所有可行路径的评估值(根据实际需求定义)
从根节点开始,每个非叶子节点继续递归调用到叶子节点或者存有评估值的节点
以左半边为例子(右同)
- 首先从根节点一直递归到A节点,此时为叶节点,发现其下有三条路径对应评估值10、12、15 ,由于A节点为Min节点,所以应选择三者中的最小值,即10。将此结果记录在A节点上并返回。
- 如果想要判断C节点对应的评估值需要A与B中取得极大值(C为Max节点),而递归调用B节点由于只有一条路径,所以直接记录返回。此时C节点的评估值更新为 max(10,9) = 10。
- 对G节点的右子树重复上述两步,得到D节点评估值 min(5, 15) = 5 ; 得到E节点评估值 min(51, 4, 22) = 4; 得到F节点评估值 max(5, 4) = 5 。
- 综上求得G节点的评估值为 min(10,5) = 5 。完成了左子树,如法炮制右子树就可以得出根节点的最终评估值,如下图所示(深色即为算法选择路径)
通过上述讲解我们发现当层数过多时,计算压力指数提升,所以我们需要通过一些方法提升效率。比如减少实际递归深度,规定到一定层数就不再继续递归,而是以近似的估计函数得到其子节点的估计值(这种做法会导致达不到最优解情况,比如我打一把3分钟的游戏,对手在1.5分钟就推掉了我两座公主塔,那能说明我一定会输吗?可能我在倒计时结束前推掉其国王塔拿下胜利…)
另一种做法就是剪枝,通过减少不必要的枝叶判断来提高搜索效率。一种最常见的算法就是α-β剪枝
2. α-β剪枝(Alpha-Beta Pruning)
α表示所有可行解中的最大下界,而β表示所有可行解中的最小上界,算法伪代码如下:
#α剪枝
def max-value(state, α, β):
initialize v = -∞ #起始最大下界
for each successor of state:
v = max(v, max-value(successor, α, β))
if v ≥ β return v #遍历过程中只要存在一处α ≥ β,则后续兄弟节点就无需遍历
α = max(α, v) #否则判断当前节点现有最大下界是否大于子节点的最大值
return v
#β剪枝
def min-value(state , α, β):
initialize v = +∞ #起始最小上界
for each successor of state:
v = min(v, min-value(successor, α, β))
if v ≤ α return v
β = min(β, v)
return v
总结来说,只要出现α ≥ β的情况,就剪枝,下面通过图片方式模拟一遍算法过程
还是刚才的例子
- 递归到最右下角的叶子节点,由于该节点是min节点且初始化的α、β没有被更新过,所以我们将β更新为最小值10并返回,此时回溯到其父节点,将其的α值设置为10(因为该节点为max节点,找孩子节点的最大值,此时已经知道一个是10了,如果剩下一个孩子节点小于10,则无需进行遍历,即等同于α ≥ β )
-
注意,子节点会延续父节点自带的α、β值!由于只有一个节点直接更新,更新后发现α ≥ β,即无需在继续遍历其他节点,直接返回当前β值,由于其父节点原α值大于9,所以不做更新,继续返回到其父min节点…
-
下面遍历右子树,递归调用到叶子节点,由于其最小值是5,5<β=10,所以更新该节点的β值并返回value到上一节点,其父节点即根节点会更新α(-∞<5)为5,然后继续向右子树递归
-
由于右子树为叶子节点,直接遍历其评估值,第一个51显然大于当前β即10,第二个4小于当前β,所以更新其值,然而更新完后发现α ≥ β,所以无需再遍历后续节点,直接返回到根节点,此时左子树就已经遍历完成了,根节点处接收到的返回值5即是左子树的返回值,由于我们是max节点,所以更新α为5!!
-
当递归到子节点时,由于是min节点且11小于正无穷,所以更新β,将value返回父节点并赋值给其α(由于原α值为5<11),然后继续递归右子树
-
33、55中最小值33,需更新其节点的β值,将value返回父节点并赋值给其α(由于原α值为11<33)
- 再向上返回一层,将value值33赋值给其β值,并继续递归右子树,此时发现14<33,应更改β值
- 最后一个叶子节点其最小评估值为16,即应更新原来的β(16<33)
- 向上返回,更新父节点的α值(14<16),然后再返回value值到上层节点,此时β = min(33,16) = 16
- 最终左右子树都遍历完成,右子树的返回值为16>5(左子树的返回值),我们可以看到同原先结果相同
3. 评估函数
前面我们反复提到评估函数,那么这个究竟是什么东东呢?
官方解释 : 输入一个局面的信息,输出是一个表明相应局面好坏程度的数值
不同的场景下适用相应的评估函数,这样才能更好的估计。一种常见的评估函数是穷尽每种可能解×其相应权重
以井字棋为例,我们往往用x方出现胜利的可能减去o方出现胜利的可能
经过枚举f(O)=4 , f(X)=5, 所以在这种情况下X的赢面大一点,经常下的肯定不难看出!
4. 总结
剪枝往往结果是好的,能减少不必要的递归提升效率,但是其本质任然是一个深度遍历递归,当通过评估函数得到的评估值很极端时就会导致剪不了几条枝叶,达不到提升性能的目的(像上面这个例子,基本上就剪了一条无关紧要的,其他还是得遍历🙃)
Minimax - 当我们的对手表现最佳时使用,可以使用α-β修剪进行优化。万一遇上不太聪明的对手呢。。。
Minimax提供了比expectimax更保守的操作,因此往往当对手未知时,会产生有利的结果