[CSP校内集训]贪吃蛇(阿尔法-贝塔剪枝)

题目

有两条蛇(1号蛇和2号蛇)在n行m列的地图上,地图上有障碍物。一条蛇碰到蛇身/障碍物/边界就会死。蛇身会不断长长——可以理解为蛇尾位置不会变,蛇只会向前伸展不会缩尾巴。两条蛇都绝顶聪明,如果自己能赢,一定会尽量快地赢;如果自己会输,一定会死得尽量晚。给出初始局面,两蛇轮流走,每次可以且必须向上下左右移动一格。1号蛇先走,请告诉我谁会在多少回合时赢。((n,mleq 20))(0)的数量不超过(50)


(alpha - eta)剪枝

AlphaBeta剪枝算法是一个搜索算法,旨在减少在其搜索树中,被极大极小算法评估的节点数。
这是一个常用人机游戏对抗的搜索算法。它的基本思想是根据上一层已经得到的当前最优结果,决定目前的搜索是否要继续下去——百度百科

前提:双方绝顶聪明,各自为营

用我的话来说,这个剪枝方法是这样的一个结构:

  1. 所有的估价都是由同一个角度来看待的,不能说在1的回合说对1有利,在2的回合又说对2有利(应该说对1有害)

  2. 搜索树上先手通过一条边转移成后手

  3. 搜索树的叶子节点是估价函数,评估到达的这个状态的好坏,函数值越大对先手越有利,父亲节点的权值均由儿子转移来

  4. 奇偶性不同的层求的东西不一样,即两个人的策略不同,先手求的是所有儿子中最大的一个(即对先手最有利),后手求的是最小的一个(对先手最有害)

总而言之,就是搜索树的奇数层节点取(max),偶数层取(min),叶子节点需要自己估价(根据具体的题目)

扯了半天,如何剪枝?很简单,比如对于先手节点(u),它会取最大的儿子,假设当前权值为(ans_u),如果当前节点的儿子(v)的一个儿子(vv)权值小于(ans_u),就不用再遍历这个儿子(v)剩下的儿子了(因为儿子(v)的权值取(min){孙子}),这个儿子(v)的权值到目前为止可以判断一定小于(ans_u)了;后手相反(可能会有点抽象,建议画图理解)


本题思路

本题显然可以用(alpha - eta)剪枝(双方绝顶聪明且操纵的蛇不一样)

主要是估价函数的设计:如果先手无路可走,就是绝对有害,设为(-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
",INF-ans);//最后到达的是2号对应的叶子,减掉INF还原 
	else printf("2 %d
",ans+INF);//最后到达的是1号对应的叶子,加上INF(这个见上面dfs中估价函数的计算) 
	return 0;
}

(learning from)博客1博客2

P.S. 笔者小白一枚,如有错误还请指出

原文地址:https://www.cnblogs.com/Chtholly/p/11650808.html