博弈练习&总结

文章PDF链接http://pan.baidu.com/share/link?shareid=138959164&uk=1913509805

在前面的话

博弈一类的解题报告,许多都是给你个结论,包括博弈基础的三大经典模型,许多人也是知道结论,但是不知道为什么是这个结论,也就是知其然,不知其所以然。虽然一些结论,给出了证明过程,使我们知道了所以然,但是我们仍然只知道此一结论,题目可以千变万化,我们能把所有的结论知道吗?显然不能,所以我们需要知道结论的由来,大则可以是定理的发现过程,小则是我们解题的过程,思路。

我们每做一道题,就是一个探索的过程,由不会做到会做,由不知到知。这个过程很难用文字表达出来,或许很多人也懒得表达。一些人只顾做题,却忽略了总结,其实总结是知识归纳和经验积累的最好途径,当然也是创新的基础。

所以我提倡,解题报告不要只给解,应该详细的把思路呈现出来,让别人知道你是怎么想的,否则那终究是填鸭式教育,就像许多人做DP一样,会做的是会做,不会做的仍然是不会做,即使是你当时会做,那么过一段时间等记忆模糊了再做,你就不一定能解出答案。但如果你具备解题的这种能力,那么就算是来个新题,也不会手足无措,无从下手。

但是有时候思路是很难详细记录的,一个题的解出(尤其像博弈一类),思路(由不知到知)也是一个漫长的过程,举一反三是很难做到的。所以我们能做什么的,或许就是多做几道题,总结一下,大致分为几类,然后标记一些不容易想到的地方,为以后做此类题,少走弯路。

这次写的一些东西,也只是告诉大家我是怎么想的,但我是怎么“这样想”的。其中一些是,顺藤摸瓜,究果索因;还有一些是经验和直觉,根据现象找本质。当然有些东西妙不可言,是只能意会不能言传,所以我这里献出的仅仅是一些糟粕而已,真正的精华需要读者自行试之,悟之。

2147 kiki's game 2

2188 悼念512汶川大地震遇难同胞——选拔志愿者 3

1846 Brave Game 3

1517 A Multiplication Game 4

1847 Good Luck in CET-4 Everybody! 4

3863 No Gambling 6

1536 S-Nim 6

1527 取石子游戏 8

1850 Being a Good Boy in Spring Festival 8

2149 Public Sale 9

1730 Northcott Game 10

1079 Calendar Game 10

1848 Fibonacci again and again 11

3951  Coin Game 12

1564 Play a game 13

2516 取石子游戏 13

1849 Rabbit and Grass 14

1729 Stone Game 14

1907 John 15

1404 Digital Deletions 16

1525 Euclid's Game 18

2954 Marble Madness 19

2176 取(m堆)石子游戏 19

1524 A Chess Game 20

1760 A New Tetris Game 21

总结 23

2147 kiki's game

简单的巴什博弈(bash game)详见《博弈基础知识总结》。

#include 
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF && (n || m))
    {
        puts((n*m)&1?"What a pity!":"Wonderful!");
    }
    return 0;
}

2188 悼念512汶川大地震遇难同胞——选拔志愿者

同样是巴什博弈这个经典模型,只不过原来是“取石子”,现在是“加石子”。

#include 
int main()
{
    int n,m,c;
    while(scanf("%d",&c)!=EOF)
    {
        while(c--)
        {
            scanf("%d%d",&m,&n);
            puts((m%(n+1))?"Grass":"Rabbit");
        }
    }
    return 0;
}

1846 Brave Game

还是巴什博弈,不解释。。。

#include 
int main()
{
    int n,m,c;
    scanf("%d",&c);
    while(c--)
    {
        scanf("%d%d",&m,&n);
        puts((m%(n+1))?"first":"second");
    }
    return 0;
}

1517 A Multiplication Game

题目给的范围比较大(1 < n < 4294967295),显然不能直接写SG函数,只能找到SG函数的规律。

