从一个策略游戏谈搜索算法优化Word格式.docx
《从一个策略游戏谈搜索算法优化Word格式.docx》由会员分享,可在线阅读,更多相关《从一个策略游戏谈搜索算法优化Word格式.docx(23页珍藏版)》请在冰豆网上搜索。
你的程序必须判断,对一个初始的游戏状态,谁将获胜或者会否出现平局.
你的程序将处理G(1=G=1000)个游戏状态.
该问题要求使用不超过1.00MB的内存.
问题名:
twofour
输入格式:
*行1:
用空格隔开的两个整数:
N和G.
*行2.G+1:
每行包含空格隔开的N个整数用于描述该游戏.
第一个整数是堆1的球数,
第二个整数是堆2的球数,
.
行2描述了游戏1,行3描述了游戏2,
你的程序应该计算每个特定游戏的胜者.
输入样例(文件twofour.in):
54
0341222222
1122443210
输出格式:
*行1.G:
每个游戏的结果.
行1给出游戏1的结果,.
结果是一个整数:
1代表第一个游戏者获胜,2代表第二个获胜,
以及0代表平局.
输出样例(文件twofour.out):
12
11
二.问题的初步分析和第一种解答
从问题的描述来看,我们可以得到如下的一些基本信息:
1)player1总是先手的,问题也是求player1的胜负情况;
player2在瓜分最初的'
2'
堆的时候具有优先权。
2)整个的游戏过程,就是把不均有分布的堆,逐步均匀化,直至所有的堆都是'
堆,由于2N个球,放置在N堆上,这是个必然。
3)'
堆在游戏过程中会变化成'
1'
堆或者'
3'
堆;
每堆的球数不超过4,允许'
0'
堆。
4)如果两堆之间的球数目差1,就可以进行球的移动,这是我们在程序中判断可行步的依据。
在上面的基本信息的基础上,我们就要决定用什么搜索算法,用广度优先搜索还是深度优先搜索呢?
如果用广度优先搜索的话,一般的方法是从根节点出发,平行的推进所有从根节点衍生出来的子节点,每个节点都要保存当前棋局的状态,同时还要记住父节点的索引;
子节点需要父节点的信息来进行衍生,而父节点需要子节点的结果来决定本节点的结果。
一般我们用一个队列来实现这个过程;
由于所有的节点都需要保存,一些废节点也被保存了,而对于奇偶深度的节点的处理还不一样(奇偶深度,对应player1还是player2),下面会具体讲到,节点所要保存和处理的信息都比较复杂,另外针对这个问题的广度优先搜索的剪枝工作也是个问题,因为广搜的处理方式是先把子节点全部入队列,然后从队头逐个再扩展,一些无用的节点就占用了宝贵的队列空间。
比如player1的任何一步移动,只要能胜利,其它方式的移动都不需要考虑了;
然而对于走法的判断是在所有子节点入队之后,所以其他废节点就占用了多余的空间。
综合上面的讨论,我认为本问题不太适合使用基于广度优先搜索的算法。
下面考虑深度优先搜索。
深度优先搜索有一个好处就是占用的空间少。
先讲讲我们的思路,请大家注意对于player1和player2的处理的不同。
1)我们对所有棋局的考虑都以player1为中心,不论当前棋局是由player1的移动还是player2的移动造成的.
2)如果相对与当前棋局,对于player1的所有移动中,有一种能导致player1胜利,则当前节点就能导致player1胜利,也就是不需要再扩展了,直接返回结果到上层父节点;
如果对于player1的所有移动,全都导致player1失败,才能说本节点导致player1失败;
如果没有走法能胜利,但有一种走法能使得player1和局,那么player1会选择和局。
3)同样的道理,如果相对与当前棋局,对于player2的所有移动中,有一种能导致player2胜利,则当前节点就能导致player1失败,也就是不需要再扩展了,直接返回结果到上层父节点;
如果对于player2的所有移动,全都导致player2失败,才能说本节点导致player1胜利;
如果没有走法能使得player2胜利,但有一种走法能使得player2和局,那么player2会选择和局,也就是导致player1和局。
4)由于player1先手,从初始棋局出发,逐个扩展出子节点,并递规的调用自身来扩展子节点,直到无法再深度扩展为止。
下面来看我们的第一个版本的解答。
#defineQMAX30typedefstruct{
intqn;
//球的个数
intowner;
//所有者,0:
没有所有者,1:
属于player1,-1:
属于player2
}sq_str;
//上面的数据结构用来描述'
堆'
的属性。
sq_stra[QMAX];
//用来描述棋局状态
intn;
//记录堆的个数,全局变量
intdepthmax=-1;
//用来记录最深的递规深度,以便了解堆栈内存的使用
使用下面的代码得到输入,当然后面我们会讨论随机生成棋局的方式,初始的棋局在这里就准备好
voidtest1()
{
inti,j;
intG,c;
scanf("
%d%d"
&
n,&
G);
for(i=0;
iG;
i++)
c=0;
for(j=0;
jn;
j++)
%d"
a[j].qn);
if(a[j].qn==2)
if((c&
1)==0)a[j].owner=-1;
//由player2所有
elsea[j].owner=1;
//由player1所有
c++;
}
elsea[j].owner=0;
//无人所有
depthmax=-1;
c=qiuv0(0);
printf("
result:
%d.depthmax=%d.\n"
c,depthmax);
//求解棋局的函数,胜负是指player1的胜负。
depth表示当前迭代的深度,同时也表示当前执子的玩家,depth为偶数,表示player1执子,depth为奇数,表示player2执子。
//返回1:
胜利,-1:
失败,0:
和局
intqiuv0(intdepth)
inti,j,k=0,s,tx=0;
intret0;
intfu=0;
//会导致player1失败的节点的个数
intshen=0;
//会导致player1胜利的节点的个数
sq_strsavei,savej;
if(depthdepthmax)depthmax=depth;
in;
if(a[i].qn-a[j].qn1)//判断可行移动步的标准,从第i堆到第j堆
k++;
//记录子节点的个数
//保留被修改的堆,方便以后恢复
savei=a[i];
savej=a[j];
a[i].qn--;
a[j].qn++;
if(a[i].owner)a[i].owner=0;
if(a[j].owner)a[j].owner=0;
if(a[i].qn==2)
if((depth&
1)==0)a[i].owner=1;
elsea[i].owner=-1;
1)==0)a[j].owner=1;
elsea[j].owner=-1;
ret0=qiu(depth+1);
//递规调用
//恢复被修改的棋局
a[i]=savei;
a[j]=savej;
//请大家注意下面的不同处理,所有的胜负都是对于player1来说的
1)==0)//player1的局势
if(ret0==1)return1;
elseif(ret0==-1)fu++;
else//player2的局势
if(ret0==-1)return-1;
elseif(ret0==1)shen++;
if(k==0)//如果没有子节点可以扩展,那么这是个终局
k+=a[i].owner;
if(k==0)return0;
if(k0)return1;
if(k0)return-1;
else//否则是扩展了子节点的
1)==0)
if(fu==k)return-1;
//所有的走法都导致输
elsereturn0;
//至少有一种走发可以保证平局
else
if(shen==k)return1;
//player2所有的走法都导致player1胜利
//player2会选择平局
三.初步的优化和剪枝
好了程序很简单,应该很容易理解,但是效率也很低!
下面我们来逐步分析优化,看看问题在哪里。
首先我们发现,同层扩展出的各棋局的状态只和各堆的状态有关,而和各堆的排列没有关系,所以在同一层我们就可以进行剪枝,把那些本质上是一样的子节点剪掉。
考虑所有的走法一共有8种:
4-2(-1,1),4-1,4-0,3-1,3-0,2(-1,1)-0,括号里的数表示'
堆的归属情况。
新的程序如下:
intqiuv1(intdepth)
sq_strtt[8][2];
//一共有8种不同的移动方式
intre[8];
//8种移动方式带来的结果
intti=0;
if(a[i].qn-a[j].qn1)
for(s=0;
sti;
s++)
if((a[i].owner==tt[s][0].owner)&
&
(a[i].qn==tt[s][0].qn)&
(a[j].owner==tt[s][1].owner)&
(a[j].qn==tt[s][1].qn))
break;
if(s!
=ti)
ret0=re[s];
continue;
tt[ti][0]=a[i];
tt[ti][1]=a[j];
re[ti]=ret0;
ti++;
if(k==0)
四、进一步的优化和剪枝
经过上面的优化,程序的性能有所提升,但是还不够快,问题出在哪里呢?
我们知道深度优先搜索的一大缺点就是对已有的计算结果没有继承,造成了大量的计算浪费;
qiuv1已经做了一点工作,但还不够;
我们需要记录所有的已经访问过的棋局和相应的结果,方便在后续的搜索中进行剪枝。
这就需要对以往结果的记录。
定义如下数据结构:
#defineSTAMAX11000unsignedcharx_sta[STAMAX][8];
//player1的2就在[2],player2的2在[5],局势的结果放在[7],depth的奇偶性放在[6]里
intx_inx=0;
//x_sta中的下一个空位置
我们知道棋局只和堆本身有关,而和堆的排列顺序无关,进一步,两个相同的堆也是无区别的,这样我们只需要记录一个棋局中'
堆的个数,'
堆的个数,'
2
(1)'
2(-1)'
4'
堆的个数,以及当前棋局对应的执子方,这种棋局的结果。
由于最多也就30堆,所以1个字节足以存放相关的信息。
算算上面的空间才不到90KB,离1MB尚远。
还有一个问题,那就是棋局的对偶性,棋局A的对偶棋局,就是把depth的奇偶性取反,把相应的'
堆的归属性取反,相应的棋局结果自然取反。
这个很容易理解,相当与player2和player1交换位置,下对方的棋。
这样我们每求得一个棋局,实际上是得到了两个棋局的结果。
好了还是看看新程序吧.
intqiuv2(intdepth)
intold_xinx;
unsignedcharx[7];
memset(x,0,7);
sn;
if(a[s].qn!
=2)
x[a[s].qn]++;
if(a[s].owner==1)x[2]++;
elsex[5]++;
x[6]=depth&
1;
//开始搜索以前保存的结果
sx_inx;
if(memcmp(x,x_sta[s],7)==0)break;
=x_inx)//找到了!
ret0=(char)x_sta[s][7];
memcpy(x_sta[x_inx],x,7);
//每找到,把当前棋局添加进去
old_xinx=x_inx;
//记录自己的位置,后面好吧结果填进去,因为下面的递规会加入新的内容到存储队列
x_inx++;
ret0=qiuv1(depth+1);
x_sta[old_xinx][7]=ret0;
//把对偶的情况放进去
memcpy(x_sta[x_inx],x_sta[old_xinx],8);
x_sta[x_inx][2]=x_sta[old_xinx][5];
x_sta[x_inx][5]=x_sta[old_xinx][2];
x_sta[x_inx][6]=!
x_sta[old_xinx][6];
x_sta[x_inx][7]=-x_sta[old_xinx][7];
好了,这下我们的程序有了质的飞跃,速度提高很多,比v1版本提高了几个数量级!
很开心^-^。
五.HASH函数的应用
但是当我们加大测试强度,把堆的数量推到顶,达到30之后,我们的程序还是不够快,对有的棋局,几乎半分钟内都解不出来,这是不能接受的。
那好吧,看来我们还需要进一步优化。
还有什么地方值得优化呢?
就是下面这个地方!
通过测试发现,对于30堆的情况,我们的存储会有超过10000的情况,一般也会超过5000!
对于这么大的存储空间,如果向上面那样每次都进行线性匹配,效率是很低下的,这也就是为什么在堆变多之后,我们的程序变慢的原因。
好了问题找到了,那么如何解决呢?
还是有办法的,那就是hash表!
通过hash表就可以一步定位到我们需要的位置,只要hash函数设置的好,就可以尽量减少碰撞的产生;
即使有了碰撞也不怕,搞个拉链链接起来就可以了,经过测试新的程序,使用hash表,最多也就产生两三次碰撞,一般都能一击中的!
#defineSTAMAX0x4000typedefstruct{
unsignedcharx_sta[8];
unsignedshortpr;
//低14bit是索引,高2bit:
00(空项目),01(低14bit表示下一个的位置),10(没有下一个了)
}X_sta;
//hash表
X_stahash[STAMAX];
//占用160KB空间,实际测试,可以处理的堆数达35堆的情况
//返回14bit的HASH值,我自己构造的hash函数
unsignedshortcal_hash(unsignedchar*in,intlen)
unsignedshortx1=0;
inti;
ilen;
x1=((x1*11)+in[i])^0x3d;
return(x1&
0x3fff);
intqiuv3(intdepth)
unsignedcharx[8];
//把结果也含进来了
unsignedshorts_indx;
intnexti;
intfind;
memset(x,0,8);
elsea[i