[CSP校内集训]贪吃蛇(阿尔法-贝塔剪枝)
题目
有两条蛇(1号蛇和2号蛇)在n行m列的地图上,地图上有障碍物。一条蛇碰到蛇身/障碍物/边界就会死。蛇身会不断长长——可以理解为蛇尾位置不会变,蛇只会向前伸展不会缩尾巴。两条蛇都绝顶聪明,如果自己能赢,一定会尽量快地赢;如果自己会输,一定会死得尽量晚。给出初始局面,两蛇轮流走,每次可以且必须向上下左右移动一格。1号蛇先走,请告诉我谁会在多少回合时赢。\((n,m\leq 20)\)且\(0\)的数量不超过\(50\)
\(\alpha - \beta\)剪枝
AlphaBeta剪枝算法是一个搜索算法,旨在减少在其搜索树中,被极大极小算法评估的节点数。
这是一个常用人机游戏对抗的搜索算法。它的基本思想是根据上一层已经得到的当前最优结果,决定目前的搜索是否要继续下去——百度百科
前提:双方绝顶聪明,各自为营
用我的话来说,这个剪枝方法是这样的一个结构:
-
所有的估价都是由同一个角度来看待的,不能说在1的回合说对1有利,在2的回合又说对2有利(应该说对1有害)
-
搜索树上先手通过一条边转移成后手
-
搜索树的叶子节点是估价函数,评估到达的这个状态的好坏,函数值越大对先手越有利,父亲节点的权值均由儿子转移来
-
奇偶性不同的层求的东西不一样,即两个人的策略不同,先手求的是所有儿子中最大的一个(即对先手最有利),后手求的是最小的一个(对先手最有害)
总而言之,就是搜索树的奇数层节点取\(max\),偶数层取\(min\),叶子节点需要自己估价(根据具体的题目)
扯了半天,如何剪枝?很简单,比如对于先手节点\(u\),它会取最大的儿子,假设当前权值为\(ans_u\),如果当前节点的儿子\(v\)的一个儿子\(vv\)权值小于\(ans_u\),就不用再遍历这个儿子\(v\)剩下的儿子了(因为儿子\(v\)的权值取\(min\){孙子}),这个儿子\(v\)的权值到目前为止可以判断一定小于\(ans_u\)了;后手相反(可能会有点抽象,建议画图理解)
本题思路
本题显然可以用\(\alpha - \beta\)剪枝(双方绝顶聪明且操纵的蛇不一样)
主要是估价函数的设计:如果先手无路可走,就是绝对有害,设为\(-inf\),但又活的越久越好,所以还要加上一个存活时间\(step\),后手同理
这个剪枝还是比较简单好理解的,主要注意不要把先后大小关系搞反了就行
Code(即本题的标程)
#include<bits/stdc++.h>
#define N 25
#define Min(x,y) ((x)<(y)?(x):(y))
#define Max(x,y) ((x)>(y)?(x):(y))
using namespace std;
const int INF = 10000;
int n,m,a[N][N],sx[2],sy[2];
int dox[4]={0,1,0,-1},doy[4]={1,0,-1,0};
template <class T>
void read(T &x)
{
char c;int sign=1;
while((c=getchar())>'9'||c<'0') if(c=='-') sign=-1; x=c-48;
while((c=getchar())>='0'&&c<='9') x=x*10+c-48; x*=sign;
}
int dfs(int step,int player,int alpha,int beta,int fx,int fy,int px,int py)//alpha是下界只降不增,beta是上界只增不降
{
bool flag=false;
int ans=(player==1 ? beta : alpha);//如果是1就会选尽量大
for(int i=0;i<4;++i)
{
int dx=fx+dox[i],dy=fy+doy[i];
if(dx<1||dy<1||dx>n||dy>m||a[dx][dy]) continue;
flag=true;
a[dx][dy]=1;
int now=0;
if(player==1) now=dfs(step+1,2,alpha,ans,px,py,dx,dy);//1号
else now=dfs(step+1,1,ans,beta,px,py,dx,dy);
a[dx][dy]=0;
if(player==1)//注意这里的玩家1在上面的剪枝中相当于后手的儿子
{
if(now>=alpha) return alpha;
//如果now比最小值大,那么这个节点会贡献值一定大于等于now,而最小值一定不会考虑它
else ans=Max(ans,now);
}
else//它的父亲是先手,和上面讨论的一致
{
if(now<=beta) return beta;
else ans=Min(ans,now);
}
}
if(!flag)//移动不了,即到了叶子(输),需要估价
{
if(player==1) return -INF+step;//step越大先手活的越久,但总体上还是自己输了
else return INF-step;//step越大对于先手越坏,但总体上还是对面赢了
}
return ans;
}
int main()
{
freopen("h.in","r",stdin);
freopen("h.out","w",stdout);
read(n);read(m);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
{
read(a[i][j]);
if(a[i][j]==1) sx[0]=i,sy[0]=j;
if(a[i][j]==2) sx[1]=i,sy[1]=j;
}
int ans=dfs(1,1,INF,-INF,sx[0],sy[0],sx[1],sy[1]);
if(ans>0) printf("1 %d\n",INF-ans);//最后到达的是2号对应的叶子,减掉INF还原
else printf("2 %d\n",ans+INF);//最后到达的是1号对应的叶子,加上INF(这个见上面dfs中估价函数的计算)
return 0;
}
P.S. 笔者小白一枚,如有错误还请指出