该题按部就班的推导即可,可以发现,[1,9]为必胜,(9,18)为必败,(18,18*9]为必胜,(18*9,18*18]必败,依次类推……需要注意,这里需要用double,但是不能用int,否则结果可能因丢失精度而出错。

#include 
int main()
{
    double n;
    while(scanf("%lf",&n)!=EOF)
    {
        while(n>18)
            n/=18;
        puts(n>9?"Ollie wins.":"Stan wins.");
    }
    return 0;
}

1847 Good Luck in CET-4 Everybody!

由于SG的范围比较小,只有1000,而且每次规则不变,所以可以写一个初始化SG函数,然后非递归的从0开始正向推出所有SG值即可,可套模板。如果你懒得手推就可这样写,前提是SG函数比较规范化,容易写。做完后一打表发现规律很明显。

#include 
#include 
const int M=1010;
int sg[M],F[15];
bool b[M];
void Init()
{
    int i,j,t;
    sg[0]=0;
    t=1;
    for(i=0;;i++)
    {
        F[i]=t;
        t*=2;
        if(t>1000)
            break;
    }
    t=i;
    for(i=1;i<=M-10;i++)
    {
        memset(b,0,sizeof(b));
        for(j=0;j=F[j];j++)
            b[sg[i-F[j]]]=1;
        for(j=0;;j++)
        {
            if(!b[j])
            {
                sg[i]=j;
                break;
            }
        }
    }
}
int main()
{
    int n;
    Init();
    while(scanf("%d",&n)!=EOF)
    {
        puts(sg[n]?"Kiki":"Cici");
    }
    return 0;
}
OR
#include 
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        puts(n%3?"Kiki":"Cici");
    }
    return 0;
}

3863 No Gambling

这个题是各自操作自己的棋子,所以根据定义不算博弈,所以其结论皆不可用,这里可试着推导几个,你会发现先手必胜,这说明了两句俗语,那即是“先下手为强”,还有“棋输一步”。

#include 
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF && n!=-1)
    {
        puts("I bet on Oregon Maple~");
    }
    return 0;
}

1536 S-Nim

典型的模板题,不解释,可以排下序用作剪枝,由于每次规则不同,再加上可能也用不到那么大数的SG值,所以只需一步一步向前递推求值即可,每次记录SG值可提高效率。

#include 
#include 
#include 
int a[100],k,sg[10001];
int cmp(const void *a,const void *b)
{
    return (*(int *)a)-(*(int *)b);
}
int SG(int t)
{
    bool b[100];
    memset(b,0,sizeof(b));
    int i,p;
    for(i=0;i

1527 取石子游戏

威佐夫博奕(Wythoff Game),直接套结论,详细结论分析过程可看《博弈基础知识总结》,这里需要拐一点弯,如果每次从i=1开始遍历寻找必败点,会T的很惨。

#include 
#include 
#include 
using namespace std;
int main()
{
    int a,b,i;
    while(scanf("%d%d",&a,&b)!=EOF)
    {
        if(a>b)
            swap(a,b);
        i=b-a;
        if(a==(int)(i*(1+sqrt(5.0))/2))
            puts("0");
        else puts("1");
    }
    return 0;
}

1850 Being a Good Boy in Spring Festival

简单的Nim博弈,但这里需要给出必胜策略的走法,也就是怎么执行一步后可以使Nim-Sum为0,这里需要知道若a^b=c,则b^c=a;如果b变成b^c,那么,a^(b^c)=0,这是两堆的情况;多堆可自行证明。所以若b>=(b^c),那么这就是取这堆可使之败。

#include 
int main()
{
    int M,i,s,t,a[101];
    while(scanf("%d",&M)!=EOF && M)
    {
        t=s=0;
        for(i=0;i=(s^a[i]))
                {
                    t++;
                }
            }
        }
        printf("%d\n",t);
    }
    return 0;
}

2149 Public Sale

简单的巴什博弈(bash game)详见《博弈基础知识总结》。

