博弈学习 3 - SG函数与“游戏的和”

链接 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\n");
        else printf("Kiki\n");
    }
    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\n");
        else printf("Nacci\n");
    }    
    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;
}

 

posted @ 2016-07-29 13:26  Ember  阅读(314)  评论(0编辑  收藏  举报