zoukankan      html  css  js  c++  java
  • 博弈学习 3

    链接 1 :http://blog.csdn.net/logic_nut/article/details/4711489
    链接 2 :http://blog.sina.com.cn/s/blog_83d1d5c70100y9yd.html
    链接 3 :http://blog.csdn.net/luomingjun12315/article/details/45479073

    继续总结~~~


    上一次学到了 Nim 游戏,
    并且了解了找出必胜策略的方法。

    通常的Nim游戏的定义是这样的:
    有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,
    如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。

    但如果把Nim的规则略加改变,
    比如说:有 n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗…应该如何解题。

    (1)Sprague-Garundy函数:

    现在我们来研究一个看上去似乎更为一般的游戏:

    给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,
    无法移动者判负。

    也就是把游戏抽象成一个有向图。
    可以通过把每个局面看成一个顶点,对每个局面和它的子局面连一条有向边来抽象成这个 "有向图游戏 "。
    下面就在有向无环图的顶点上定义Sprague-Garundy函数。

    Sprague-Grundy函数,在此简称 sg函数。

    首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。
    例如: mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

    对于一个给定的有向无环图,关于图的每个顶点的 Sg 函数 g 如下:

    g(x)=mex{ g(y) | y是能够由 x 移动到的点,即后继节点}。

    引用链接 2 中的例子:
    如果在取子游戏中每次只能取{1,2,3},那么各个数的SG值是多少?

    x 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14. . .
    g(x) 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 . . .

    利用上次学到的结论也可以推出上表 , 但这次要学习的是 如何计算 Sg 函数。
    由于没有找到求 sg 函数值的具体过程,所以就按照定义自己脑补了,如有不对望指正 (^-^)

    g(0) = mex{ 空集 } = 0;
    因为 0 不能拿走任何棋子,没有后继 ;

    g(1) = mex{g(0)} = 1;

    因为合法的移动集合是{1,2,3},当还有 1 颗石子时,可以拿 1,那么 他的后继节点就是 0 ,
    又 g(0) = 0; 所以 g(1) = mex{ g(0)} = mex{ 0 } = 1;

    g(2) = mex{g(1),g(0)} = mex{0,1} = 2 ; ( g(1),g(0)在上面已算出)

    之后的同理可得。

    (2) “游戏的和”

    引用链接 1 原文:

    再考虑在本文一开头的一句话:任何一个ICG都可以抽象成一个有向图游戏。
    所以“SG函数”和“游戏的和”的概念就不是局限于有向图游戏。
    我们给每个 ICG的每个position定义SG值,也可以定义n个ICG的和。
    所以说当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,
    就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略了!

    回到本文开头的问题。有n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗……
    我们可以把它看作3个子游戏,
    第1个子游戏只有一堆石子,每次可以取1、2、3颗,很容易看出x颗石子的局面的SG值是x%4。
    第2个子游戏也是只有一堆石子,每次可以取奇数颗,经过简单的画图可以知道这个游戏有x颗石子时的SG值是x%2。
    第3个游戏有n-2堆石子,就是一个Nim游戏。
    对于原游戏的每个局面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,
    然后就可以根据这个SG值判断是否有必胜策略以及做出决策了。
    其实看作3个子游戏还是保守了些,干脆看作n个子游戏,其中第1、2个子游戏如上所述,第3个及以后的子游戏都是“1堆石子,每次取几颗都可以”,
    称为“任取石子游戏”,这个超简单的游戏有x颗石子的SG值显然就是x。其实,n堆石子的Nim游戏本身不就是n个“任取石子游戏”的和吗?

    所以,对于我们来说,SG函数与“游戏的和”的概念不是让我们去组合、制造稀奇古怪的游戏,
    而是把遇到的看上去有些复杂的游戏试图分成若干个子游戏,对于每个比原游戏简化很多的子游戏找出它的SG函数,
    然后全部异或起来就得到了原游戏的SG函数,就可以解决原游戏了。

    (3) 模板

    (1) 求出 1 -n 范围的 sg 值

    /*
    s 数组表示移动集合,即可以走的步数或可以取石子的数量;k 表示集合大小;
    s 数组要从小到大排序,以保证更方便的找到它的所有后继 
    sg 数组保存 sg 值;
    vis 数组标记已访问的点;
    另:
    1.如果可以移动的步数为 1-m 的连续整数,那么它的 sg 值就是 g(x) = x % (m+1) ;
    
    */
    
     
    int sg[maxn],vis[maxn],s[maxn],k;
    void getsg()
    {
        for(int i=0;i<=n;i++){   // g(x)=mex{ g(y) | y是能够由 x 移动到的点,即后继节点}。
            memset(vis,0,sizeof(vis));  
            for(int j=0;s[j] <= i && j<k;j++){  // 找到它所有能够移动到的点(即后继)并标记 
                vis[sg[x-s[i]]] = 1;
            }
            for(int j = 0;;j++){   // 找到最小的不属于集合 g(y) 的非负整数
                if(vis[j] == 0){
                    sg[i] = j; break;
                } 
            }
        }     
    } 

    (2) 得到单个 sg 值 

    int sg[maxn],s[maxn],k;
    int getsg(int x) /// 得到单个 sg 值 
    {
        int hash[110] = {0};
        for(int i=0;s[i]<=x && i<k;i++){
            if(sg[x-s[i]] == -1) sg[x-s[i]] = getsg(x-s[i]);
            hash[ sg[x-s[i]]] = 1;
        }
        for(int i=0;;i++){
            if(hash[i] == 0) return i;
        }
    }

    题目: HDU1847  1848  1536

    HDU1847

    题意:

    KiKi 和 CiCi 玩游戏,总共n张牌;
    双方轮流抓牌;
    每人每次抓牌的个数只能是2的幂次(即:1,2,4,8,16…)

    如果Kiki能赢的话,请输出“Kiki”,否则请输出“Cici”。

    解题:

    巴什博奕,用上述方法计算出各点的 sg值 就可以发现规律辣 ~ 

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<iostream>
    #include<string>
    using namespace std;
    int main()
    {
        int n;
        while(scanf("%d",&n)!=EOF){
            if(n%3 == 0) printf("Cici
    ");
            else printf("Kiki
    ");
        }
        return 0;
    }

    HDU 1848

    题意:

    一共有3堆石子,数量分别是m, n, p个;两人轮流走;每次可以选择任意一堆石子,然后取走f个;
    f只能是菲波那契数列中的元素; 先取完所有石子的人为胜;

    解题:
    求出所有 sg 值,然后异或一下就好辣~ (> <)

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<iostream>
    #include<string>
    using namespace std;
    int fib[20],sg[1010],vis[1010];
    void init()
    {
        fib[1] = 1,fib[2] = 2;
        for(int i=3;i<20;i++){
            fib[i] = fib[i-1]+fib[i-2];
            if(fib[i] > 1000) break;
        }
    }
    void getsg()
    {
        sg[0] = 0; // 此点为先手必败点,sg 值为 0 ; 
        for(int i=1;i<1001;i++){  // n,m ,p 的范围在1000以内 所以只要算 1000个点 的 sg 值 
            memset(vis,0,sizeof(vis)); // 每次清空 vis 数组 ; 
            for(int j=1;fib[j]<=i;j++){  // 找出后继节点 sg 值并标记 ;g(x) = mex(g(y)); 
                vis[ sg[ i-fib[j] ] ] = 1;
            }                    
            for(int j=0;j<1010;j++){  // 找到在它后继节点里 未出现过的 最小的数 
                if(vis[j] == 0) {
                    sg[i] = j; break;
                }
            }        
        }        
    }
    int main()
    {
        int n,m,p;
        init();
        getsg();
        while(scanf("%d%d%d",&n,&m,&p)!=EOF && (n||m||p)){
            if( (sg[n] ^ sg[m] ^ sg[p] ) != 0 ) printf("Fibo
    ");
            else printf("Nacci
    ");
        }    
        return 0;
    }

    HDU 1536

    题意:
    首先输入 k ,表示集合大小,,接下来 k 个数,表示可以取的石子的个数 ;
    输入 m 表示m次询问
    接下来m行 每行输入一个n,表示有 n 堆,接下来 n 个数,表示每堆有ni个石子;
    输出每次询问的结果,赢输出 W ,否则 L ;


    解题:
    求出每一堆的 sg 值,异或一下就好辣;(因为眼瞎数组开的不够大WA好几次= = 百思不得其解,重新看了一下数据范围= =)

    值得注意的是,这里不能每次都把所有的 sg 值都求出来,会超时。
    类似的,有时候会遇到 只需要某些 sg 值就可以了,而其他的有些 sg 值是用不到的 ,
    这时候只要求出单个 sg 值就可以,如果之后还有可能用到,可以用类似 "记忆化搜索"的方法保存下来。

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<iostream>
    #include<string>
    using namespace std;
    const int maxn = 10010;
    int sg[maxn],k,n,m,s[110],x;
    int getsg(int x) /// 得到单个 sg 值 
    {
        int hash[110] = {0};
        for(int i=0;s[i]<=x && i<k;i++){
            if(sg[x-s[i]] == -1) sg[x-s[i]] = getsg(x-s[i]);
            hash[ sg[x-s[i]]] = 1;
        }
        for(int i=0;;i++){
            if(hash[i] == 0) return i;
        }
    }
    int main()
    {
        while(scanf("%d",&k)!=EOF && k){
            memset(s,0,sizeof(s)); 
            for(int i=0;i<k;i++)
                scanf("%d",&s[i]);
            sort(s,s+k);
            memset(sg,-1,sizeof(sg));
            int fans[110];
            scanf("%d",&m);        
            for(int i=0;i<m;i++){
                int ans = 0;
                scanf("%d",&n);
                for(int j=0;j<n;j++){
                    scanf("%d",&x);
                    if(sg[x] == -1) sg[x] = getsg(x);  // 若是 sg 值未得出就调用函数求出来,已经得出 直接异或 
                    ans ^= sg[x];
                }
                fans[i] = ans;        
            }
            for(int i=0;i<m;i++)
                printf((fans[i])?"W":"L");
            cout<<endl;
        }
        return 0;
    }
  • 相关阅读:
    往下滚动,导航栏隐藏
    判断是模拟器还是真机
    根据颜色生成图片
    UITextfiled 设置输入前面空格
    iOS 滑动TableView控制导航栏隐藏与显示
    时间 多少分钟前
    时间戳转时间
    iOS 常用公共方法(一)
    找工作感悟
    java 内存泄露
  • 原文地址:https://www.cnblogs.com/ember/p/5718045.html
Copyright © 2011-2022 走看看