#include 
int main()
{
    int i,n,m;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        if(m>=n)
        {
            for(i=n;i

1730 Northcott Game

该题是Nim的变形,模型化亦可理解为有n堆石子(n为行数),石子数量即为坐标差的绝对值减1,唯一不同的是,这堆石子数貌似可以加,当然在有限的范围内。让我们看看这会不会影响结果,若当前是必败点即Sum-Nim=0;如果向反方向移动令石子数“增加”,对手依然可以取石子至原来的状态,依然是必败点,如果是必胜点,那直接下手就行了,不必后退了,对吧。

#include 
#include 
int main()
{
    int n,a,b,F;
    while(scanf("%d%*d",&n)!=EOF)
    {
        F=0;
        while(n--)
        {
            scanf("%d%d",&a,&b);
            F^=(abs(a-b)-1);
        }
        if(F)
            puts("I WIN!");
        else puts("BAD LUCK!");
    }
    return 0;
}

1079 Calendar Game

这个题看上去貌似很麻烦,但是你得老老实实的推导,会柳暗花明的,写一个月份日历表,然后表明N,P点即可解除,需注意的是两处特殊情况。

#include 
int main()
{
    int T,m,d;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%*d%d%d",&m,&d);
        if(((m+d)&1)==0 ||(d==30 && (m==11||m==9)))
            puts("YES");
        else puts("NO");
    }
    return 0;
}

1848 Fibonacci again and again

典型的模板题,不过多解释。

#include 
#include 
const int M=1010;
int SG[M],F[M];
bool b[M];
void Init()
{
    int i,j,Cnt;
    F[1]=1;F[2]=2;
    SG[0]=0;
    for(i=3;;i++)
    {
        F[i]=F[i-1]+F[i-2];
        if(F[i]>M)
            break;
    }
    Cnt=i;
    for(i=1;i<=M-10;i++)
    {
        memset(b,0,sizeof(b));
        for(j=1;j=F[j];j++)
            b[SG[i-F[j]]]=1;
        for(j=0;;j++)
        {
            if(!b[j])
            {
                SG[i]=j;
                break;
            }
        }
    }
}
int main()
{
    int m,n,p;
    Init();
    while(scanf("%d%d%d",&m,&n,&p)!=EOF && (m||n||p))
    {
        if((SG[m]^SG[n]^SG[p])!=0)
            puts("Fibo");
        else puts("Nacci");
    }
    return 0;
}

3951  Coin Game

SG函数不明显,或写着费劲,遂手推。显然在进行第一次动作之后,环就断了,如果最多取一个,可以判断奇偶来定结果,如果K大于1,则第二次操作一定可以把剩下的一堆(这里视作一堆)可以分为同样数目的2堆,第三次怎么操作,第四次只需在另一堆进行与上一次同样的操作即可,这样可以保持到最后,也就是Second必胜。当然如果K>=N,那么还是First必胜的。

#include 
int main()
{
    int i,T,n,m;
    scanf("%d",&T);
    for(i=1;i<=T;i++)
    {
        scanf("%d%d",&n,&m);
        printf("Case %d: ",i);
        if(m>=n)
            puts("first");
        else
        {
            if(m==1)
                puts(n&1?"first":"second");
            else
                puts("second");
        }
    }
    return 0;
}

1564 Play a game

简单推导后发现的简单规律

#include 
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF && n)
        puts(n&1?"ailyanlu":"8600");
    return 0;
}

2516 取石子游戏

根据题意推导了几个,发现好像是斐波那契数列,后来发现真是,但是不知道怎么证明。有待研究。。。

#include 
int F[44]={2,3};
int main()
{
    int n,i;
    for(i=2;i<44;i++)
    {
        F[i]=F[i-1]+F[i-2];
    }
    while(scanf("%d",&n)!=EOF && n)
    {
        for(i=0;i<44;i++)
            if(F[i]==n)
                break;
        if(i==44)
            puts("First win");
        else puts("Second win");
    }
    return 0;
}

1849 Rabbit and Grass

依旧是Nim博弈模型

#include 
int main()
{
    int n,t,i;
    while(scanf("%d",&n)!=EOF && n)
    {
        t=0;
        while(n--)
        {
            scanf("%d",&i);
            t^=i;
        }
        puts(t?"Rabbit Win!":"Grass Win!");
    }
    return 0;
}

