目录
- 01 极大极小算法的思维
- 02 极大极小算法的雏形
- 03 极大极小算法的进化
内容
01 极大极小算法的思维
最初的人们开始下五子棋的时候,会选择当前局面对于自己优势高的方向发展。后来人们开始在这方面卷,开始投资未来,每一步棋都希望让接下来的几步棋成为高优势的方向发展,极大极小算法的思维就是在于此,这也比最初的思维高了不止一个层次。
02 极大极小算法的雏形
极大极小的核心思维如下:
模拟下棋,得到下棋最有未来优势的点;
在模拟下棋的过程中:
当自己下棋的时候,选择对于我们最有利的;
当对手下棋的时候,选择对于我们最劣势的;
假如你小气的哥哥要给你零花钱,他左右手拿着不同面值的钱币,让你选一个手,然后他挑一个钱币给你。假设:
左手: 1 5 100
右手: 10 20 50
当然选择右手,是因为小气的哥哥一定会选择钱最少的给你,说白了其实就相当于在1块和10块里面选择。若假如你是哥哥,你现在必须给你讨厌的弟弟零花钱,你现在左右手有上面例子的钱,你得选一个手让弟弟选里面的钱,你讨厌的弟弟一定会拿钱最大的。那么很明显,你会选择右手兜。那么实际上:
在你和你哥的博弈过程中:
当自己选择的时候,选择对自己最有利的;
当对手选择的时候,选择对自己最劣势的;
接下来介绍一下极大极小算法的基本知识:评估函数、模拟、深度。
评估函数:评估函数的作用是评价一个棋局对于我们的优劣状态,返回评估得到的整数。
当有了评估函数之后,利用返回的整数可以知道当前棋局对于我们的优劣,然后我们可以通过优劣进行相应处理。那么很高兴的是,极大极小算法通过评估函数的存在使得核心进化成了:
在模拟下棋的过程中:
当己方下棋时,选择分数最大的;
当敌方下棋时,选择分数最小的;
假设现在棋盘上有一个黑棋四连,但是有一边被白棋堵住了。那么,假如下一手是白方,则当前的局势对于棋手来说很不利,是因为若这次没有白棋五连,不堵则失败了;假如下一手是黑方,则当前局势对于棋手来说非常有利。那么可得:同一个棋局对于敌我的评估函数不相同。
假设黑白两方下棋。模拟到一定程度后便可以对模拟的棋局进行评估,得到对应的评估分数。
模拟:模拟的深度数越深,则代表模拟的次数越多,那么所考虑的结果也越多,所以可得:**模拟可以决定最终结果。**但实际上,对于一个五子棋棋局,模拟深度为1次时,可能需要200次左右,模拟深度为2时,则大概需要200*200次,模拟深度为3,4,5,则能够达到一个可怕的数字,计算时间需要达到几分钟、几个小时甚至几天。所以可得:比起模拟深度,模拟次数才是极大极小算法优化的最终形态的遍历控制。
深度:类似于树的结构,这里的深度代表模拟一次黑白下棋的次数。若读者学习过DFS(Depth First Search),那么可知:**深度可以方便我们进行程序的递归。**当深度为0时进行评估,直接返回评估分数。
下面是极大极小算法的逻辑代码:
int MinMax(int depth) {//程序入口分流
if (SideToMove() == Myself) return Max(depth);
else return Min(depth);
}
int Max(int depth) {
int best = -INFINITY;//系统最小值,这里可以换成分数不可达的下限
if (depth <= 0) return Evaluate();//假如深度为0,则不能在模拟,返回当前局面的评估值
GenerateLegalMoves();//得到当前棋局的符合规矩的下法的
while (hasLegalNext()) {//若存在符合规矩的下一个走法
MakeNextMove();//模拟开始
val = Min(depth - 1);//下一步为敌方落棋,则选择最小
UnmakeMove();//模拟回溯,不对初始棋局为产生任何效果
if (val > best) best = val;//更新最高的评估值
}
return best;
}
int Min(int depth) {
int best = INFINITY; //系统最大值,这里可以换成分数不可达的上限
if (depth <= 0) return Evaluate();
GenerateLegalMoves();
while (hasLegalNext()) {
MakeNextMove();
val = Max(depth - 1);
UnmakeMove();
if (val < best) best = val;
}
return best;
}
03 极大极小算法的进化
很明显的是,上面的代码似乎有点笨重了,因为Min和Max函数存在很多相同的地方,那么可以优化的是——将这两个方法进化成一个方法。值得一提的是,虽然在代码的设计中,会尽量减少程序间的耦合程度,意思是尽量把两个功能写成两个方法,但实际上,当我们优化Min和Max其中一个方法的时候,另外一个方法也需要进行相同的优化,所以这里的合并是存在必要的。
下面是负值极大算法逻辑代码:
int NegaMax(int depth) {
int best = -INFINITY;
if (depth <= 0) return Evaluate();
GenerateLegalMoves();
while (hasLegalNext()) {
MakeNextMove();
val = -NegaMax(depth - 1); // 注意这里有个负号,负号的存在使得不管你是哪一方,程序都求最大值即可。
UnmakeMove();
if (val > best) best = val;
}
return best;
}
很明显的是,这里的评估函数需要进行黑白无差别化,因为这里的评估函数是对于子递归深度为0的当前方而评估的。下面举例:
-
若深度为1,我方白棋,那么这里深度为0的时候就是针对的黑方:
-
深度为1,白方,递归;
-
深度为0,黑方,对于当前方(黑方)进行评估,返回评估分数;
-
深度返回1,白方,得到负值的评估分数,选取其中最大值,实则是,选取黑方的最小值;
-
-
若深度为2,我方白棋,那么这里的深度为0的时候就是针对的白方:
-
深度为2,白方,递归;
-
深度为1,黑方,递归;
-
深度为0,白方,对于当前方(白方)进行评估,返回评估分数;
-
深度返回1,黑方,得到负数的评估函数,选择其中最大值,实则选取白方的最小值;
-
深度返回2,白方,得到正数的评估函数,选择其中最大值,实则选取黑方的最小值,选取白方的最大值;
-
随着程序优化的情况下,两种情况的评估分数都是对于深度为0的当前方而言,则评估函数需要无差别化。
每个棋的评估函数都存在不一样的差异:对于井字棋,其评估函数的无差别化很简单,直接进行棋盘的正负交换即可。对于简单五子棋,同样如此。但是值得一提的是,返回相反数并不会对评估函数有任何影响。笔者在对这里理解的时候总会将负数带入评估函数,但实际上,利用相反数只是为平衡当前方的措施,即求返回分值的最大值,和评估函数没有任何关系,也不会对当前的评估有任何影响。
但是,若遍历并非是按照深度进行,而是按照遍历次数进行,那么对评估函数的要求更加严格。因为很有可能存在一种深度不平衡的情况,一部分递归较深,一部分递归较浅,那么若返回的分值相差奇数次,则在某一阶段得到的分值为一些是负数一些是正数,这里时候那些负数的就会被莫名地淘汰,反而失去了希望的效果。这个需要待笔者往后研究之后再商讨。