博弈论

原文地址:https://wenku.baidu.com/view/74ffdb44bb68a98271fefa57.html
(一)巴什博奕(Bash Game):只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。
显然,如果n=m+1,那么由于一次最多只能取m个,所以,无论先取者拿走多少个,后取者都能够一次拿走剩余的物品,后者取胜。因此我们发现了如何取胜的法则:如果n=(m+1)r+s,(r为任意自然数,s≤m),那么先取者要拿走s个物品,如果后取者拿走k(≤m)个,那么先取者再拿走m+1-k个,结果剩下(m+1)(r-1)个,以后保持这样的取法,那么先取者肯定获胜。总之,要保持给对手留下(m+1)的倍数,就能最后获胜。
这个游戏还可以有一种变相的玩法:两个人轮流报数,每次至少报一个,最多报十个,谁能报到100者胜。
取石子(一)
时间限制:3000 ms | 内存限制:65535 KB
难度:2
描述
一天,TT在寝室闲着无聊,和同寝的人玩起了取石子游戏,而由于条件有限,他/她们是用旺仔小馒头当作石子。游戏的规则是这样的。设有一堆石子,数量为N(1<=N<=1000000),两个人轮番取出其中的若干个,每次最多取M个(1<=M<=1000000),最先把石子取完者胜利。我们知道,TT和他/她的室友都十分的聪明,那么如果是TT先取,他/她会取得游戏的胜利么?
输入
第一行是一个正整数n表示有n组测试数据
输入有不到1000组数据,每组数据一行,有两个数N和M,之间用空格分隔。
输出
对于每组数据,输出一行。如果先取的TT可以赢得游戏,则输出“Win”,否则输出“Lose”(引号不用输出)
样例输入
2
1000 1
1 100
样例输出
Lose
Win

最优解:

#include<iostream>
using namespace std;
int main()
{
    int k;
    long m,n;
    cin>>k;
    while(k--)
    {
        cin>>n>>m;
        if(n%(m+1)==0)
            cout<<"Lose"<<endl;
        else
            cout<<"Win"<<endl;
    }
}       

巴什博弈变形:
有两种解,依实际情况而定:
取石子(七)
时间限制:1000 ms | 内存限制:65535 KB
难度:1
描述
Yougth和Hrdv玩一个游戏,拿出n个石子摆成一圈,Yougth和Hrdv分别从其中取石子,谁先取完者胜,每次可以从中取一个或者相邻两个,Hrdv先取,输出胜利着的名字。
输入
输入包括多组测试数据。
每组测试数据一个n,数据保证int范围内。
输出
输出胜利者的名字。
样例输入
2
3
样例输出
Hrdv
Yougth
解一:

#include<cstdio>
int n;
int main()
{
    while(~scanf("%d",&n))
        printf(n>=3?"Yougth\n":"Hrdv\n");
    return 0;
}

解二:3的倍数的是Yougth嬴
#include<iostream>
using namespace std;
int main()
{
int a;
while(cin>>a)
{
if(a%3!=0)
cout<<"Hrdv"<<endl;
else cout<<"Yougth"<<endl;

}
return 0;
}

