找出游戏的必胜的策略(博弈论的学习)

题目:硬币游戏1,Alice和Bob在玩这样一个游戏。给定k个数字a1,a2,···ak。 一开始,有x枚硬币,Alice和Bob轮流取硬币。每次所取硬币的枚数

一定要在a1,a2···,ak当中。Alice先取,取走最后一枚硬币的一方获胜。当双方都采取最有策略时,谁会获胜?假定a1a2···ak中一定有1

限制条件:1<=x<=10000 1<=k<=100 1<=ai<=k

样例:

输入

x=9

k=2

a={1,4}

输出

Alice

样例2

x=10

k=2

a={1,4}

输出

Bob

下面考虑轮到自己的时,还有j枚硬币的情况

1、题目规定取光所有硬币就获胜,这等价于轮到自己时如果没有了硬币就失败了。因此,j=0时是必败态

2、如果对于某个i(1<=i<=k),j-ai是必败态的话,j就是必胜态。(如果当前有j枚硬币,只要取走ai枚,对手就必败->自己必胜)

3、如果对于任意的i(1<=i<=k),j-ai都是必胜态的话,j就是必败态(不论怎么取,对手都必胜->自己必败)

根据上面这些规则,我们利用动态规划算法按照j从小到大的顺序计算必胜态必败态。只要看x是必胜态还是必败态,我们就知道谁会获胜了

像这样,通过考虑各个状态的胜负条件,判断必胜态和必败态,是有胜败的游戏的基础

看代码

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
#define INF 0x3f3f3f
int main()
{
    bool win[10005];
    int x,k;
    int a[110];
    cin>>x>>k;
    for(int i=0;i<k;i++)
        cin>>a[i];
    win[0]=false;//0枚硬币必败
    for(int i=1;i<=x;i++)
    {
        win[i]=false;//先初始化为为必败态
        for(int j=0;j<k;j++)
        {
            win[i]|=a[j]<=i&&!win[i-a[j]];//异或运算,有一个为必败态则为必胜态
        }
    }
    if(win[x])
        cout<<"Alice"<<endl;
    else
        cout<<"Bob"<<endl;
    return 0;
}

 

2、A Funny Game

题目:n枚硬币排成一个圈。Alice和Bob轮流从中取一枚或两枚硬币。不过,取两枚时,所取的两枚硬币必须是连续的。硬币取走之后留下空位

,相隔空位的硬币视为不连续。Alice开始先取,取走最后一枚硬币的一方获胜。当双方都采取最优策略时,谁会获胜

0<=n<=1000000

输入

n=1

输出

Alice

输入

n=3

输出

Bob

n高达1000000,考虑到还有将连续部分分裂成几段等的情况,状态数非常的多,搜索和动态规划都难以胜任。需要更加巧妙地判断胜败关系

首先,试想一下如下情况。能够把所有硬币分解成两个完全相同的状态,是必败态还是必胜态呢?

事实上,是必败态。不论自己采取何种策略,对手是要在另一组采取相同的策略,就又回到了分成两个相同的组的状态了

不断循环下去,总会轮到自己没有硬币了。也就是说,因为对手取走了最后一枚硬币而败北

接下来,回到正题。Alice在第一步取走了一枚或者两枚硬币之后,原本成圈的硬币变成了长度为n-1或者n-2的链。这样只要Bob在中间位子

根据链长的奇偶性,取走一枚或者两枚硬币,就可以把所有硬币正好分为两个长度相同的链

这也正如我们前面说的必败态。也就是说Alice必败,Bob必胜,只不过当n<=2时,Alice可以在第一次取光,所以胜利的是Alice。在这类游戏中

做出对称的状态再完全模仿对手的策略常常是有效的

看代码

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
#define INF 0x3f3f3f
int main()
{
    int n;
    cin>>n;
    if(n<=2)
        cout<<"Alice"<<endl;
    else
        cout<<"Bob"<<endl;
    return 0;
}

 