1729 Stone Game

当Ci为0,或者Si=Ci时显然对结果没有影响,这里对其他情况的SG值做一些讨论,设当前箱子里的石子数为x,那么一次执行后可以到达的x+x*x的石子数,也就是如果最多为x+x*x=Ci的两个根一正一负,设x1为正,则x1就是一个临界,(x1-1)也一定是必败点,显然可得SG(x1-2)=1;SG(x1-3)=2这样一直到下一个必败点,可以先找到与Si最从右接近的那个临界点,然后求的其SG值。

#include 
#include 
int main()
{
    int N,i=0,t,c,C,s;
    while(scanf("%d",&N)!=EOF && N)
    {
        t=0;
        i++;
        while(N--)
        {
            scanf("%d%d",&c,&s);
            if(s==0 || c==s)
                continue;
            do
            {
                C=c;
                c=(-1+sqrt(1.0+4*c))/2+0.999999;
                c--;
            }while(s

1907 John

这个与Nim游戏唯一的不同是,胜负的判断变了,最后取完的为必败点了。这个与经典的Nim游戏恰恰相反,但是结论就相反么,不一定,我们看看那个万能的异或定理是不是满足这个游戏,如果我们记得Nim结论的证明的话,肯定会发现(详见《博弈论基础知识的一些总结》),需满足三个因素,只有第一条不满足,因为如果剩下一堆为1,这个是在这必败点,但经典的Nim中是必败点,其他两个因素依然满足,所以这个结论依旧差不多,特判一些情况就行了,SG(1)有变化,其他都没变。

#include 
int main()
{
    int T,N,t,s,i,F;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d",&N);
        s=F=0;
        for(i=0;i

1404 Digital Deletions

这道题,我们把每个数字看成一堆,分别求每个SG值有点困难,或者说不可行,因为,几堆之间是有联系的。我们这里采用的是爆搜,一共1000000个数,如果有前导零,则肯定必胜,可作为特例输出。否则分别求SG值,把SG数组初始化全为0,然后,假设a数字可以有b一步的来,如果SG(a)为0,则SG(b)=1;遍历所有点,然后求出SG值为1的,剩下的即是必败点。

#include 
#include 
#include 
const int M=1000000;
bool sg[M]={1};
char a[10];
void Find(int x)
{
    int l,i,j,m,n;
    itoa(x,a,10);
    l=strlen(a);
    for(i=0;i

1525 Euclid's Game

首先上来也是没有头绪的推导测试案例中(25,7),后来你会发现,(a,b)(不失一般性,我们假设a>=b)如果a>=2*b,那么这个点可以到达(a-b,b)还可以到达,(a-2b,b)对吧,这里我们假设(a-2b,b)必败,那么(a-b,b)和(a,b)必胜了,对吧;注意了,当(a-2b,b)必胜时,由于(a-b,b)只能到(a-2b,b),所以其必败,这样,(a,b)可以到达(a-b,b)所以其是不是也必胜了,对的。也就是当a>=2*b时,必胜,当然显而易见,a=b时也是必胜的。否则就只能递归求SG值了,注意,这里没有那么多堆,不用异或,所以直接返回0或1就行了。

#include 
#include 
using namespace std;
bool SG(int n,int m)
{
    if(n>=2*m || n==m)
        return 1;
    else return !SG(m,n-m);
}
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF && (m||n))
    {
        if(n

2954 Marble Madness

两种颜色的球(B,W),三种取法,第一种是两个白的换一个黑的,后面两种都是去一个黑的,有两个黑的可以去,一个黑的和一个白的也可以去,显然,黑的可以一个一个去,白的只能一次取两个,所以,最后剩下黑的白的,取决于白色数的奇偶性。

#include 
int main()
{
    int T,a,b;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&a,&b);
        puts(b&1?"0.00 1.00":"1.00 0.00");
    }
    return 0;
}

2176 取(m堆)石子游戏

1009 Being a Good Boy in Spring Festival

#include 
int a[200000];
int main()
{
    int m,i,t;
    while(scanf("%d",&m)!=EOF && m)
    {
        t=0;
        for(i=0;i=(t^a[i]))
            {
                printf("%d %d\n",a[i],t^a[i]);
            }
        }
    }
    return 0;
}

1524 A Chess Game

也算是模板题吧,只是题挺难读懂的。

#include 
#include 
const int M=1000;
bool a[M][M];
int sg[M],N,X;
int SG(int t)
{
    bool b[100];
    int i;
    memset(b,0,sizeof(b));
    for(i=0;i

1760 A New Tetris Game

也算是一个模板题吧,写一个递归求SG函数,放置完后,递归后手所有可行的点得SG值,如果为0,则当前可返回1,否则为0。

#include 
#include 
char a[50][50];
int n,m;
bool SetJudge(int i,int j)
{
    if(a[i][j]=='0' && a[i][j+1]=='0' && a[i+1][j]=='0' && a[i+1][j+1]=='0')
    {
        a[i][j]=a[i][j+1]=a[i+1][j]=a[i+1][j+1]='1';
        return 1;
    }
    return 0;
}
void Unset(int i,int j)
{
    a[i][j]=a[i][j+1]=a[i+1][j]=a[i+1][j+1]='0';
}
int SG()
{
    int i,j;
    for(i=0;i


总结

博弈可分为几种类型,其中一种为可以手工推出SG函数的,可以直接在代码上写公式,这种题目,给的范围一般比较大,直接给整型范围,肯定不能一点一点的求SG值,那样会超内存或者超时,所以这样题要么很简单,要么就是很难得,思路巨复杂;

还有的得写求SG的代码,SG函数又分为几种,有的是可以写一个初始化Init()分别求出各个SG值,每次用O(1)内就可以给结果,这个前提是,题目给的博弈规则是固定的,还有数据范围不能太大,否则空间和时间都容易超;

还有一种是就是SG函数需要求多次,例如每次取石子的数量是题目现给的,或者SG函数范围不算大,也不算小(一万或十万)时,因为如果太大,这样盲目的多次求SG函数肯定会超时,或者题目输入数据中可能根本用不到那么大数据的SG值。此时需要一个递归的SG函数,开一个数组记录以便提高效率例如HDU_S-Nim;

还有一些小技巧,如果题目就一堆“石子”,显然不用异或,此时只需只求SG是0或1就可以解题了;如果是多堆,一般得求出其每堆的SG(),然后异或,但是有例外,例如HDU_Digital Deletions,起初把他看成若干堆就不容易求,因为这些堆之间是联系的,这里不妨把堆放在一起,直接求出SG(a1,a2……)的值。

这个题给了我们一个提示,我们往常求其SG值时一般看根据执行一次后状态的SG值,如子节点没有0则为1,否则为0,这里给了我们提示,如果当前SG为0,那么凡是可以经过一步到达其的状态皆为必胜。显然这适用于,子节点比较复杂的那种,反其道而行之可能好些。这个思想我们以前就用过,例如求素数,

bool F(int n)

{

    for(i=2;i*i<=n;i++)

        if(n%i==0)

            return 0;

    return 1;

}

上面类比为传统求SG的方法,那下面这个就可以类比为刚才那个思想。

a[1001]={0,1};

for(i=2; i<=1000; i++)

    if(a[i]==0)

        for(j=i+i; j<=1000; j=j+i)

            a[j]=1;

怎么样,其实有些东西是相通的,我们一旦收获一种思想,就要深入的理解,思考,这样以后用时才能想到,迪杰斯特拉就是对贪心的一次应用,发明人为什么是他,这肯定不是偶然。博弈用到递归其实就有些深搜的思想,所以有些题,难免与深搜结合;SG函数本来就是图论的东西过来的,所以博弈与图论的联系是很密切的,你做题时会发现,稿纸上的都是拓扑图,许多东西都是相通,我们一定要多思考。

你可能感兴趣的:(博弈,HDOJ,算法,总结,思考,解题报告)