您的位置:首页 > 编程语言

英雄会第一届在线编程大赛:单词博弈 (解题思路) ---miss若尘

2013-12-08 15:48 369 查看
代码链接:单词博弈 C++代码---miss若尘

闲话不多说,直接上原题:

题目详情

甲乙两个人用一个英语单词玩游戏。两个人轮流进行,每个人每次从中删掉任意一个字母,如果剩余的字母序列是严格单调递增的(按字典序a < b < c <....<z),则这个人胜利。两个人都足够聪明(即如果有赢的方案,都不会选输的方案 ),甲先开始,问他能赢么?

输入: 一连串英文小写字母,长度不超过15,保证最开始的状态不是一个严格单增的序列。

输出:1表示甲可以赢,0表示甲不能赢。

例如: 输入 bad, 则甲可以删掉b或者a,剩余的是ad或者bd,他就赢了,输出1。

又如: 输入 aaa, 则甲只能删掉1个a,乙删掉一个a,剩余1个a,乙获胜,输出0。

函数头部:

C:int who (const char * word);

C++:int who (string word);

Java:public static int who(String in);

C# :public static  int who(string word);

1、读题。

嘀嗒……嘀嗒……嘀嗒……嗯……读完了吗?读完了吧!那明白题意吗?没事,明白不明白我都得写一下(>_<|||)。

冒号后面是内心的独白。。。。

首先,题目只输入小写字母:多方便呀,不用转大小写了>_<;

其次,轮流进行:好像下棋呀,人生可不是一人一步,不像下棋,唉,说多了都是泪……>_<;

再其次,每人每次从中删掉任意一个字母:好自由,想删哪个删哪个;

再其次,如果剩余的字母序列是严格单调递增的,则这个人胜利:删完后判断输赢呀,注意,是删完后;

再其次,两个人都足够聪明:足够聪明……水好深的样纸……既然足够聪明,为什么还要玩呀,明知道看一眼就知道是输还是赢……这游戏自己和自己玩挺好的。。。;

再其次,输入 一连串英文小写字母,长度不超过15:是不是超15了就太长不好弄呀,求官方解释>_<;

最后,保证最开始的状态不是一个严格单增的序列:废话嘛,开始就是严格单增,表示不知道啥意思,算谁赢?乙?(因为甲先开始嘛,甲还没开始就是严格单增了,可不可以看成是乙“删”过了>_<);

最最后>_<,输出 1表示甲可以赢,0表示甲不能赢:输出不是0就是1,看样子某一字符串只有两种状态呀,0或1。

2、读样例。

例如: 输入 bad, 则甲可以删掉b或者a,剩余的是ad或者bd,他就赢了,输出1。

又如: 输入 aaa, 则甲只能删掉1个a,乙删掉一个a,剩余1个a,乙获胜,输出0。

问①,甲可以删'b','a','d'三个中的任意一个,但是他会选择删b或者a(因为他足够聪明,删d剩两个乙再删一个自己不就输了吗>_<),(有点小白的感脚。。。);

问②,原来单独一个字母的“字符串” 是算在严格单调递增里的呀。

3、继续读。。。

C,C++,Java,C#,都行呀,倒挺开放,原来只是完成一个函数功能就行(后台系统会调用自己写的这个函数的,所以main函数神马的写了人家也不用,人家会写……),那我用C++吧,表示Java没自学多少呢……C#太高端,更是Hello World都没写过(不过什么语言不是重点吧,关键是能解决问题……)。

4、分析,开始干。

原来这是一个博弈游戏,不是我赢,就是你输(好像是同一个结果,请无视低智商孩纸的言语。。。)的关系。

那定义如下,

若某字符串对应结果 1,称其为 必胜状态;

若某字符串对应结果 0,称其为 必败状态;(博弈中必胜必败是用N和P表示的,欲知更多请自行Search。。。本篇文章足以搞定这道题……)