尼姆博弈基本思想:
两人从n堆物品中取任意个,先取完者胜。
即将n堆物品的数量异或,得到的值如果为0,则先手败,反之先手胜。
如果要求先手在胜的条件下,到奇异局势的方法数,则判断异或的值与每一堆原值异或后(结果应该表示该堆没有参加异或时的异或值)与原值比较大小,
如果小于,则方法数加一。且对应的方法后,该堆的数目应变为异或的值与每一堆原值异或的值。
取石子(二)
时间限制:3000 ms | 内存限制:65535 KB
难度:5
描述
小王喜欢与同事玩一些小游戏,今天他们选择了玩取石子。
游戏规则如下:共有N堆石子,已知每堆中石子的数量,并且规定好每堆石子最多可以取的石子数(最少取1颗)。
两个人轮流取子,每次只能选择N堆石子中的一堆,取一定数量的石子(最少取一个),并且取的石子数量不能多于该堆石子规定好的最多取子数,等哪个人无法取子时就表示此人输掉了游戏。
假设每次都是小王先取石子,并且游戏双方都绝对聪明,现在给你石子的堆数、每堆石子的数量和每堆石子规定的单次取子上限,请判断出小王能否获胜。
输入
第一行是一个整数T表示测试数据的组数(T<100)
每组测试数据的第一行是一个整数N(1

#include<cstdio>
int main(){
    int T; 
    scanf("%d",&T);
    while(T--){
        int m,n,g,sum=0;
        scanf("%d",&g);
        while(g--){scanf("%d%d",&m,&n);sum ^= m % (n + 1);}
        puts(sum?"Win":"Lose");
    }
}       
一般解:
#include <iostream>
using namespace std;
#include <stdio.h>

bool HandleEachCase();

int main(){
int iCaseCount;
cin>>iCaseCount;
while(iCaseCount--){
if(HandleEachCase()){
cout<<"Win"<<endl;
}else{
cout<<"Lose"<<endl;
}
}
}

bool HandleEachCase(){
long long magic= 0;
long long iCount;
long long m, n;
cin>>iCount;
for(int i= 0; i< iCount; ++i){
cin>>m>>n;
m%= (n+1);
magic^= m;
}
return magic != 0;
}

取石子(六)
时间限制:1000 ms | 内存限制:65535 KB
难度:3
描述
最近TopCoder的PIAOYI和HRDV很无聊,于是就想了一个游戏,游戏是这样的:有n堆石子,两个人轮流从其中某一堆中任意取走一定的石子,最后不能取的为输家,注意: 每次只能从一堆取任意个,可以取完这堆,但不能不取。假设PIAOYI先取石子,请你帮他判断他是否能赢(假设他们取的过程中不发生失误,他们足够聪明 )。
输入
第一行输入n,代表有n组测试数据(n<=10000)
以下每组测试数据包含两行:第一行:包含一个整数m,代表本组测试数据有m(m<=1000)堆石子;
:第二行:包含m个整数Ai(Ai<=100),分别代表第i堆石子的数量。
输出
若PIAOYI赢输出“PIAOYI”,否则输出“HRDV”注意每组结果占一行。。
样例输入
3
2
1 1
3
3 8 11
2
5 10

最优解:

#include<iostream>
#include<stdio.h>
using namespace std;
void in(int &a)
{
    char ch;
    while((ch=getchar())<'0'||ch>'9');
    for(a=0;ch>='0'&&ch<='9';ch=getchar()) a=a*10+ch-'0';
}
int main()
{
    int T;in(T);
    while(T--)
    {
        int n;in(n);
        int ans=0;
        for(int i=0;i!=n;++i)
        {
            int b;in(b);
            ans^=b;
        }
        if(ans) puts("PIAOYI");
        else     puts("HRDV");
    }return 0;
}        

取石子(三)
时间限制:1000 ms | 内存限制:1000 KB
难度:6
描述
小王喜欢与同事玩一些小游戏,今天他们选择了玩取石子。
游戏规则如下:共有N堆石子,已知每堆中石子的数量,两个人轮流取子,每次只能选择N堆石子中的一堆,取一定数量的石子(最少取一个),取过子之后,还可以将该堆石子中剩下的任意多个石子中随意选取几个放到其它的任意一堆或几堆上。等哪个人无法取子时就表示此人输掉了游戏。注意,一堆石子没有子之后,就不能再往此处放石子了。
假设每次都是小王先取石子,并且游戏双方都绝对聪明,现在给你石子的堆数、每堆石子的数量,请判断出小王能否获胜。
例如:如果最开始有4堆石子,石子个数分别为3 1 4 2,而小王想决定要先拿走第三堆石子中的两个石子(石子堆状态变为3 1 2 2),然后他可以使石子堆达到的状态有以下几种:
3 1 2 2(不再移动石子)
4 1 1 2(移动到第一堆一个)
3 2 1 2(移动到第二堆一个)
3 1 1 3(移动到第四堆一个)
5 1 0 2(全部移动到第一堆)
3 3 0 2(全部移动到第二堆)
3 1 0 4(全部移动到最后)
输入
可能有多组测试数据(测试数据组数不超过1000)
每组测试数据的第一行是一个整数,表示N(1<=N<=10)
第二行是N个整数分别表示该堆石子中石子的数量。(每堆石子数目不超过100)
当输入的N为0时,表示输入结束
输出
对于每组测试数据,输出Win表示小王可以获胜,输出Lose表示小王必然会败。
样例输入
3
2 1 3
2
1 1
0
样例输出
Win
Lose
一般解:

#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;

bool HandleEachCase();

int main(){
while(HandleEachCase()){
//empty while
}
}


bool HandleEachCase(){
int iCount;
int count[101];
memset(count, 0, sizeof(count));
cin>>iCount;
if(!iCount){
return false;
}
int iStone;
for(int i= 0; i< iCount; ++i){
cin>>iStone;
++count[iStone];
}
int magic = 0;
for(int i= 0; i< 101 && !magic; ++i){
magic+= count[i]&1;
}
if(magic){
cout<<"Win"<<endl;
}else{
cout<<"Lose"<<endl;
}
return true;
}

分析:
显然,如果石头是能够两两配对,每一对的数目相同,比如:2,3,2,4可以配对成(2,2),(4,4) ,这样的话就是先拿的输,因为后拿的可以使自己拿完后仍然能够使得两两配对,且每一对的数目相同.

刚刚开始的时候,如果已经两两配对了,那么先拿的输了,否则,选拿的可以把最大的动一下手脚,使剩下的两两配对,且每一对的数目相同.

最优解:

#include<iostream>
#include<cstring>
using namespace std;
bool ok(int stone[])
{
    for(int i=0;i!=110;i++)
        if(stone[i]&1) return true;
    return false;
}
int main()
{
    int stone[110];
    int n,m;
    while(cin>>n && n)
    {
        memset(stone,0,sizeof(stone));
        while(n--)
        {
            cin>>m;
            stone[m]++;
        }
        cout<<(ok(stone)?"Win":"Lose")<<endl;
    }
}         

威佐夫博奕(Wythoff Game):有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
这种情况下是颇为复杂的。我们用(ak,bk)(ak ≤ bk ,k=0,1,2,…,n)表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。
可以看出,a0=b0=0,ak是未在前面出现过的最小自然数,而 bk= ak + k,奇异局势有如下三条性质:
1。任何自然数都包含在一个且仅有一个奇异局势中。
由于ak是未在前面出现过的最小自然数,所以有ak > ak-1 ,而 bk= ak + k > ak-1 + k-1 = bk-1 > ak-1 。所以性质1。成立。
2。任意操作都可将奇异局势变为非奇异局势。
事实上,若只改变奇异局势(ak,bk)的某一个分量,那么另一个分量不可能在其他奇异局势中,所以必然是非奇异局势。如果使(ak,bk)的两个分量同时减少,则由于其差不变,且不可能是其他奇异局势的差,因此也是非奇异局势。
3。采用适当的方法,可以将非奇异局势变为奇异局势。
假设面对的局势是(a,b),若 b = a,则同时从两堆中取走 a 个物体,就变为了奇异局势(0,0);如果a = ak ,b > bk,那么,取走b - bk个物体,即变为奇异局势;如果 a = ak , b < bk ,则同时从两堆中拿走 ak - ab - ak个物体,变为奇异局势( ab - ak , ab - ak+ b - ak);如果a > ak ,b= ak + k,则从第一堆中拿走多余的数量a - ak 即可;如果a < ak ,b= ak + k,分两种情况,第一种,a=aj (j < k),从第二堆里面拿走 b - bj 即可;第二种,a=bj (j < k),从第二堆里面拿走 b - aj 即可。
从如上性质可知,两个人如果都采用正确操作,那么面对非奇异局势,先拿者必胜;反之,则后拿者取胜。
那么任给一个局势(a,b),怎样判断它是不是奇异局势呢?我们有如下公式:
ak =[k(1+√5)/2],bk= ak + k (k=0,1,2,…,n 方括号表示取整函数)奇妙的是其中出现了黄金分割数(1+√5)/2 = 1。618…,因此,由ak,bk组成的矩形近似为黄金矩形,由于2/(1+√5)=(√5-1)/2,可以先求出j=[a(√5-1)/2],若a=[j(1+√5)/2],那么a = aj,bj = aj + j,若不等于,那么a = aj+1,bj+1 = aj+1+ j + 1,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异
局势。

取石子 (四)
时间限制:1000 ms | 内存限制:65535 KB
难度:4
描述
有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。
输入
输入包含若干行,表示若干种石子的初始情况,其中每一行包含两个非负整数a和b,表示两堆石子的数目,a和b都不大于1,000,000,000。
输出
输出对应也有若干行,每行包含一个数字1或0,如果最后你是胜者,则为1,反之,则为0。
样例输入
2 1
8 4
4 7
样例输出
0
1
0
最优解:

#include <iostream>
#include <cmath> 
using namespace std;

int main()
{    
    int m,n;    
    while(cin>>m>>n)
    {
        if (m > n)
        {
            int temp;
            temp = m;
            m = n;
            n =temp;
        }
        int k = n - m;
        int data = floor(k*(1.0+sqrt(5.0))/2.0);
        if (data == m)
            cout<<0<<endl;
        else
            cout<<1<<endl;
    }
}        

Wythoff Game
时间限制:1000 ms | 内存限制:65535 KB
难度:1
描述
最近ZKC同学在学博弈,学到了一个伟大的博弈问题–威佐夫博弈。
相信大家都学过了吧?没学过?没问题。我将要为你讲述一下这个伟大的博弈问题。
有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。
游戏规定,每次有两种不同的取法:
一是可以在任意的一堆中取走任意多的石子;
二是可以在两堆中同时取走相同数量的石子。
最后把石子全部取完者为胜者。
我们今天要做的是求前n个必败态。
什么是必败态?比如我们把(a,b)称为一种状态,a,b分别为两堆石子中所剩的数目。如果a=0,b=0,我们说该种状态为必败态,因为我不能再进行游戏,即使是可以进行,那也是必败的,你知道,游戏的我们都是非常聪明的。(0,0)(1,2)(3,5)…都是必败态,我们今天要做的就是求前n个必败态。不会?好吧!
我再告诉你:假设第n个必败态为(a,b)a为前n-1个必败态中没有出现的最小自然数,b=a+n。这下大家应该明白了吧。好吧,我们的任务就的要前n个必败态。规定第0个必败态为(0,0)。
输入
多组数据。
输入为一个数n(0<=n<=100000)。
输出
按照要求求出前n个必败态。输出格式看下面样例。
样例输入
3
1
样例输出
(0,0)(1,2)(3,5)(4,7)
(0,0)(1,2)
提示
注意:每种情况中间没有空格

#include<stdio.h>
#include<math.h>
typedef struct Node
{
    int a,b;
}N;
N res[100001];
void init()
{
    res[0].a=0;
    res[0].b=0;
    for(int i=1;i<100001;i++)
    {
        res[i].a=(1+sqrt(5))*i/2;
        res[i].b=res[i].a+i;
    }
}
int main()
{
    int n;
    init();
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=0;i<=n;i++)
        {
            printf("(%d,%d)",res[i].a,res[i].b);    
        }
        printf("\n");
    }
    return 0;
}  

