Minimax算法
Minimax 算法又叫极小化极大算法,是一种找出失败的最大可能性中的最小值的算法。(维基百科)
alpha-beta 剪枝
Minimax算法中,由于每个节点都是取的极值,所以可以利用这个性质进行剪枝。
以上是简介,下面通过井字棋这个游戏,按照
DFS -> Minimax -> a - b剪枝
这样的顺序进行引导
一、需要解决的问题
井字棋中,我们要做到电脑每一步棋都是下在所有他可以下的位置中对他最有利的位置。
也就是Minimax算法的找出失败的最大可能性中的最小值,换句话也就是找出当前可以做的动作中对自己最有利的动作。
二、DFS -> Minimax
对于井子棋中,当我们考虑这一步应该下在那个位置上的时候,是基于当前局面进行考虑的,所以现在假设当前局面是初始局面,这时候还没有下棋。
那么我们可以下的位置有9个,然后对于我们下的每一种情况,形成一个新的局面,然后到了对方下棋,对于每一个分支在对方看来又有8种情况,又形成很多局面。下面给出部分图示,没有涉及到全部情况:
那么对于全部的图示,可以形成一颗搜索树,最后的叶子节点就是平局和某一方赢了的全部局面。
那么可以想到,对于上面的图示,是可以用DFS实现的。
虽然DFS可以通过遍历所有情况找到很多条可以使得最后局面达到我方不输或者赢下比赛的路线,但是这是一场博弈,搜索树中牵扯到了对方的下棋位置,对方是不可能配合我们的。
所以我们应该在DFS的同时,求出当前来说我们可以下的所有位置中,对我们最有利的位置,而不是对我们有利的位置中的随便一个位置。
这时候,Minimax算法就能解决这个问题,它在搜索上加上了博弈。
所以
Minimax = 搜索 + 博弈
首先Minimax算法第一步就是规定一下怎样把一个局面映射成一个值,而这个值代表的是对于当前下棋者他到达这个局面的时候的赢面大小。
值越大,赢面越大。
然后我们需要映射的局面只有,最后叶子节点的局面,也就是某一方胜利,或者平局的界面。
以下的规定都是在搜索我方现在应该下棋的位置的基础上:
1.当当前局面为我方胜利的时候,对应的值为空格数 +1
2.当当前局面为对方胜利的时候,对应的值为-(空格数+1)
(因为对方胜利的局面,对于我们来说是极其不利的所以取负数)
3.平局,对应的值为0
当这样规定之后,叶子节点都会对应的一个值。
然后,在上图中可以发现,奇数层是我方在下棋,偶数层是到了对方下棋。
假设现在是在奇数层,并且他的所有子儿子的局面赢面大小值也已经求出来了。
那么奇数层作为我方下棋,并且现在是在搜索我方的位置,所以奇数层的父节点,都会选择各自子儿子节点中的赢面最大的那个局面。
假设现在是偶数层,并且他的所有子儿子的局面赢面大小值也已经求出来了。
由于这个树的根节点是我方下棋的时候,也就是第1层为奇数层,也就代表着,根节点会选择所有子儿子中赢面最大的。那么作为对方肯定会想办法减少我们的赢面值,那么他在每次选择的时候,肯定会选择所有子儿子中赢面最小的,从而达到影响我们最后可以取到的最大值。
那么结论就是:
当为奇数层的时候,选择所有子儿子中的最大值
当为偶数层的时候,选择所有子儿子中的最小值
那么最后我们根节点所有子儿子中最大值对应的那个下法,就是我们在对方极致干扰的情况下,赢面最大的下法。
以上就是Minimax算法的思想
在算法实现的时候,就是DFS的同时,处理奇偶层的取值,和赢面值的返回。
/*
井字棋
OXO
OOO
OOO
X代表玩家,O代表电脑
从左上角开始编一维下标为0 ~ 8,二维下标为(0,0)~(2,2)
一维位置转化成2维公式
设t为1维下标,(x,y)为2维下标
x = t / 3,y = t % 3
t = x * 3 + y;
*/
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
char board[3][3];//棋盘
int player;//记录当前是玩家还是电脑在下棋,0代表玩家,1代表电脑
void init()//初始化棋盘
{
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++) board[i][j] = '_';
}
void draw_board()//画棋盘
{
system("cls");
for(int i = 0;i < 3;i ++)
{
for(int j = 0;j < 3;j ++)
printf("%c ",board[i][j]);
puts("");
}
}
int is_Win()//判断谁赢了
{
for(int i = 0;i < 3;i ++) //判断行列
{
int x = board[i][0] != '_' && board[i][0] == board[i][1] && board[i][1] == board[i][2];//行
int y = board[0][i] != '_' && board[0][i] == board[1][i] && board[1][i] == board[2][i];//列
if(x) return (board[i][0] == 'X' ? 0 : 1);
if(y) return (board[0][i] == 'X' ? 0 : 1);
}
if(board[0][0] == 'X' && board[1][1] == 'X' && board[2][2] == 'X' || board[0][2] == 'X' && board[1][1] == 'X' && board[2][0] == 'X') return 0;
if(board[0][0] == 'O' && board[1][1] == 'O' && board[2][2] == 'O' || board[0][2] == 'O' && board[1][1] == 'O' && board[2][0] == 'O') return 1;
return -1;
}
int eval()//评估函数,当电脑赢的时候,评估为空格数+1,玩家赢的时候评估为-空格数-1,平局的时候为0
{
int res = 0;
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++)
if(board[i][j] == '_') res ++;
int flag = is_Win();
if(flag == 1) return res + 1;
if(flag == 0) return -(res + 1);
if(flag == -1) return 0;
}
int MinMaxSearch(int &idx,int step)//step用来记录层数,奇数层为电脑操作,偶数层为玩家操作
{
int val;//极值
if(step & 1) val = -100;//奇数层取极大值
else val = 100;//偶数层取极小值
if(is_Win() >= 0) return eval(); //有一方赢了
vector<int> positions;//记录还有那些位置可以下棋
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++)
if(board[i][j] == '_') positions.push_back(i * 3 + j);
if(positions.size() == 0) return eval();//平局的情况
for(int i = 0;i < positions.size();i ++)
{
int x = positions[i];
int t = x;
board[x / 3][x % 3] = (step & 1) ? 'O' : 'X';
int Sonval = MinMaxSearch(x,step + 1);
board[t / 3][t % 3] = '_';
if(step & 1)
{
if(val < Sonval)
{
val = Sonval;
if(step == 1) idx = positions[i];
}
}
else
{
if(val > Sonval)
{
val = Sonval;
}
}
}
return val;
}
/*
int MinMaxSearch(int &idx,int step,int a,int b)//剪枝版本,a代表最大下限,b代表最大上限
{
if(is_Win() >= 0) return eval(); //有一方赢了
vector<int> positions;//记录还有那些位置可以下棋
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++)
if(board[i][j] == '_') positions.push_back(i * 3 + j);
if(positions.size() == 0) return eval();//平局的情况
for(int i = 0;i < positions.size();i ++)
{
int x = positions[i];
int t = x;
board[x / 3][x % 3] = (step & 1) ? 'O' : 'X';
int Sonval = MinMaxSearch(x,step + 1,a,b);
board[t / 3][t % 3] = '_';
if(step & 1)
{
if(a < Sonval)
{
a = Sonval;
if(step == 1) idx = positions[i];
if(a >= b) break;
}
}
else
{
if(b > Sonval)
{
b = Sonval;
if(a >= b) break;
}
}
}
if(step & 1) return a;
else return b;
}
*/
void com_play()
{
int x;//电脑下的位置
MinMaxSearch(x,1);
//MinMaxSearch(x,1,-100,100);//a - b剪枝
board[x / 3][x % 3] = 'O';
}
int main()
{
while(1)
{
init();
system("cls");
printf("请输入先手,0代表玩家,1代表电脑\n");
printf("请输入你的选择:");
scanf("%d",&player);
int i;
for(i = 1;i <= 9;i ++)
{
if(player & 1)//电脑下棋
{
com_play();
draw_board();
}
else //玩家下棋
{
draw_board();
printf("请输入需要下的位置,棋盘从左上角开始编号为1~9:");
printf("请输入你的选择:");
int x;
scanf("%d",&x);
x -= 1;
if(x < 0 || x > 8 || board[x / 3][x % 3] != '_')
{
do
{
printf("该位置不能下,请另选一个\n");
printf("请输入你的选择:");
scanf("%d",&x);
x -= 1;
}while(x < 0 || x > 8 || board[x / 3][x % 3] != '_');
}
board[x / 3][x % 3] = 'X';
}
player ^= 1;//改变操作对象
int Winer = is_Win();//看是否有人赢了
if(Winer >= 0)
{
draw_board();
if(Winer) printf("电脑赢了\n");
else printf("你赢了\n");
break;
}
}
if(i >= 9)
{
draw_board();
printf("平局\n");
}
printf("是否继续,1代表继续,0代表不玩了\n");
int op;
scanf("%d",&op);
if(!op) break;
}
return 0;
}
Minimax -> a - b剪枝
可以发现,Minimax算法中遍历了从当前局面开始的所有局面情况,所以当应用到其他情况多的博弈中的时候时间复杂度太高了。
a-b剪枝就是在利用Minimax算法中每一个节点都是对应的是他所有子儿子的极值的这个性质,来进行剪枝的。
当进行Minimax算法的时候,当我们知道所有子儿子的值后,可以得出该父节点的值。
但是如果只知道部分子儿子的节点值的时候, 虽然得不到该节点的准确值,但是可以得到该节点的取值范围
以奇数层为例子,偶数层类似分析
给每个节点设立一个a,b
表示这个节点取值的最大下限和最小上限
当奇数层节点的部分子儿子计算出来后,因为奇数层节点取的是最大值,所以 它的a会发生变化,值为计算出来的子儿子中的最大值。
那么在计算其他子儿子的时候,把a传下去
当a传下去之后,在计算子儿子的时候,就是偶数层了,那么计算出部分子儿子的子儿子后,子儿子的b会更新,当在这个子儿子上出现a>=b后,就可以不用继续计算这个子儿子还没有算出来的子儿子了,因为他的父节点取值>=a,而这个子儿子取值<=b。
/*
井字棋
OXO
OOO
OOO
X代表玩家,O代表电脑
从左上角开始编一维下标为0 ~ 8,二维下标为(0,0)~(2,2)
一维位置转化成2维公式
设t为1维下标,(x,y)为2维下标
x = t / 3,y = t % 3
t = x * 3 + y;
*/
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
char board[3][3];//棋盘
int player;//记录当前是玩家还是电脑在下棋,0代表玩家,1代表电脑
void init()//初始化棋盘
{
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++) board[i][j] = '_';
}
void draw_board()//画棋盘
{
system("cls");
for(int i = 0;i < 3;i ++)
{
for(int j = 0;j < 3;j ++)
printf("%c ",board[i][j]);
puts("");
}
}
int is_Win()//判断谁赢了
{
for(int i = 0;i < 3;i ++) //判断行列
{
int x = board[i][0] != '_' && board[i][0] == board[i][1] && board[i][1] == board[i][2];//行
int y = board[0][i] != '_' && board[0][i] == board[1][i] && board[1][i] == board[2][i];//列
if(x) return (board[i][0] == 'X' ? 0 : 1);
if(y) return (board[0][i] == 'X' ? 0 : 1);
}
if(board[0][0] == 'X' && board[1][1] == 'X' && board[2][2] == 'X' || board[0][2] == 'X' && board[1][1] == 'X' && board[2][0] == 'X') return 0;
if(board[0][0] == 'O' && board[1][1] == 'O' && board[2][2] == 'O' || board[0][2] == 'O' && board[1][1] == 'O' && board[2][0] == 'O') return 1;
return -1;
}
int eval()//评估函数,当电脑赢的时候,评估为空格数+1,玩家赢的时候评估为-空格数-1,平局的时候为0
{
int res = 0;
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++)
if(board[i][j] == '_') res ++;
int flag = is_Win();
if(flag == 1) return res + 1;
if(flag == 0) return -(res + 1);
if(flag == -1) return 0;
}
/*
int MinMaxSearch(int &idx,int step)//step用来记录层数,奇数层为电脑操作,偶数层为玩家操作
{
int val;//极值
if(step & 1) val = -100;//奇数层取极大值
else val = 100;//偶数层取极小值
if(is_Win() >= 0) return eval(); //有一方赢了
vector<int> positions;//记录还有那些位置可以下棋
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++)
if(board[i][j] == '_') positions.push_back(i * 3 + j);
if(positions.size() == 0) return eval();//平局的情况
for(int i = 0;i < positions.size();i ++)
{
int x = positions[i];
int t = x;
board[x / 3][x % 3] = (step & 1) ? 'O' : 'X';
int Sonval = MinMaxSearch(x,step + 1);
board[t / 3][t % 3] = '_';
if(step & 1)
{
if(val < Sonval)
{
val = Sonval;
if(step == 1) idx = positions[i];
}
}
else
{
if(val > Sonval)
{
val = Sonval;
}
}
}
return val;
}*/
int MinMaxSearch(int &idx,int step,int a,int b)//剪枝版本,a代表最大下限,b代表最大上限
{
if(is_Win() >= 0) return eval(); //有一方赢了
if(step & 1) a = -100;
else b = 100;
vector<int> positions;//记录还有那些位置可以下棋
for(int i = 0;i < 3;i ++)
for(int j = 0;j < 3;j ++)
if(board[i][j] == '_') positions.push_back(i * 3 + j);
if(positions.size() == 0) return eval();//平局的情况
for(int i = 0;i < positions.size();i ++)
{
int x = positions[i];
int t = x;
board[x / 3][x % 3] = (step & 1) ? 'O' : 'X';
int Sonval = MinMaxSearch(x,step + 1,a,b);
board[t / 3][t % 3] = '_';
if(step & 1)
{
if(a < Sonval)
{
a = Sonval;
if(step == 1) idx = positions[i];
if(a >= b) break;
}
}
else
{
if(b > Sonval)
{
b = Sonval;
if(a >= b) break;
}
}
}
if(step & 1) return a;
else return b;
}
void com_play()
{
int x;//电脑下的位置
// MinMaxSearch(x,1);
MinMaxSearch(x,1,-100,100);//a - b剪枝
board[x / 3][x % 3] = 'O';
}
int main()
{
while(1)
{
init();
system("cls");
printf("请输入先手,0代表玩家,1代表电脑\n");
printf("请输入你的选择:");
scanf("%d",&player);
int i;
for(i = 1;i <= 9;i ++)
{
if(player & 1)//电脑下棋
{
com_play();
draw_board();
}
else //玩家下棋
{
draw_board();
printf("请输入需要下的位置,棋盘从左上角开始编号为1~9:");
printf("请输入你的选择:");
int x;
scanf("%d",&x);
x -= 1;
if(x < 0 || x > 8 || board[x / 3][x % 3] != '_')
{
do
{
printf("该位置不能下,请另选一个\n");
printf("请输入你的选择:");
scanf("%d",&x);
x -= 1;
}while(x < 0 || x > 8 || board[x / 3][x % 3] != '_');
}
board[x / 3][x % 3] = 'X';
}
player ^= 1;//改变操作对象
int Winer = is_Win();//看是否有人赢了
if(Winer >= 0)
{
draw_board();
if(Winer) printf("电脑赢了\n");
else printf("你赢了\n");
break;
}
}
if(i >= 9)
{
draw_board();
printf("平局\n");
}
printf("是否继续,1代表继续,0代表不玩了\n");
int op;
scanf("%d",&op);
if(!op) break;
}
return 0;
}