足够聪明,这么深的水咋想呀,好吧,咱试着玩一玩。给我一个长度不超15的字符串,且长度不小于2(因为前面说了1个字母算严格单调递增的序列,题目保证了开始给的不是严格单调递增的序列)。问,我删哪个呀?……表示没思路 >_< ,好吧,我试着删第一个,看剩下的是不是严格单调递增的,如果是我就删第一个了,删完游戏结束,我赢,哈哈~~~;如果不是呢。。。我再试着删第二个,看剩下的是不是严格单调递增的序列;……;我试着删最后一个,剩下的还是不严格单调递增的序列, Oh,天哪,这是要闹哪样,玩个游戏累不累,能不能一起快乐地玩耍了。。。没事!我还有办法!!既然删一个结束不了游戏,那我删某一个剩下的序列是不是
必败的呢,如果是那我就删那一个了,删了后对于对方来讲他爱删哪个删哪个都是他输,哇咔咔,爽!!!呃……万一删任意一个剩下的序列都不是必败的状态,好吧,因为状态只有两种:必败或必胜。如果剩的都不是必败,那对方就是必胜了,那我输了,55555,求安慰~~~~

小结一下,这还是没有思路呀,明显是递归的办法,栈太深脑容量太小装不下呀,我…我……,好吧,给大家整理一下思路,抓出重点,如下:

int who(string word)
{
if(长度为2)
return 1;//呃……赢了,你真菜!哈哈
if(长度为3)
//如果是非增序列,哇,输了,return 0;

//一个一个删,看剩的是不是严格单调递增
for(;如果没试完;准备删下一个)
{
//剩的序列是不是严格单调递增的呢
//如果是,啊,break;
//不是呀,继续试下一个去。。。
}

//循环完了?
//是break的吗,啊,是,好吧,我赢了,return 1;

//不是break出来的呀,是循环到头了呀?好吧,下面继续试
for(;如果没试完;准备删下一个)
{
//剩的序列是不是必败的呢,呃,不舍得用递归,费时间,555
//if( who(我删掉某一个字母剩的序列) 等于 0)啊,好,我赢了,直接return 1;
}

//哇,试完啦?居然试完了,5555,没办法了,我输了,return 0;
}


好吧,我解释一下为什么有个长度为3的要判断是不是非增序列呢:

我枚举了三个字母的所有情况,如下:

abc,acb,bac,bca,cab,cba,aab,aba,baa,abb,bab,bba,aaa。其他情况可以归结为这13类中的一种,因为字母只能是三个都不一样;或者有两个一样,这两个一样的比较大或者比较小;或者三个字母都一样。

我发现上述13种情况只有 cba,baa,bba,aaa。是必败序列,其他的序列先手都有办法必赢。这4个序列先手删哪个最后都会输。伪代码中的“非增”的含义,定义如下(学数学吧孩纸们,学了你就会定义各种东西……):一个数列a
,若对任意的i(要有意义),有a[i]>=a[i+1],则称数列是非增的。>_<

(其实长度为4的字符串我也枚举了一下,有75种情况,必败序列没找出共性>_<,逆序数等概念不起啥作用,DFS神马的也想过,感觉还是具体问题具体分析的好。。。)

咱继续聊,上述思路已经能解决问题了,可是,但是,哇咔咔,说出来全是泪,为什么长度为12,13神马的一个字符串时间还是零点零几秒,怎么长度到15了时间就长到66秒了呢……如图:(图的背后全是泪。。。)



再仔细想想吧,肯定是递归引起的,表示这个多叉树也太深了,难画呀。
好吧,我试着用语言描述一下,假设开始的长度为 6,咱们给删掉第 i 的字母剩的 长度为5的子列 标号哈,分别为①②③④⑤⑥,在我想知道 who ( ①)的时候要知道再短一个字母的子列的状态值(0或1),继续递归下去,当我递归算出  who ( ① ) 的值的时候,(我以长度为4的说明 )我已经至少知道了一部分长度为4的 字符串对应的状态值;但是当我再想递归 who(②) 的时候,我可能又算了一遍 这些长度更短的子列的状态值(0或1),那这样递归下去,数量级就是
n!了,而15!很大呀,必然费时间呀。