本人自己写的代码:

#include<stdio.h>
int main()
{
    int n,k,b,a[100001],i,m=0;
    a[0]=0;
    while(~scanf("%d",&n))
    {
        if(n<=m)
        {
            for(k=0;k<=n;k++)
            {
                printf("(%d,%d)",a[k],a[k]+k);
            }
        }else
        {
            printf("(%d,%d)",a[0],a[0]);
            for(k=1;k<=n;k++)
            {
                b=a[k-1]+1;
                for(i=k-1;i>=0;i--)
                {
                    if(b==(a[i]+i))
                    {
                        b++;
                    }else if(b>(a[i]+i))
                    {
                        break;
                    }
                }
                a[k]=b;
                printf("(%d,%d)",a[k],a[k]+k);
            }
            m=n;
        }
        printf("\n");

    }
    return 0;
}                      

取石子(八)
时间限制:1000 ms | 内存限制:65535 KB
难度:3
描述
有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。如果你胜,你第1次怎样取子?
输入
输入包含若干行,表示若干种石子的初始情况,其中每一行包含两个非负整数a和b,表示两堆石子的数目,a和b都不大于1,000,000。a=b=0退出。
输出
输出也有若干行,如果最后你是败者,则为0,反之,输出1,并输出使你胜的你第1次取石子后剩下的两堆石子的数量x,y,x<=y。如果在任意的一堆中取走石子能胜同时在两堆中同时取走相同数量的石子也能胜,先输出取走相同数量的石子的情况,假如取一堆的有多种情况,先输出从石子多的一堆中取的情况,且要求输出结果保证第二个值不小于第一个值。
样例输入
1 2
5 7
2 2
0 0
样例输出
0
1
3 5
3 5
4 7
1
0 0
1 2