3、Euclid's Game

让我们看一下这个以辗转相除法为基础的游戏

给定两个整数a,b。Stan和Ollie轮流从较大的数字中减去较小数字的倍数。这里的倍数是指1倍,2倍这样的正整数倍,并且相减后的结果不能

小于零。Stan先手,在自己的回合将其中一个数变为0的一方获胜。当双方都采取最优策略时,谁会获胜?

输入

a=64,b=12

输出

Stan wins

输入

a=15,b=24

输出

Ollie wins

让我们来找找看该问题中必胜态和必败态。首先,如果a>b则交换,假设a<b。另外,如果b已经是a的倍数了则必胜,所以假设b并非a的倍数

此时,a和b的关系按照自由度的观点。可以分为以下两类

b-a<a的情况

b-a>a的情况

对于第一种情况,只能从b中减去a,没有选择的余地。相对的,对于第二种情况,有从b中减去a,减去2a,或者更高的倍数的情况

对于第一种情况,要判断必胜还是必败并不难。因为没有选择的余地,如果b-a之后所得状态是必败态的话,他就是必胜态,如果得到的是必胜态的话,它就是必败态

例如,从(4,7)这个状态出发就完全没有选择的余地,按照

(4,7)->(4,3)->(1,3)的顺序,轮到(1,3)的一方将获胜

所以有必胜->必败->必胜 可见(4,7)是必胜态

接下来,我们来看第二种情况是必胜态还是必败态。假设x是使得b-ax<a的整数,考虑一下,从b中减去a(x-1)的情况。例如对于(4,19)则减去12

此时,接下来的状态成了前边讲过的没有选择的情况,如果改状态是必败的话,则当前状态就是必胜态。

那么,如果减去a(x-1)后的状态是必胜态的话,该如何是好?  此时,从b中减去ax后的状态就是减去a(x-1)后的状态唯一可以转移到的状态,根据假设,减去a(x-1)是必胜态,所以该状态是必败态。因此是必胜态

由此可知,对于第二种情况,总是必胜的。所以,从初始状态开始,最先到达有自由度的第二种状态的一方必胜

看代码

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
#define INF 0x3f3f3f
int a,b;
void solve(int a,int b)
{
    bool f=true;
    while(1)
    {
        if(a>b)
            swap(a,b);
        if(b%a==0)
            break;
        if(b-a>a)
            break;
        b-=a;
        f=!f;
    }
    if(f)
        cout<<"Stan wins"<<endl;
    else
        cout<<"Ollie wins"<<endl;
}
int main()
{
    cin>>a>>b;
    solve(a,b);
    return 0;
}

 4、Nim游戏

算法树上有点简略,没有看懂,参考资料:https://baike.baidu.com/item/Nim游戏/6737105?fr=aladdin

题目大意:有n堆石子,每堆石子有a[i]个。Alice和Bob轮流从非空的石子中取走至少一颗石子。Alice先取,取光所有石子的一方获胜。当双方都采取最优策略时

,谁会获胜?

限制条件:1<=n<=1000000   1<=ai<=10^9

样例:

输入

n=3

a={1,2,4}

输出:

Alice

让我们来看看这个游戏,该游戏的策略也成为了许多游戏的基础。要判断该游戏的胜负只要用异或运算就好了。有以下结论:

a1^a2^...^an!=0    必胜态

a1^a2^...^an==0  必败态

因此,只要计算异或值就知道谁胜了

分析一下为什么是这样的:

有三种情况:

1、无法进行移动     那么它就是必败态

2、可以移动到必败态        那么它就是必胜态

3、所有的移动都导致必胜态    那么它就是必败态

第一个命题显然,无法进行移动只有一个,就是全0,异或仍然是0。