解决办法,再仔细考虑,不管我删哪一个,最多也就删14次呗,换种角度看问题,15个字母中,每一个字母最后不是没了就是留下来的,删或不删,两个状态,15位,2^15个子列,况且我不用删到最后一个,显然比2^15个数还要少,而 2^15 是远小于 15!的;另一种角度,是同样的,从组合的角度来看,长度为k的子列有C(15,k),数学上加起来最多也是2^15这个数,结论一致,再次说明2^15这个数是正确的。
这就好办啦,因为后面的递归可能需要前面算过的这些子列字符串及其对应的状态值的,那我可以把已经算出来的子列及其状态值一并保存起来,等在后面递归的时候,先去保存的数据里面找,如果已经有了,直接给出相应的状态值就行了,如果没有再递归,这样就省时间了,不用每次不管用过没用过都递归,真傻,哈哈。

好了,有思路了,基本和上面的过程是一样的,再贴一次:

int who_win(string word,map<string, int>& m)
{
if(/*长度为2*/)
return 1;
if(/*长度为3*/)
{
//如果是非增序列,return 0;
//如果不是非增序列,return 1;
}

//一个一个删,看剩的是不是严格单调递增
for(;/*如果没试完*/;/*准备删下一个*/)
{
//剩的序列是不是严格单调递增的呢
//如果是,啊,break;
//不是呀,继续试下一个去。。。
}

//是break出来的,return 1;

//不是break出来的
for(;/*如果没试完*/;/*准备删下一个*/)
{
//去保存的数据里找,如果有,给出对应的状态值
//判断 给出的值是不是为 0,如果是,return 1;

//如果容器里没有该子列的记录,递归,调用 who_win(删掉一个字母的子列,m);
//insert 这个子列与其状态值:
//判断 其状态值是不是为 0,如果是,return 1;
}

//insert word和0 到容器里去
return 0;
}


(第一个思路伪代码中有一句漏写……请参考第二个。。。不想改了>_<。)

知道你肯定有问题,哈哈,问吧,为什么函数名字改了,而且参数也变了,是的,因为要保存了,不能在递归里建立容器,在递归函数里建立容器每次递归调用都建立新的容器,原来的就没了吧,覆盖掉了?不能这么玩,所以只能在递归函数的外面建立容器,因为人家要咱完成who函数,所以稍微改成不同的名字>_<。
然后who函数里建立容器,直接return who_win(word, m);  大功告成!编译运行,怎么感觉要是10组数据会超3秒呀,555,后来找原因,是miss若尘的小本本性能不给力,提交到人家的OJ系统里,时间就少了很多,参考图如下:
                              

 
                                       


此办法经测试,提交通过!!恭喜,哈哈~~

PS:关于算法性能,有人建议在每一个return前插入当前子列与要return的值,并且在who_win函数体最开始部分就加上查找容器里是否有当前序列记录,如果有直接返回。但是经实践发现时间并没有减少,反而增加了一点点,考虑到每次调用都要查找,并且每一个return都要插入保存的话,那么容器里元素就会很多,虽然达不到2^15,但是频繁的插入与查找也会增加时间吧,就算不是瓶颈但频繁调用是不是减少性能也不确定吧,毕竟map的插入查找神马的不是自己实现的,不了解其本质呀。所以miss若尘大胆决定:①
递归函数最前面不加查找,避免每次调用都要去那么多的元素里查找;② 前面几个比较容易的return,不插入,因为至多递归调用一次压一次栈就能弹出返回值,并且那个循环最多也就十几次,相比用别人写的库的插入函数,感觉循环这么几次应该更省时间。③ 关于后面的插入,因为序列的状态是较难得到的,故插入以备后用。④另外,如果在for循环体内最前加入判断当前字母是否与前一个字母重复,若重复则continue,则可对baaaaac这类字符串中多次循环删a有优化,效果要根据实际字符串来判断是否明显了。

接下来呢……直接上完整代码?大忌,代码贴到另一个文章中去,链接:单词博弈 C++代码---miss若尘
转载请注明出处,原创打字很累的哦~~~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息