最优解:

#include<iostream>
#include <cstdio>
#include <algorithm>
#include<math.h>
using namespace std;
int main()
{
    int a,b,temp,temp2,k,i;
    while(scanf("%d%d",&a,&b),a+b)
    {
        if(a>b)
            swap(a,b);
        k=b-a;
        temp=k*(1.0+sqrt(5.0))/2.0;
        if(a==temp)    //奇异局势
            printf("0\n");
        else
        {
            printf("1\n");
            if(abs(temp-a)==abs(temp+k-b)&&temp<a)    //两堆
                printf("%d %d\n",temp,temp+k);
            if(a==0)    //0 0情况,第一种奇异局势
                printf("0 0\n");
            for(i=1;i<=b;i++)  //一堆
            {
                temp=i*(1.0+sqrt(5.0))/2.0;
                temp2=temp+i;
                if(temp>b)
                    break;
                if(temp==a&&temp2<b)
                    printf("%d %d\n",a,temp2);
                else if(temp2==a&&temp<b)
                    printf("%d %d\n",temp,a);
                else if(temp2==b&&temp<a)
                    printf("%d %d\n",temp,b);
            }
        }
    }
    return 0;
}        

Fibonacci’s Game(斐波那契博弈)
斐波那契博弈模型,大致上是这样的:
有一堆个数为 n 的石子,游戏双方轮流取石子,满足:
1. 先手不能在第一次把所有的石子取完;
2. 之后每次可以取的石子数介于1到对手刚取的石子数的2倍之间(包含1和对手刚取的石子数的2倍)。
约定取走最后一个石子的人为赢家,求必败态。

(转)分析:
n = 2时输出second;
n = 3时也是输出second;
n = 4时,第一个人想获胜就必须先拿1个,这时剩余的石子数为3,此时无论第二个人如何取,第一个人都能赢,输出first;
n = 5时,first不可能获胜,因为他取2时,second直接取掉剩下的3个就会获胜,当他取1时,这样就变成了n为4的情形,所以输出的是second;
n = 6时,first只要去掉1个,就可以让局势变成n为5的情形,所以输出的是first;
n = 7时,first取掉2个,局势变成n为5的情形,故first赢,所以输出的是first;
n = 8时,当first取1的时候,局势变为7的情形,第二个人可赢,first取2的时候,局势变成n为6得到情形,也是第二个人赢,取3的时候,second直接取掉剩下的5个,所以n = 8时,输出的是second;
…………
从上面的分析可以看出,n为2、3、5、8时,这些都是输出second,即必败点,仔细的人会发现这些满足斐波那契数的规律,可以推断13也是一个必败点。
借助“Zeckendorf定理”(齐肯多夫定理):任何正整数可以表示为若干个不连续的Fibonacci数之和。n=12时,只要谁能使石子剩下8且此次取子没超过3就能获胜。因此可以把12看成8+4,把8看成一个站,等价与对4进行”气喘操作”。又如13,13=8+5,5本来就是必败态,得出13也是必败态。也就是说,只要是斐波那契数,都是必败点。
所以我们可以利用斐波那契数的公式:fib[i] = fib[i-1] + fib[i-2],只要n是斐波那契数就输出No。

取石子(五)
时间限制:1000 ms | 内存限制:65535 KB
难度:4
描述
himdd最近很想玩游戏,于是他找到acmj和他一起玩,游戏是这样的:有一堆石子,两个人轮流从其中取走一定的石子,取走最后所有石子的人为赢家,不过得遵循如下规则:
1.第一次取不能取完,至少取1颗.
2.从第二次开始,每个人取的石子数至少为1,至多为对手刚取的石子数的两倍。
himdd事先想知道自己会不会赢,你能帮帮他吗?(每次himdd先手)
输入
有多组测试数据,每组有一个整数n(2<=n<2^64);
输出
himdd会赢输出Yes,否则输出No;
样例输入
2
5
6
样例输出
No
No
Yes
一般解:

#include<iostream>
using namespace std;
int main()
{   
    long long n,fib[100]; 
    int i,flag;
    fib[0]=2;
    fib[1]=3;
    for(i=2;i<100;i++)  
        fib[i]=fib[i-1]+fib[i-2];  
    while(cin>>n&&n)
    {           
        flag=0;
        for(i=0;i<100;i++)   
            if(fib[i]==n)     
            {           
                cout<<"No\n";
                flag=1;
                break;
            }  
            if(flag==0)    
                cout<<"Yes\n";   
    }   
    return 0;
}

Nim Staircase博奕:
这个问题是尼姆博弈的拓展:游戏开始时有许多硬币任意分布在楼梯上,共n阶楼梯从地面由下向上编号为0到n。游戏者在每次操作时可以将楼梯j(1<=j<=n)上的任意多但至少一个硬币移动到楼梯j-1上。游戏者轮流操作,将最后一枚硬币移至地上(0号)的人获胜。
算法:将奇数楼层的状态异或,和为0则先手必败,否则先手必胜。证明略。
例题:Poj1704
这道题可以把两个棋子中间间隔的空格子个数作为一堆石子,则原题转化为每次可以把左边的一堆石子移到相邻的右边的一堆中。也就是阶梯尼姆博弈,注意对输入数据先排序,然后倒着往前数(a[n]-a[n-1]-1为第一个),奇数个数到的就做一下xor,其中最前面的看做a[1]-0-1,参考程序:

vart,n,b,i,j:longint;a:array[0..1000]of longint;beginreadln(t);repeatdec(t);readln(n);for i:=1 to n do read(a[i]);qsort(1,n);//快排略j:=0;b:=0;for i:=n downto 1 dobegininc(j);if odd(j) then b:=b xor (a[i]-a[i-1]-1);end;if b=0 then writeln('Bob will win') else writeln('Georgia will win');until t=0;end.

SG函数模板
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
对于一个给定的有向无环图,定义关于图的每个顶点的Sprague-Grundy函数g如下:g(x)=mex{ g(y) | y是x的后继 },这里的g(x)即sg[x]
例如:取石子问题,有1堆n个的石子,每次只能取{1,3,4}个石子,先取完石子者胜利,那么各个数的SG值为多少?
sg[0]=0,f[]={1,3,4},
x=1时,可以取走1-f{1}个石子,剩余{0}个,mex{sg[0]}={0},故sg[1]=1;
x=2时,可以取走2-f{1}个石子,剩余{1}个,mex{sg[1]}={1},故sg[2]=0;
x=3时,可以取走3-f{1,3}个石子,剩余{2,0}个,mex{sg[2],sg[0]}={0,0},故sg[3]=1;
x=4时,可以取走4-f{1,3,4}个石子,剩余{3,1,0}个,mex{sg[3],sg[1],sg[0]}={1,1,0},故sg[4]=2;
x=5时,可以取走5-f{1,3,4}个石子,剩余{4,2,1}个,mex{sg[4],sg[2],sg[1]}={2,0,1},故sg[5]=3;
以此类推…..
x 0 1 2 3 4 5 6 7 8….
sg[x] 0 1 0 1 2 3 2 0 1….
计算从1-n范围内的SG值。
f(存储可以走的步数,f[0]表示可以有多少种走法)
f[]需要从小到大排序,这个模版f是从1开始的。hash数组大小跟f[]大小差不多
1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
2.可选步数为任意步,SG(x) = x;
3.可选步数为一系列不连续的数,用GetSG()计算

//f[]:可以取走的石子个数
//sg[]:0~n的SG函数值
//hash[]:mex{}
int f[N],sg[N],hash[N];     
void getSG(int n)
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=n;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1;f[j]<=i;j++)
            hash[sg[i-f[j]]]=1;
        for(j=0;j<=n;j++)    //求mes{}中未出现的最小的非负整数
        {
            if(hash[j]==0)
            {
                sg[i]=j;
                break;
            }
        }
    }
}

下边补充一点东西。

上边那个模版求hash时候并没有考虑f[j]有效长度,在某些题目中可以通过,比如这个。因为在求斐波那契数列时候肯定多求了一个,而就是因为这个会在求hash时候打破循环。

其实总的来说还是这个函数并不严密。因为有的时候f[]是有有效长度的,如果多出了这个长度就会出现错误,如果你的初值都是0,那么就会取到0,如果是-1,那么就会取到-1,肯定不对。比如这个题。

下面这两个模版应该就比较严密了,这个里边的f[]是从零开始的。转自:http://blog.csdn.net/primoblog/article/details/13376057

1、sg打表

