简单组合博弈

本文详细介绍了组合博弈的基本概念,包括必败点和必胜点的性质,并通过实例展示了简单的取子游戏算法实现。此外,还探讨了Nim游戏的nim-sum和SG函数,以及如何通过它们来判断先手的输赢。最后,列举了多个相关问题的代码实现,涵盖了单堆型和多堆型游戏的策略分析。
摘要由CSDN通过智能技术生成

学习篇

组合博弈定义

①有两个玩家
②游戏的操作状态是一个有限集合(比如:限定的总的牌的数量
③游戏双方轮流操作
④双方每次操作符合游戏规定(按规定取牌数量取牌
⑤当一方不能继续进行的时候,游戏结束,同时,对方为获胜方
⑥无论如何操作,游戏总能在有限次数操作后结束

必败点、必胜点(属性很重要 思想!!!

定义:

必败点(P点):前一个选手(Previous player)将取胜的位置称为必败点(即将要在这个点操作的玩家,一定会输

必胜点(N点):下一个选手(Next player)将取胜的位置称为必胜点(在必胜点的玩家不一定赢,是要在不犯错误的情况下,整个游戏才会赢)

属性:

一、所有终结点是必败点(P点);(终结点有限且易标记)
(终结状态:是游戏进行不下去的点;而必败点是游戏可能还能进行下去,改变不了要输的命运)
举个例子:每次取牌规则是两张或三张;则终结点为 剩0张 或 剩1张
所以:必败点不一定是终结点,但终结点一定是必败点

二、从任何必胜点(N点)操作,至少有一种方式可以进入必败点(P点);
因为必胜点不一定怎么走都赢,一定至少一种方式进入必败点
举个例子:剩三张牌,取牌规则(1,2,3),若取了两张或一张,一定是对方赢自己输

三、无论如何操作,从必败点都只能进入必胜点

简单的取子游戏算法实现(组合游戏的一种)

根据以上属性:

步骤一:将所有终结位置标为必败点(P点)(属性一)

步骤二、将所有一步操作能进入必败点(P点)的位置标为必胜点(N点)

步骤三、如果从某点开始的所有一步操作都只能进入必胜点(N点),则标记该点为必败点(P点)

步骤四:如果在步骤三未能找到新的必败点,则算法终止,否则返回到步骤二。

举三个例子:

例子一:一共九张牌 规则(1,2,3)

PNNNPNNNPN
0123456789

例子二:一共16张牌 规则(1,3,4)

PNPNNNNPNPNNNNPNP
012345678910111213141516

例子三:一个n*m的方格图,从左上角开始走,走到不能走的最后一个玩家赢,规则(左走一步,下走1步,左下走1步)

NNNNNNNNN起点
PNPNPNPNPN
NNNNNNNNNN
P(终结点)NPNPNPNPN

Nim游戏

游戏简介:
①有两个玩家
②有三堆扑克牌(比如;5,7,9
③游戏双方轮流操作
④玩家每次操作是选择其中某一堆牌,然后从中取走任意张
⑤最后一次取牌的一方为获胜方

思考问题:
1、判断先手的人输还是赢
2、对于必胜点,有几种可行性操作方案

引入:
Nim-Sum按位的异或运算

定理一:(也适合多堆的情况)
对于nim游戏的某个位置(x1,x2,x3)当且仅当他的各部分的nim-sum等于0是(即x1 ^ x2 ^ x3=0),则当前位于必败点

定理一证明

一、(0,0,0)的nim-sum=0,满足属性一:终结点->必败点

二、属性二:至少有一种方法从必胜点走到必败点
在这里插入图片描述
将 1101 改成 0100 则nim-sum 就由非0(N点)变成0(P点)
并且可以发现 任何非0的nim-sum都可以改成0,因此,满足属性二

怎么改呢?其实学过异或就知道了,没学过也没关系,观察!
观察发现: 三个数对应二进制数的 某个位值的 1的个数 的奇偶性 决定了nim-sum 中该位置是0还是1;比如,nim-sum最高位(第四位)是1;而 13,12,8,最高位(第四位)是1,1,1一共是奇数个

三、同样,nim-sum为0的一定能改成非0的,满足属性三

所以定理一证毕

解决问题上述
1、三堆异或判断,若为0,必输;否则反之

2、可行性方案数 等价于求 改变?堆使得三堆互相异或的nim-sum为0(即必败点)
方法:判断每堆的数量的最高位(此最高位为nim-sum的最高位)为1的数量
原理:想要将nim-sum变成0,则将最高位是1 的那个堆对应的二进制数改一下(先把最高位的1改成0,其余的在改改),从而使得nim-sum为0,不难看出,这一堆的数量一定是变少的,即符合游戏规则,这就是一种方案,求方案数 就是 求有多少堆的最高位是1
(上面的证明已经很清楚了)

SG函数的含义以及实现

sg函数的定义:
X节点的sg值是除去后继点的sg值得最小非负整数。

sg函数的使用:
必败点:节点sg值等于0
必胜点:节点sg值大于0

解决的问题:组合游戏

引入定理:
如果图游戏G有若干个子图游戏Gi组成,即:G=G1+G2+···+Gn,假设gi是Gi(i=1,,,n)的SG函数值,那么,图游戏的SG值计算如下:
g(x1···xn)=g1(x1)^···gn(xn)

举个例子:
·····三堆牌,5,7,9;规则:每次可以取走任意一堆牌的1-3张······问先手的人是输还是赢

则判断sg(5)sg(7)sg(9)是否为0

游戏类型 总结

一、单堆型,求先手输赢:

1、共n张,可以取1-m张
数学方法做:若n%(1+m)==0则先手必败

2、共n张,可以取x1、x2、x3···不连续的,且题目范围是1e2量级的可以用sg函数;若是1e3量级的尽量找规律

二、多堆型

1、求先手输赢

①若规则是:可以取任意一堆的任意张,则该堆的sg值等于该堆的数量,所以不用额外求sg值

②若规则是:取走任意一堆(共n张)的有限张;
若是1-m张类型的,直接 sg值=n/(1+m)
若是不连续类型的规则,就要写一个sg函数

2、求方案数

水题集

hdu 1847:单堆型 取规定集合 求先手输赢

hdu 1847

#include <bits/stdc++.h>
using namespace std;
int n,a[15],sg[1010];

int SG(int x){
    bool v[1010];memset(v,0,sizeof v);
    for(int i=1;i<=11;i++){
        int t=x-a[i];
        if(t<0)break;
        if(sg[t]==-1)sg[t]=SG(t);
        v[sg[t]]=1;
    }
    for(int i=0;;i++){
        if(!v[i])return i;
    }
}


int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int cnt=0;
    for(int i=1;i<=1024;i*=2)a[++cnt]=i;//cnt=11
    while(cin>>n){
        memset(sg,-1,sizeof sg);sg[0]=0;
        if(SG(n))cout<<"Kiki"<<endl;
        else cout<<"Cici"<<endl;
    }
    return 0;
}

hdu 1848:单堆型 取规定集合 求先手输赢

hdu 1848

#include <bits/stdc++.h>
using namespace std;

int a[20],sg[1010],m,n,p;

int SG(int x){
    bool v[1010];memset(v,0,sizeof v);
    for(int i=1;i<=15;i++){
        int t=x-a[i];
        if(t<0)break;
        if(sg[t]==-1)sg[t]=SG(t);
        v[sg[t] ]=1;
    }
    for(int i=0;;i++)if(!v[i])return i;
}



int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    a[1]=1;a[2]=2;
    for(int i=3;i<=16;i++)a[i]=a[i-1]+a[i-2];//a[15]=987
    while(cin>>m>>n>>p){
        if(m==0&&n==0&&p==0)break;
        memset(sg,-1,sizeof sg);sg[0]=0;//这一行放哪都可以;因为SG函数是值传递的,
        int s=SG(m)^SG(n)^SG(p);
        if(s)cout<<"Fibo"<<endl;
        else cout<<"Nacci"<<endl;
    }
    return 0;
}

hdu 1849:多堆型 取任意 求先手输赢

hdu 1849
问题转化:多堆牌,可以任意一堆取任意张

#include <bits/stdc++.h>
using namespace std;

int m,a;
int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);

    while(cin>>m){
        if(m==0)break;
        int s=0;
        while(m--){
            cin>>a;
            s=s^(a);
        }
        if(s)cout<<"Rabbit Win!"<<endl;
        else cout<<"Grass Win!"<<endl;
    }

    return 0;
}

hdu 1850:多堆型 取任意 求方案数

hdu 1850

wa一次:注意异或的优先级低于小于号,记得加括号

#include <bits/stdc++.h>
using namespace std;

int n,k,a[110];

int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    while(cin>>n&&n){
        int s=0;
        for(int i=1;i<=n;i++){
            cin>>a[i];
            s=s^a[i];
        }
        if(s==0){cout<<0<<endl;continue;}
        int cnt=0;
        for(int i=1;i<=n;i++){
            if((s^a[i])<a[i])cnt++;//上面学习篇说过了,若是最高位是1的堆 与 s 异或 则一定小于该堆的量 ,因为最高位 1^1=0
        }
        cout<<cnt<<endl;
    }
    return 0;
}

