zoukankan      html  css  js  c++  java
  • hdu 1847 Good Luck in CET-4 Everybody!(入门SG值)

    Problem Description

    大学英语四级考试就要来临了,你是不是在紧张的复习?也许紧张得连短学期的ACM都没工夫练习了,反正我知道的Kiki和Cici都是如此。当然,作为在考场浸润了十几载的当代大学生,Kiki和Cici更懂得考前的放松,所谓“张弛有道”就是这个意思。这不,Kiki和Cici在每天晚上休息之前都要玩一会儿扑克牌以放松神经。“升级”?“双扣”?“红五”?还是“斗地主”?当然都不是!那多俗啊~
    作为计算机学院的学生,Kiki和Cici打牌的时候可没忘记专业,她们打牌的规则是这样的:
    1、  总共n张牌;
    2、  双方轮流抓牌;
    3、  每人每次抓牌的个数只能是2的幂次(即:1,2,4,8,16…)
    4、  抓完牌,胜负结果也出来了:最后抓完牌的人为胜者;
    假设Kiki和Cici都是足够聪明(其实不用假设,哪有不聪明的学生~),并且每次都是Kiki先抓牌,请问谁能赢呢?
    当然,打牌无论谁赢都问题不大,重要的是马上到来的CET-4能有好的状态。
    Good luck in CET-4 everybody!

    Input

    输入数据包含多个测试用例,每个测试用例占一行,包含一个整数n(1<=n<=1000)。

    Output

    如果Kiki能赢的话,请输出“Kiki”,否则请输出“Cici”,每个实例的输出占一行。

    Sample Input

    1
    3

    Sample Output

    Kiki
    Cici
    解题思路:找找规律,先举几个栗子:
    当n=1时,先手必赢;
    当n=2时,先手必赢;
    当n=3时,无论先手抓多少张牌,后手必赢;
    当n=4时,只要先手抓1张牌,接下来就转化成n=3这个局面,即先手必赢;
    当n=5时,只要先手抓2张牌,接下来就转化成n=3这个局面,即先手必赢;
    当n=6时,①当先手抓1张牌时,接下来就转化成n=5这个局面,即后手必赢;②当先手抓2张牌时,后手可以一次性抓走剩下的4张牌,即后手必赢;③当先手抓4张牌时,后手同样可以一次性取完剩下的2张牌,即后手必赢;所以无论先手抓多少张牌,后手必赢;
    当n=7时,只要先手抓走1张牌,接下来就转化成n=6这个局,即先手必赢;
    ......
    再多举几个栗子,我们可以发现只要n是3的倍数,则后手必赢;反之,先手必赢,因此可以用以下简单代码水过:
     1 #include<bits/stdc++.h>
     2 using namespace std;
     3 int main()
     4 {
     5     int n;
     6     while(cin>>n){
     7         if(n%3)cout<<"Kiki"<<endl;//不是3的倍数,先手必赢
     8         else cout<<"Cici"<<endl;//是3的倍数,后手必赢
     9     }
    10     return 0;
    11 }

    这题还可以用SG值解决,所谓的SG值就是记录当前状态是N是P的具体值,N-position表示必赢状态(其SG值不为0),P-position表示必输状态(其SG值为0)。下面介绍怎么求SG值:首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示不属于mex这个集合的最小非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

    对于一个给定的有向无环图,定义关于图的每个顶点的Sprague-Grundy函数g如下:g(n)=mex{ g(m) | m是n的后继 },这里的g(n)即sg[n]

    拿本题的栗子来讲:首先有sg[0]=0,f[]={1,2,4...};(f数组存放可以抓走扑克牌的张数,并且按升序存放)

    当n=1时,先手可以抓走1-f{1}张牌,剩余{0}张,mex{sg[0]}={0},故sg[1]=1;

    当n=2时,先手可以抓走2-f{1,2}张牌,剩余{1,0}张,mex{sg[1],sg[0]}={1,0},故sg[2]=2

    当n=3时,先手可以抓走3-f{1,2}张牌,剩余{2,1}张,mex{sg[2],sg[1]}={2,1},故sg[3]=0;

    当n=4时,先手可以抓走4-f{1,2,4}张牌,剩余{3,2,0}张,mex{sg[3],sg[2],sg[0]}={0,2,0},故sg[4]=1;

    当n=5时,先手可以抓走5-f{1,2,4}张牌,剩余{4,3,1}张,mex{sg[4],sg[3],sg[1]}={1,0,1},故sg[5]=2

    以此类推.....

       n  0 1 2 3 4 5 6 7 8 9....

    sg[n] 0 1 2 0 1 2 0 1 2 0....

    由上述实例我们就可以得到1~n的SG值的计算步骤,如下所示:
    ①、使用f数组保存可抓取的扑克牌张数。
    ②、然后使用vis数组来标记当前状态n的后继m状态。
    ③、最后模拟mex运算,也就是我们在集合mex中查找未被标记值的最小值,将其赋值给sg(n)。
    ④、不断的重复 ② - ③ 的步骤,即可完成计算1~n的SG值。

    关于3种SG值计算方法(重点):

    1、可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
    2、可选步数为任意步,SG(x) = x;
    3、可选步数为一系列不连续的数,用get_SG()计算 

    此题就是选取第3种方法来计算SG值。

    AC代码(非递归版本比较好理解):
     1 #include<bits/stdc++.h>
     2 using namespace std;
     3 const int maxn = 1010;
     4 int n,f[11],sg[maxn];
     5 bool vis[maxn];
     6 //f[]:每次抓牌的个数
     7 //sg[]: 0~n的SG函数值
     8 //vis[]:mex{}
     9 void init(){//初始化
    10     f[1] = 1;//下标从1开始
    11     for(int i=2;i<=10;++i)f[i]=f[i-1]*2;//这里只需枚举到512即可,因为1024已经超过n=1000了
    12 }
    13 void get_SG(){
    14     memset(sg,0,sizeof(sg));
    15     for(int i=1;i<maxn;++i){
    16         memset(vis,false,sizeof(vis));//每轮到当前i就重新初始化vis都为未访问状态,找出不属于这个集合的最小非负整数
    17         for(int j=1;j<11 && f[j]<=i;++j)//j<11要放在判断条件的前面,不然会出现错误即越界,因为数组长度只有10
    18             vis[sg[i-f[j]]]=true;//i-f[j]为后继状态,vis[sg[i-f[j]]]收录mex集合
    19         for(int j=0;j<maxn;++j)//求没有出现在mex集合中的非负最小值
    20             if(!vis[j]){sg[i]=j;break;}
    21     }
    22 }
    23 int main()
    24 {
    25     init();
    26     get_SG();
    27     while(cin>>n){
    28         if(sg[n])cout<<"Kiki"<<endl;//当sg[n]不为0时,即为N-position,此时先手必赢
    29         else cout<<"Cici"<<endl;
    30     }
    31     return 0;
    32 }

     再贴一下dfs版本代码:

     1 #include<bits/stdc++.h>
     2 using namespace std;
     3 const int maxn = 1050;
     4 int n,f[11],sg[maxn];
     5 /*
     6 SG值:一个点的SG值就是一个不等于它的后继点的SG的且大于等于零的最小整数。
     7 同mex()函数。简单点来讲就是当前状态离最近一个必败点的距离。距离为0就是必败点
     8 SG(x)=mex(S),S是x的后继状态的SG函数值集合,mex(S)表示不在S内的最小非负整数
     9 SG值是P/N状态的具体化
    10 */
    11 int mex(int x){//求该点的SG值(采用记忆化搜索)
    12     if(sg[x]!=-1)return sg[x];//搜索过了
    13     bool vis[maxn];//vis数组要在此声明,不然会出错,因为这里是递归操作
    14     memset(vis,false,sizeof(vis));
    15     for(int i=1;i<=10;++i){
    16         int tmp=x-f[i];
    17         if(tmp<0)break;//当差值小于0,直接退出
    18         sg[tmp]=mex(tmp);//找sg[tmp]的后继值
    19         vis[sg[tmp]]=true;//回退的时候标记后继sg值标记为true
    20     }
    21     for(int i=0;i<=maxn;++i)//每次break退出时就取不属于mex集合的最小非负整数
    22         if(!vis[i]){sg[x]=i;break;}
    23     return sg[x];//返回x的最小非负整数
    24 }
    25 int main()
    26 {
    27     f[1]=1;
    28     for(int i=2;i<=10;++i)
    29         f[i]=f[i-1]*2;//只需枚举到512就行了,因为1024>1000没必要取到
    30     memset(sg,-1,sizeof(sg));//初始化为-1,记忆化搜索
    31     while(cin>>n){
    32         if(mex(n))cout<<"Kiki"<<endl;//当sg[n]不为0时即为N-position,先手必赢
    33         else cout<<"Cici"<<endl;
    34     }
    35     return 0;
    36 }

     更多详解参考一下这篇博文:博弈论 SG函数

  • 相关阅读:
    Kotlin Coroutines不复杂, 我来帮你理一理
    Refresh design pattern
    Android App安装包瘦身计划
    Google IO 2019 Android 太长不看版
    Effective Java读书笔记完结啦
    探究高级的Kotlin Coroutines知识
    移动应用中的非功能性(跨职能)需求
    Android程序员的Flutter学习笔记
    如何正确使用Espresso来测试你的Android程序
    MVP模式, 开源库mosby的使用及代码分析
  • 原文地址:https://www.cnblogs.com/acgoto/p/9095533.html
Copyright © 2011-2022 走看看