//f[]:可以取走的石子个数  
//sg[]:0~n的SG函数值  
//hash[]:mex{}  
int f[K],sg[N],hash[N];  
void getSG(int n)  
{  
        memset(sg,0,sizeof(sg));  
        for(int i=1; i<=n; i++) {  
                memset(hash,0,sizeof(hash));  
                for(int j=0; f[j]<=i && j < k; j++) //k是f[]的有效长度  
                        hash[sg[i-f[j]]]=1;  
                for(int j=0; ; j++) {   //求mes{}中未出现的最小的非负整数  
                        if(hash[j]==0) {  
                                sg[i]=j;  
                                break;  
                        }  
                }  
        }  
}  

2、Dfs

//注意 S数组要按从小到大排序 SG函数要初始化为-1 对于每个集合只需初始化1遍  
//n是集合s的大小 S[i]是定义的特殊取法规则的数组  
int s[N],sg[N],n;  
int getSG(int x)  
{  
        if(sg[x]!=-1)  
                return sg[x];  
        bool vis[M];  
        memset(vis,0,sizeof(vis));  
        for(int i=0; i<n; i++) {  
                if(x>=s[i])  
                        vis[getSG(x-s[i])]=1;  
        }  
        for(i=0;; i++)  
                if(!vis[i]) {  
                        sg[x]=i;  
                        break;  
                }  
        return sg[x];  
} 

博弈问题之SG函数博弈小结
2013-09-05 09:11:30 我来说两句 作者:Bright-xl 收藏 我要投稿
SG函数:
给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,无法移 动者判负。事实上,这个游戏可以认为是所有Impartial Combinatorial Games的抽象模型。也就是说,任何一个ICG都可以通过把每个局面看成一个顶点,对每个局面和它的子局面连一条有向边来抽象成这个“有向图游戏”。下 面我们就在有向无环图的顶点上定义Sprague-Garundy函数。首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
在我的理解中,sg函数就是一个对有向无环图dfs的过程,在处理nim博弈时,多个石堆可以看成多个sg函数值的异或。
例题:
POJ2311 Cutting Game
典型的sg博弈,找后继状态。题意是给出一个n*m的纸片,每次剪成两部分,谁先剪到1*1就胜利。这就是一个找后继的题目,每次剪成的两部分就是前一状态的后继,只要将两个部分的sg值异或起来就是前一状态的sg值。

[cpp]  
#include<iostream>  
#include<cstdio>  
#include<algorithm>  
#include<cstring>  
#include<string>  
#include<cmath>  
#include<set>  
#include<vector>  
#include<stack>  
#define mem(a,b) memset(a,b,sizeof(a))  
#define FOR(a,b,i) for(i=a;i<=b;++i)  
#define For(a,b,i) for(i=a;i<b;++i)  
#define N 1000000007  
using namespace std;  
inline void RD(int &ret)  
{  
    char c;  
    do  
    {  
        c=getchar();  
    }  
    while(c<'0'||c>'9');  
    ret=c-'0';  
    while((c=getchar())>='0'&&c<='9')  
    {  
        ret=ret*10+(c-'0');  
    }  
}  
inline void OT(int a)  
{  
    if(a>=10)  
    {  
        OT(a/10);  
    }  
    putchar(a%10+'0');  
}  
int sg[201][201];  
int dfs(int a,int b)//sg函数的一般写法  
{  
    if(sg[a][b]>=0)  
    {  
        return sg[a][b];  
    }  
    int i,map[201],r;//map标记数组一定要在dfs内部定义,不然会出现错误  
    mem(map,0);  
    FOR(2,(a/2),i)  
    {  
        r=dfs(i,b)^dfs(a-i,b);//后继的异或得到前一状态的sg值  
        map[r]=1;  
    }  
    FOR(2,(b/2),i)  
    {  
        r=dfs(a,i)^dfs(a,b-i);  
        map[r]=1;  
    }  
    FOR(0,200,i)  
    {  
        if(map[i]==0)  
        {  
            return sg[a][b]=i;//mex公式的应用  
        }  
    }  
}  
int main()  
{  
    int n,m,sum;  
    mem(sg,-1);  
    while(scanf("%d%d",&n,&m)!=EOF)  
    {  
        sum=dfs(n,m);  
        if(sum>0)  
        {  
            printf("WIN\n");  
        }  
        else  
        {  
            printf("LOSE\n");  
        }  
    }  
    return 0;  
}  

POJ2425 A Chess Game
题意是给你一个拓扑图,一个起点上的n个棋子,两个玩家交替移动棋子,谁无法移动谁输,典型的sg函数运用。套用模板就行了。此题数据量较大,加入了输入优化后刷到了第一版第四名,nice!
[cpp]

