从一个策略游戏谈搜索算法优化Word格式.docx

上传人:b****4 文档编号:18417740 上传时间:2022-12-16 格式:DOCX 页数:23 大小:23.79KB
下载 相关 举报
从一个策略游戏谈搜索算法优化Word格式.docx_第1页
第1页 / 共23页
从一个策略游戏谈搜索算法优化Word格式.docx_第2页
第2页 / 共23页
从一个策略游戏谈搜索算法优化Word格式.docx_第3页
第3页 / 共23页
从一个策略游戏谈搜索算法优化Word格式.docx_第4页
第4页 / 共23页
从一个策略游戏谈搜索算法优化Word格式.docx_第5页
第5页 / 共23页
点击查看更多>>
下载资源
资源描述

从一个策略游戏谈搜索算法优化Word格式.docx

《从一个策略游戏谈搜索算法优化Word格式.docx》由会员分享,可在线阅读,更多相关《从一个策略游戏谈搜索算法优化Word格式.docx(23页珍藏版)》请在冰豆网上搜索。

从一个策略游戏谈搜索算法优化Word格式.docx

你的程序必须判断,对一个初始的游戏状态,谁将获胜或者会否出现平局.

你的程序将处理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

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育 > 语文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1