第二个命题,对于某个局面(a1,a2,...,an),若a1^a2^...^an<>0,一定存在某个合法的移动,将ai改变成ai'后满足a1^a2^...^ai'^...^an=0。不妨设a1^a2^...^an=k,则一定存在某个ai,它的二进制表示在k的最高位上是1(否则k的最高位那个1是怎么得到的)。这时ai^k<ai一定成立。则我们可以将ai改变成ai'=ai^k,此时a1^a2^...^ai'^...^an=a1^a2^...^an^k=0。

第三个命题,对于某个局面(a1,a2,...,an),若a1^a2^...^an=0,一定不存在某个合法的移动,将ai改变成ai'后满足a1^a2^...^ai'^...^an=0。因为异或运算满足消去率,由a1^a2^...^an=a1^a2^...^ai'^...^an可以得到ai=ai'。所以将ai改变成ai'不是一个合法的移动。证毕。

看代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const ll maxa=32050;
#define INF 0x3f3f3f3f3f3f
ll n;
ll a[maxn];
void solve()
{
    ll x=0;
    for(int i=0;i<n;i++)
        x^=a[i];
    if(x!=0)
        cout<<"Alice"<<endl;//总存在一个变化使得x==0,使得它为必败态,所以它本身是必胜态
    else
        cout<<"Bob"<<endl;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>a[i];
    solve();
    return 0;
}

 

题目大意:排成直线的格子上放有n个棋子。棋子i放在左数第a[i]个格子上。Georgia和Bob轮流选择一个棋子向左移动。每次可以移动一个或者任意多格】

但是不允许反超其他的棋子,也不允许将两个棋子放在同一个格子内。无法进行移动的失败。假设Georgia先移动,当双方都采取最优策略时,谁会获胜?

限制条件:1<=n<=1000 1<=a[i]<=10000

输入

3

1 2 3

输出

Bob

输入

8

1 5 6 7 9 12 14 17

输出

Georgia

思路:如果将棋子两两成对当做整体进行考虑,我们就可以把这个游戏转化为Nim游戏了。先按棋子个数分奇偶情况讨论。

我们可以将每对棋子看作Nim中的一堆石子。石子堆中石子的个数等于两个棋子的间隔、

让我们看一下为什么可以这样转化。考虑其中的某一对石子,将右边的棋子向左移动就相当于从Nim的石子堆中取走石子,另一方面,将左边的石子向左移动,就相当于增加石子。这就与Nim游戏不同了。但是,即便对手增加了石子数量,只要将所加部分减回去就回到原来的状态了。所以这个游戏的胜负和Nim游戏胜负是一样的

看代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=2e5+10;
const ll maxa=32050;
#define INF 0x3f3f3f3f3f3f
int main()
{
    int n,x=0;
    int a[1050];
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    if(n%2==1)
        a[n++]=0;
    sort(a,a+n);
    for(int i=1;i<n;i++)
    {
        x^=(a[i]-a[i-1]-1);
    }
    if(x==0)
        cout<<"Bob"<<endl;
    else
        cout<<"Georgia"<<endl;
    return 0;
}

 5、硬币游戏2

Alice和Bob在玩这样一个游戏。给定k个数字q1,a1···ak。一开始,有n堆硬币,每堆各有xi枚硬币。Alice和Bob轮流选出其中一堆硬币,从中取出硬币。每次所取i硬币的枚数一定要在a1,a2

···ak当中。Alice先取,取光硬币的一方获胜。当双方都采取最优策略时谁会获胜?保证a1,a2···ak一定有个1

限制条件:1<=n<=1000000   1<=k<=100 1<=xi,ai<=10000

输入

n=3

k=3

a={1,3,4}

x={5,6,7}

输出

Alice

这和我们之前介绍的硬币问题1相似,只不过那道题中只有一堆硬币,而本题有n堆。如果依然用动态规划的话,状态数高达O(x1x2···xn)

在此,为了高效的求解该问题,引出Grundy值这一重要概念。利用它,不光这个游戏,其他许多游戏都可以转化成前面所介绍的Nim