#include<iostream>  
#include<cstdio>  
#include<algorithm>  
#include<cstring>  
#include<string>  
#include<cmath>  
#include<set>  
#include<vector>  
#include<stack>  
#define mem(a,b) memset(a,b,sizeof(a))  
#define FOR(a,b,i) for(i=a;i<=b;++i)  
#define For(a,b,i) for(i=a;i<b;++i)  
#define N 1000000007  
using namespace std;  
inline void RD(int &ret)  
{  
    char c;  
    do  
    {  
        c=getchar();  
    }  
    while(c<'0'||c>'9');  
    ret=c-'0';  
    while((c=getchar())>='0'&&c<='9')  
    {  
        ret=ret*10+(c-'0');  
    }  
}  
inline void OT(int a)  
{  
    if(a>=10)  
    {  
        OT(a/10);  
    }  
    putchar(a%10+'0');  
}  
int vis[1001][1001],sg[1001];  
int n;  
int dfs(int x)//典型的sg过程  
{  
    int i;  
    if(sg[x]!=-1)  
    {  
        return sg[x];  
    }  
    int f[1001];  
    mem(f,0);  
    For(0,n,i)  
    {  
        if(vis[x][i]!=-1)  
        {  
            f[dfs(i)]=1;  
        }  
    }  
    i=0;  
    while(f[i])  
    {  
        i++;  
    }  
    return sg[x]=i;  
}  
int main()  
{  
    int i,j,k,t,x,p,sum;  
    while(scanf("%d",&n)!=EOF)  
    {  
        mem(vis,-1);  
        mem(sg,-1);  
        For(0,n,i)  
        {  
            RD(k);  
            if(k==0)  
            {  
                sg[i]=0;  
            }  
            For(0,k,j)  
            {  
                RD(t);  
                vis[i][t]=1;//建图  
            }  
        }  
        while(1)  
        {  
            RD(x);  
            if(x==0)  
            {  
                break;  
            }  
            sum=0;  
            For(0,x,i)  
            {  
                RD(p);  
                sum^=dfs(p);  
            }  
            if(sum!=0)  
            {  
                printf("WIN\n");  
            }  
            else  
            {  
                printf("LOSE\n");  
            }  
        }  
    }  
    return 0;  
}  

POJ2068 Nim
题意是圆桌上有2n个人,奇数一队,偶数一队,每个人都有一个拿走棋子的最高限额,问你最后1对能否获胜。
还是用强大的sg函数过的,记录下每个状态的sg。

[cpp]  
#include<iostream>  
#include<cstdio>  
#include<algorithm>  
#include<cstring>  
#include<string>  
#include<cmath>  
#include<set>  
#include<vector>  
#include<stack>  
#include <queue>  
#include<map>  
#define mem(a,b) memset(a,b,sizeof(a))  
#define FOR(a,b,i) for(i=a;i<=b;++i)  
#define For(a,b,i) for(i=a;i<b;++i)  
using namespace std;  
inline void RD(int &ret)  
{  
    char c;  
    do  
    {  
        c=getchar();  
    }  
    while(c<'0'||c>'9');  
    ret=c-'0';  
    while((c=getchar())>='0'&&c<='9')  
    {  
        ret=ret*10+(c-'0');  
    }  
}  
inline void OT(int a)  
{  
    if(a>=10)  
    {  
        OT(a/10);  
    }  
    putchar(a%10+'0');  
}  
int n,s,m[22],sg[22][8200],sum;  
int dfs(int x,int y)  
{  
    if(sg[x][y]!=-1)  
    {  
        return sg[x][y];  
    }  
    int i,j;  
    FOR(1,m[x],i)  
    {  
        if(y-i<0)  
        {  
            break;  
        }  
        if(x+1>=2*n)  
        {  
            j=0;  
        }  
        else  
        {  
            j=x+1;  
        }  
        if(dfs(j,y-i)==0)  
        {  
            return sg[x][y]=1;  
        }  
    }  
    return sg[x][y]=0;  
}  
int main()  
{  
    int i;  
    while(1)  
    {  
        RD(n);  
        if(n==0)  
        {  
            break;  
        }  
        RD(s);  
        For(0,2*n,i)  
        {  
            RD(m[i]);  
        }  
        mem(sg,-1);  
        FOR(0,2*n,i)  
        {  
            sg[i][0]=1;  
        }  
        sum=dfs(0,s);  
        if(sum==0)  
        {  
            printf("0\n");  
        }  
        else  
        {  
            printf("1\n");  
        }  
    }  
    return 0;  
}  

POJ3537 Crosses and Crosses
题意:给出一个1*n的矩形,上面有n个方格,现有两人分别在上面画×,谁先能画出三个×相连就赢了。这就是一个sg函数的母问题转化为子问题的题目,由于在第x位置画了×后,则就转变成(x-3)个格子画×和(n-x-2)个格子画×。。。这就能不断的分解下去,最后将所有sg值异或起来就是正解了。
[cpp]

#include<iostream>  
#include<cstdio>  
#include<algorithm>  
#include<cstring>  
#include<string>  
#include<cmath>  
#include<set>  
#include<vector>  
#include<stack>  
#include <queue>  
#include<map>  
#define mem(a,b) memset(a,b,sizeof(a))  
#define FOR(a,b,i) for(i=a;i<=b;++i)  
#define For(a,b,i) for(i=a;i<b;++i)  
using namespace std;  
inline void RD(int &ret)  
{  
    char c;  
    do  
    {  
        c=getchar();  
    }  
    while(c<'0'||c>'9');  
    ret=c-'0';  
    while((c=getchar())>='0'&&c<='9')  
    {  
        ret=ret*10+(c-'0');  
    }  
}  
inline void OT(int a)  
{  
    if(a>=10)  
    {  
        OT(a/10);  
    }  
    putchar(a%10+'0');  
}  
int sg[2001];  
int dfs(int x)  
{  
    if(x<0)  
    {  
        return 0;  
    }  
    if(sg[x]>=0)  
    {  
        return sg[x];  
    }  
    int i,y;  
    bool v[2001]={false};  
    FOR(1,x,i)  
    {  
        y=dfs(i-3)^dfs(x-i-2);//找后继(经典)  
        v[y]=true;  
    }  
    for(i=0;;i++)  
    {  
        if(v[i]==false)  
        {  
            return sg[x]=i;  
        }  
    }  
}  
int main()  
{  
    int n,sum;  
    mem(sg,-1);  
    while(scanf("%d",&n)!=EOF)  
    {  
        sum=dfs(n);  
        if(sum)  
        {  
            printf("1\n");  
        }  
        else  
        {  
            printf("2\n");  
        }  
    }  
    return 0;  
}  

