2.石头游戏(坑爹)

题目抽象:

一堆石子两个人轮流取,每次至少要取走一个。先取的人第一次可以取任意多个,但是不能全部取完。之后每个人取石子时,能取的数目最多不能超过对手刚才取的石子数的k倍(k为给定常量)。取走最后一个石子的人算赢。

在两个人都以最优策略进行游戏时,先手要么必胜要么必负,必胜还是必负取决于一开始有多少个石子,本题就是告诉你开始的石子数目,你需要判断先手必胜还是先手必负,然后对先手必胜的情况,要算出先手一方第一次至少需要取多少颗石子。

解题思路:

首先根据提示我们可以知道先手负的局面都是一些特殊局面,并且数量远少于先手胜的局面,因此分析主要针对先手负的局面进行,提示中的信息给了一个很好的入手点,我们先对k=1时的性质进行分析:

先手负的初始石子数序列为{1,2,4,8,...},通过观察规模比较小的数据不难发现,若初始石子数n不是2的幂,则取石子数最少的必胜走法恰是n用二进制表示时最后一个1所代表的数值,比如n用二进制数表示是11011010时,必胜取法为2(10)。先手取走这“最后一个1”之后,由于k=1,后手取的石子数至多和先手一样多,所以后手无法取走剩下的石子中的“最后一个1”,假如我们能证明后手取完之后先手仍然能取走剩下石子中的“最后一个1”,那么我们就证明了只要一直取走“最后一个1”,先手就能够保证取胜。事实上对于2^p以及任意一个满足x<2^p的正整数x,2^p-x用二进制表示时的“最后一个1”代表的数值必然不大于x,于是,只要有一方无法取完当前石子数的“最后一个1”,另一方就一定可以取走剩下石子数的“最后一个1”,因此,在初始石子为2的幂时,先手无法取走“最后一个1”(第一次无法取走全部石子),胜利就属于对方啦,而只要初始石子数不是2的幂,先手就能保证胜利。

然后分析k=2时的情形:

先手负的初始石子数序列为{1,2,3,5,8,13,21,...},正好是Fibonacci数列,而观察小规模数据可以发现,先手取胜时取走的最少石子数也是Fibonacci数列中的数。对于Fibonacci数列,有以下性质:

性质1:任何一个正整数都可以分解为Fibonacci数列中一些不相邻的数的和,且分解方法唯一。且这些数{fk1,fk2,...fkt}满足2fk1<fk2,2fk2<fk3,...2fk(t-1)<fkt。

例如:前1个数有1种取法,即:{1}

           前2个数有2种取法,即:{1},{2}

           前3个数有4种取法,即:{1},{2},{3},{1,3}

           前4个数有7种取法,即:{1},{2},{3},{1,3},{5},{5,1},{5,2}

           前n个数的取法为:<前n-1个数的所有取法>,{fn},{fn+<前n-2个数的所有取法>}

性质2:k=2时,如果初始石子数n不是Fibonacci数,则可以把n分解为一些不相邻的Fibonacci数的和,在该分解方案里的最小的数,即是取石子数最少的获胜方法。

#include<stdio.h>

#define MAXM 1000000

int n,k;

int a[MAXM+1];//记录Fibonacci数列的前几个数

int r[MAXM+1];//记录上述前几个数的至多取法

 

void Solve()//优秀的男人总是坚持到了最关键的一个函数

{

int i,j;

a[0]=0;

r[0]=0;

for(i=1,j=0;i<MAXM;i++)//首先由于a数组记录的是Fibonacci数列中的数值,所以初始下标应该为1,而这里j为0的原因一是由于后期有循环函数可以使得其变为i-2的情况,因为我们发现r(n)=r(n-1)+1+r(n-2),所以这里的j应该满足i-2,但是一开始在i=1,2时j不存在,所以一开始设置为0,不影响i=1,2这两种情况

{

a[i]=r[i-1]+1;//这里也是直接观察前面的性质1得到的

while(j+1<i&&a[j+1]*k<a[i])//这里是一个很厉害的设计,如果只满足j+1<i的话,那么在a[2]时,j就会变为1,后面进行r[i]=a[i]+r[j]就会出错,所以这里又要求a[j+1]*k<a[i],这样就能避免i=1,2时j已经发生改变。其实也不用那么麻烦要限制两个条件,直接用a[j+1]*k<a[i]就可以了

{

j++;//使j变为i-2

}

r[i]=a[i]+r[j];//这里也是由性质一的来推导出来,其实就是r(n)=r(n-1)+1+r(n-2)

if(a[i]>=n)  break;//对a数组的要求只要达到Fibonacci数列中的某一个数就行,这个数必须大于或者等于此时石子数

}

if(i>MAXM)//避免石子数太多,这样就不好玩了

{

printf("un solvable\n");//就是吊,老子不玩了

return;

}

if (a[i]=n)//如果石子数刚好满足Fibonacci数列中的数的话,那没办法了,先手必输

{

printf("lose\n");

return;

}

for(;i>=1;i--)//进行到这一步时就说明此时的a[i]>n的,也就是先手必胜,所以按从大到小的顺序从n中减去Fibonacci数列中<n的数直到出现结果与Fibonacci数列中的数相等,就输出此时的n值,也就是先手胜时第一手应该取出多少石子

{

if(n==a[i])//这就是最后一步,通过下面的相减,得到结果与Fibonacci数列中的数相等,也就是先手胜时第一手应该取出多少石子

{

printf("%d\n",n);

return;

}

else if (n>a[i])//这里分别减去Fibonacci数列中小于n的值,直到后期满足n==a[i]

{

n-=a[i];

}

}

printf("logic error\n");//来自一种神秘的意外,莫名其妙的错误,编译器的错

}

 

int main()

{

int ca,cc=0;

scanf("%d",&ca);//总共进行多少次实验

while(ca-->0)

{

scanf("%d %d",&n,&k);//其中n表示石子数,k表示题目中给定常量

printf("Case %d:",++cc);

Solve();//这里进行先手必胜或者必负的判断,首先判断n是否在Fibonacci数列中,在的话,就先手必负,输出lose,否则,按从大到小的顺序从n中减去Fibonacci数列中<n的数直到出现结果与Fibonacci数列中的数相等,就输出此时的n值,也就是先手胜时第一手应该取出多少石子

}

return 0;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值