让我们再来考虑一下只有一堆硬币的情况,qy硬币枚数x所对应的Grundy值的计算方法如下。

int grundy(x)

{

  集合S={}

 for(j=1...k)

 if(a[j]<=x])  将grundy(x-a[j])加入到S中

 return 最小的不属于S的非负整数

}

也就是说这样的Grundy值就是除了自己走任意一步所能到达的状态的Grundy值以外的最小非负整数。这样的Grundy值,和Nim游戏中的一个石子堆类似,有如下性质

Nim中有x颗石子的石子堆,能够转移成有0,1,···x-1颗石子的石子堆

从Grundy值为x的状态出发,能够转移到Grundy值为0,1,x-1的状态

只不过,与Nim不同的是,转移后的Grundy值也有可能增加。不过,对手总能够选取合适的策略回到相同的Grundy值的状态。,所以对胜负没有影响。

了解了一堆硬币Grundy值的计算方法之后,就可以将它看作Nim中的一个石子堆。

下面用的动态规划的方法,复杂度为O(xk)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const int maxk=100+10;
const int maxx=1e4+10;
const ll maxa=43200;
#define INF 0x3f3f3f3f3f3f
int n,k,a[maxn],b[maxk];
int grundy[maxx];
void solve()
{
    //轮到自己时剩下0枚必败
    grundy[0]=0;
    int max_a=*max_element(a,a+n);//求取最大元素
    for(int i=1;i<=max_a;i++)
    {
        set<int> s;
        for(int j=0;j<k;j++)
        {
            if(b[j]<=i)
                s.insert(grundy[i-b[j]]);
        }
        int g=0;
        while(s.count(g)!=0)//count用来判断g出现的次数,在这里只有0和1之分
        g++;
        grundy[i]=g;
    }
    //判断胜负
    int x=0;
    for(int i=0;i<n;i++)
    x^=grundy[a[i]];
    if(x!=0)
        cout<<"Alice"<<endl;
    else
        cout<<"Bob"<<endl;
}
int main()
{
    cin>>n>>k;
    for(int i=0;i<n;i++)
        cin>>a[i];
    for(int i=0;i<k;i++)
        cin>>b[i];
    solve();
    return 0;
}

  

6、两个人在玩如下游戏

准备一张分成w*h的格子的长方形纸张,两人轮流切割纸张。要沿着格子的边界切割,水平或者垂直的将纸张切成两部分。切割了n次之后就得到了n+1张纸,每次选择切得的某一张

再进行切割。首先切出只有一个格子的纸张的一方获胜。当双方都采取最优策略时,先手必胜还是必败

限制条件

2<=w,h<=200

这道题也能用Grundy值来计算。当w*h的纸张分成两张时,假设所分得的纸张的Grundy值分别为g1,g2,则这两张纸对应的状态的Grundy值可以表示为g1^g2

看代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const int maxk=100+10;
const int maxx=1e4+10;
const ll maxa=43200;
#define INF 0x3f3f3f3f3f3f
int a[210][210];
int mem[210][210];
int grundy(int w,int h)
{
    if(mem[w][h]!=-1)
        return mem[w][h];
    set<int> s;
    for(int i=2;w-i>=2;i++)
        s.insert(grundy(i,h)^grundy(w-i,h));
    for(int i=2;h-i>=2;i++)
        s.insert(grundy(w,i)^grundy(w,h-i));
    int g=0;
    while(s.count(g)!=0)
        g++;
    return mem[w][h]=g;
}
void solve(int w,int h)
{
    if(grundy(w,h)!=0)
        cout<<"WIN"<<endl;
    else
        cout<<"LOSE"<<endl;
}
int main()
{
    int w,h;
    memset(mem,-1,sizeof(mem));
    cin>>w>>h;
    solve(w,h);
    return 0;
}

 

转载于:https://www.cnblogs.com/caijiaming/p/9313671.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值