POJ2599 A funny game
记忆化搜索,这题的博弈味道不浓,更多的是搜索。题意是给一个图,两人轮流移动,走过的节点不能再走。水题,dfs+标记就行。
[cpp]

#include<iostream>  
#include<cstdio>  
#include<algorithm>  
#include<cstring>  
#include<string>  
#include<cmath>  
#include<set>  
#include<vector>  
#include<stack>  
#define mem(a,b) memset(a,b,sizeof(a))  
#define FOR(a,b,i) for(i=a;i<=b;++i)  
#define For(a,b,i) for(i=a;i<b;++i)  
#define N 1000000007  
using namespace std;  
inline void RD(int &ret)  
{  
    char c;  
    do  
    {  
        c=getchar();  
    }  
    while(c<'0'||c>'9');  
    ret=c-'0';  
    while((c=getchar())>='0'&&c<='9')  
    {  
        ret=ret*10+(c-'0');  
    }  
}  
inline void OT(int a)  
{  
    if(a>=10)  
    {  
        OT(a/10);  
    }  
    putchar(a%10+'0');  
}  
int n,v[1001][1001],vis[1001];  
int dfs(int x)  
{  
    int i;  
    FOR(1,n,i)  
    {  
        vis[x]=1;  
        if(v[i][x]&&!vis[i])  
        {  
            if(!dfs(i))  
            {  
                vis[x]=0;  
                return i;  
            }  
        }  
        vis[x]=0;  
    }  
    return 0;  
}  
int main()  
{  
    int m,i,a,b;  
    while(scanf("%d%d",&n,&m)!=EOF)  
    {  
        mem(v,0);  
        mem(vis,0);  
        FOR(1,n-1,i)  
        {  
            RD(a);  
            RD(b);  
            v[a][b]=v[b][a]=1;  
        }  
        i=dfs(m);  
        if(i!=0)  
        {  
            printf("First player wins flying to airport %d\n",i);  
        }  
        else  
        {  
            printf("First player loses\n");  
        }  
    }  
    return 0;  
}

组合博弈的通解就是sg函数,学习了sg函数之后一直没有咋用过。

学习博弈的可以在nyoj上面做10道取石子题目,作为了对博弈也就有一定理解了。

用的时候注意初始的时候只要初始sg[0]=0;

其他都通过函数求解。

这里贴一个求解sg函数的模板。

int sg[N];
bool hash[N];
void sg_solve(int *s,int t,int N)   //N求解范围 S[]数组是可以每次取的值,t是s的长度。
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=N;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=0;j<t;j++)
            if(i - s[j] >= 0)
                hash[sg[i-s[j]]] = 1;
        for(j=0;j<=N;j++)
            if(!hash[j])
                break;
        sg[i] = j;
    }
}

用set容器实现的方法,原理一样。oj上容易超时

void sg_solve()
{
    memset(sg,0,sizeof(sg));
    for(int i=1;i<N;i++)
    {
        set<int> v;
        for(int j=0;j<t;j++)
            if(i - s[j] >= 0)
                v.insert(sg[i-s[j]]);
        int g=0;
        while(v.count(g)!=0)
            g++;
        sg[i]=g;
    }
}

通过一道题目说一下。
hdoj 1536 和pku 2960 S-Nim

题意就是给出一个数组s。为每次可以取石子的数目。

然后给你n堆石子每堆si。求解先手能不能赢!标准的sg函数用法题目。

代码:

#include<stdio.h>
#include<string.h>
#include <string>
#include <iostream>
using namespace std;

const int N = 10008;
int s[108],t;
int sg[N];
bool hash[N];
void sg_solve(int *s,int t,int N)   //N求解范围 S[]数组是可以每次取的值,t是s的长度。
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=N;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=0;j<t;j++)
            if(i - s[j] >= 0)
                hash[sg[i-s[j]]] = 1;
        for(j=0;j<=N;j++)
            if(!hash[j])
                break;
        sg[i] = j;
    }
}

int main()
{
    int i,j,n,m,h;
    while(scanf("%d",&t),t)
    {
        string ans="";
        for(i=0;i<t;i++)
            scanf("%d",&s[i]);
        sg_solve(s,t,N);
        scanf("%d",&n);
        for(i=0;i<n;i++)
        {
            scanf("%d",&m);
            int res = 0;
            for(j=0;j<m;j++)
            {
                scanf("%d",&h);
                res ^= sg[h];
            }
            ans+=res?'W':'L';
        }
        cout<<ans<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值