hdu 2149:单堆型 取连续 求先手操作步数

hdu 2149

#include <bits/stdc++.h>
using namespace std;

int n,m;

int main(){
    while(scanf("%d%d",&m,&n)==2){
        if(m<=n){
            for(int i=m;i<=n;i++){
                    if(i!=m)printf(" ");
                    printf("%d",i);
            }
            printf("\n");
        }
        else {
            int t=m%(n+1);
            if(t==0)printf("none\n");
            else printf("%d\n",t);//这个余数也是走到下一个必败点的步数,应为此题是取连续,所以一定是0 1 2 ···0 1 2··
        }
    }
    return 0;
}

hdu 2188:单堆型 取连续 求先手输赢

hdu 2188

#include <bits/stdc++.h>
using namespace std;

int n,m;

int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int c;cin>>c;
    while(c--){
        cin>>n>>m;
        if(n<=m){cout<<"Grass"<<endl;continue;}
        if(n%(m+1))cout<<"Grass"<<endl;
        else cout<<"Rabbit"<<endl;
    }
    return 0;
}

hdu 1944:多堆型 取规定集合 求先手输赢

hdu 1944

#include <bits/stdc++.h>
using namespace std;

int k,n,a[110],m,sg[10010];//第一次RE是因为sg 和v 数组没开够越界了

int SG(int x){
    bool v[10010];memset(v,0,sizeof v);
    for(int i=1;i<=k;i++){
        int t=x-a[i];
        if(t<0)break;
        if(sg[t]==-1)sg[t]=SG(t);
        v[sg[t]]=1;
    }
    for(int i=0;;i++)if(!v[i])return i;
}

int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    while(cin>>k&&k){
        memset(sg,-1,sizeof sg);sg[0]=0;
        for(int i=1;i<=k;i++)cin>>a[i];
        sort(a+1,a+k+1);
        cin>>n;
        while(n--){
            cin>>m;
            int s=0;
            while(m--){
                int b;cin>>b;
                if(sg[b]==-1)sg[b]=SG(b);
                s=s^sg[b];
            }
            if(s)cout<<"W";
            else cout<<"L";
        }
        cout<<endl;
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值