博弈题解题思路及典例

解题思路与步骤: 

  • 首先明确,博弈是不公平游戏,跟玩家无关,只与当前所出状态有关,即状态确定,结果也就确定了。
  • 需要明确以下几点:
  1. 所有的终结点是必败点(即若当前处于必败点,则无论下一步怎么走,都必输无疑)
  2. 必败点只能走到必胜点(由上必败点定义可以理解,既然这个点是必败点,说明无论它走到哪一步,下一步再走都胜)
  3. 从必胜点操作,一定有一种方法可以到必败点(这点就是利用了博弈的特点推出:所有玩家都是绝顶聪明的,即从当前步就可以窥探结局。所以如果知道从这一可以走到必败点使对方必败,那当然选择这个操作)
  • 由上面的结论,在解博弈题时,从终止点开始推逆向判断每个点是必败点还是必胜点。可以找出这些点的规律解或直接利用递推式推。
  • 有些博弈题也可以用贪心做,双方都选最优的,不让就会被对方选了。

牛客练习赛41 A.翻硬币问题:

theme:Alice与Bob玩翻硬币游 戏,规则:有n个硬币,起始时全都正面朝上。给定偶数m,Alice每轮得从n枚硬币中任意选出m枚翻转,若n枚硬币全部都是反面朝上了,则Alice赢,否则Bob赢。Bob有一项特权:可在任意一轮Alice翻转完后,选择n枚中任一枚翻转,但这项特权只能使用一次,且Alice赢了之后再使用无效,问给定n,m最终谁赢?

solution:博弈问题。考虑Alice什么时候赢:当正面朝上硬币数为m时,这时只需这一轮Alice将这m枚翻转即可(所以n==m时必赢)。而Alice什么时候必输即翻转无数次正面硬币数都不为m呢?首先假设处于正面朝上硬币数为x的状态下,则选m枚时可从x选择中选0~x枚设为i枚,则正面数为x-i,那么就得从n-x枚中选m-i枚,综合后正面数为x-i+m-i=x+m-2i,这个式子即每次选择后的正面数,可看出,若x为奇数,则结果一定是奇数,不可能到m(最终目标),x为偶数时i取x/2即可。所以若Bob没有特权,分奇偶讨论即可,而第一轮(必须)后,x=n-m,无论奇偶,Bob都可以使用特权将其变为偶,所以只要第一轮后Alice没赢,她就必输。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
#define far(i,n) for(int i=0;i<n;++i)
#define fdr(i,n) for(int i=n-1;i>=0;--i)
typedef long long ll;

int main()
{
    int t;
    while(~scanf("%d",&t))
    {
        while(t--)
        {
            ll n,m;
            scanf("%lld%lld",&n,&m);
            if(n==m)
                puts("Yes"); //puts()函数自动输出回车
            else
                puts("No");
        }
    }
}

实例2:

hdu1846:

theme:(输入n,m)两个人轮流从有n个石子的石碓里取石子,每次可取[1,m]个,最先取光石子的人赢,问先取赢还是后取赢?

solution:(考虑第一个人赢为胜)从结论出发:先取光赢,则若开始就有[1,m]个,则第一个人可一次取光,所以n在[1,m]内是必胜;n=m+1时,操作后n'在[1,m]中必胜,所以n=m+1为必败点,而当n再增加一点时,m+1会被包含在n',所以为必胜点、、、可看出n为(m+1)倍数时为必败点,因为下一状态都是必胜点。而不为(m+1)倍数时下一状态包含必败点,所以为必胜点。

//ok 0ms
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using naamespace std;
#define far(i,n) for(int i=0;i<n;++i)
#define fdr(i,n) for(int i=n-1;i>=0;--i)
typedef long long ll;

int main()
{
    int c;
    cin>>c;
    while(c--)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        if(n%(m+1)==0)
            printf("second\n");
        else
            printf("first\n");
    }

}

威佐夫博奕(Wythoff Game) 

有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
int main(){
    int a,b,k,a_k;
    while(scanf("%d%d",&a,&b)!=EOF){
         k = abs(a-b);
         a = a < b? a : b;
         a_k = floor(k*(1.0 + sqrt(5.0))/2);
         printf("%d\n",a!=a_k);
         //输出为0,说明该点为必败点,1为必胜点
    }
    return 0;
}

实例3:斐波拉契博弈

hdu2516

theme:一堆n个石子(2<=n<=2^31),两个人轮流取,第一个人第一次取不能取完,每次取的石子数不能超过对方上次取的两倍,先取完者胜,问谁胜?

solution:n=2时,第一个人必败;

n=3时,第一个人必败;

n=4时,第一人取一个到n=3对方必败,所以为必胜点

n=5时,第一人取一个到n=4对方必胜,所以必败,而取超过1个,则对方可取完,所以为必败点

n=6时,选一个对方必败,所以必胜

n=7时,选两个到n=5可胜

n=8时,选一个到n=7,选两个到n=6对方都是必胜,所以为必败点                                                                                           

 借助“Zeckendorf定理”(齐肯多夫定理):任何正整数可以表示为若干个不连续的Fibonacci数之和。

对于不是斐波那契数,比如分解85成85=55+21+8+1。我们可以把n写成  n = f[a1]+f[a2]+……+f[ap]。(a1>a2>……>ap)

我们令先手先取完f[ap],即最小的这一堆。由于各个f之间不连续,则a(p-1) > ap  + 1,则有f[a(p-1)] > 2*f[ap]。即后手只能取f[a(p-1)]这一堆,且不能一次取完。此时后手相当于面临这个子游戏(只有f[a(p-1)]这一堆石子,且后手先取)的必败态,即后手不能取完这一队,则先手一定可以取到这一堆的最后一颗石子。

同理可知,对于以后的每一堆,先手都可以取到这一堆的最后一颗石子,从而获得游戏的胜利。

//ok 15ms
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
#define far(i,n) for(int i=0;i<n;++i)
#define fdr(i,n) for(int i=n-1;i>=0;--i)
typedef long long ll;

int main()
{
    ll fib[50];
    fib[0]=fib[1]=1;
    for(int i=2;i<50;++i)
        fib[i]=fib[i-1]+fib[i-2];
    int n;
    while(scanf("%lld",&n)&&n)
    {
        int i=2;
        for(;i<50;++i)
            if(n==fib[i])
            {
                printf("Second win\n");
                break;
            }
        if(i==50)
           printf("First win\n");
    }
}
/*
2
13
10000
0

Second win
Second win
First win
*/

实例4:HDOJ1564 Play a game

theme:给定n*n的格子,开始时有一个石子放在角上,每人每次可以上、下、左、右往没放过石子的格子移动石子,无处可走时失败,问最终谁赢?一般做法为用

solution:首先爆搜是不可能的,o(4^n),一般做法为用dfs打表找规律:

//打表代码
const int MAXN = 10010;
bool visit[MAXN][MAXN];
int nx[4] = {0, 1, 0, -1}, ny[4] = {1, 0, -1, 0};
int n;

bool dfs(int x, int y)
{
    for(int i = 0; i < 4; i++)
    {
        int tx = x + nx[i];
        int ty = y + ny[i];
        if(tx < 1 || tx > n || ty < 1 || ty > n)
            continue;
        if(!visit[tx][ty])
        {
            visit[tx][ty] = true;
            bool flag = dfs(tx, ty);
            visit[tx][ty] = false;
            if(!flag)
                return true;
        }
    }
    return false;
}

得出n为偶数先手赢

#include<bits/stdc++.h>
using namespace std;
#define far(i,t,n) for(int i=t;i<n;++i)
typedef long long ll;

int main()
{
    int n;
    while(scanf("%d",&n)&&n)
    {
        if(n&1)
            printf("ailyanlu\n");
        else
            printf("8600\n");
    }
}

实例5:B - Euclid's Gamehdu1525

theme:两个人玩游戏,初始时随机给定两个正整数,每次一人可以选择用大的数减去小的数的倍数,与原来小的数一起作为新的大的数(要求减后的结果要>=0),最后减后有一个数变为0则胜利。

25 7 
11 7 
4 7 
4 3 
1 3 
1 0 

solution:假设a为大的数,b为小的。则随着游戏进行一定会到b,a%b的状态(就算每次只减b的单倍也会到),而到了这一状态就相当于知道最终谁赢了,即该状态是必胜或必败状态(因为接下去就可按照a=b,b=a%b递推下去)。而从a,b到b,a%b,先手是可以选择中间有几步的(他可以选择每次减去b的几倍),所以当a>=2*b时先手必赢(a在b~2b间没有选择),否则进入a=b,b=a%b状态再由终止条件a>=2*b递推(此时先手换人)

#include<bits/stdc++.h>
using namespace std;
#define far(i,t,n) for(int i=t;i<n;++i)
typedef long long ll;

int main()
{
    ll a,b,temp;
    while(~scanf("%lld%lld",&a,&b)&&(a||b))
    {
        int flag=0;
        while(1)
        {
            if(a<b)
                swap(a,b);
            if(a>=2*b||a==b)
                break;
            temp=a;
            a=b;
            b=temp%a;
            flag^=1;
        }
        if(flag)
            printf("Ollie wins\n");
        else
            printf("Stan wins\n");
    }
}

狂赌之渊

theme:给定n堆石子,每堆有s[i]个石子,两个人轮流每次选择一堆并从该堆石子中选出一个石子拿出,拿出某堆最后一个石子的人+1分,直至所有石子被拿完。问先手最多得几分。ai(2≤ai≤10^9)

solution:(博弈题)对于一堆个数为偶数的石子,先手 先选一个之后,如果剩0个,则后手+1分,不然后手也在该堆选一个,则又回到先手面对偶数堆的情况。对于一堆个数为奇数的石子,如果都只在这堆选,则先手赢,但后手可以选择别的奇数堆。所以我们可以统计奇数堆的个数,若为奇数个,则每人都最优选择先手会达到一个奇数堆,其他全为偶数堆的情况,则能加n分,否则对手+n分,先手不加分。

#include<bits/stdc++.h>
using namespace std;
#define far(i,t,n) for(int i=t;i<n;++i)
typedef long long ll;
typedef unsigned long long ull;

int a[100010];

int main()
{
    int n;
    cin>>n;
    int sum=0;
    far(i,0,n)
    {
        scanf("%d",&a[i]);
        if(a[i]&1)
            sum++;
    }
    if(sum&1)
        cout<<n<<"\n";
    else
        cout<<0<<"\n";

}

邂逅明下

theme:多组样例,给定n,p,q代表一堆石子有n个,两人轮流取石子,每次可取的石子数为[p,q],若最后石子数<p,则一次取完,最后取完的人输,问先手必赢还是必输。

solution:巴什博弈扩展。一样地,考虑n%(p+q)

#include<bits/stdc++.h>
using namespace std;
#define far(i,t,n) for(int i=t;i<n;++i)
typedef long long ll;
typedef unsigned long long ull;
using namespace std;

int main()
{
    int n,p,q;
    while(~scanf("%d%d%d",&n,&p,&q))
    {
        if(n%(p+q)==0)
            printf("WIN\n");
        else
        {
            if(n%(p+q)<=p)
                printf("LOST\n");
            else
                printf("WIN\n");
        }
    }
}

贪心:D. Ticket Game

theme:给定长度为n,n为偶数的字符串,字符串由0~9的数字和?组成,?的个数也为0,现两个人轮流从0~9中选一个数字填到任选一个?中,最终填完所有?结束,最终如果前n/2个数的和与后n/2个数的和相同,则后手赢,否则先手赢,问最终谁赢?

solution:首先对于先手,他肯定优先往和大的那边放9,这样可以拉大距离,使得相等更难。如果两边和相等,则优先选?更少的那边放9。对于后手应该是尽量拉近距离,所以应该往小的放差值。

注意如果有一边已经没有?了要修改一下先手的策略,不是都放9,可能放0

#include<bits/stdc++.h>
#include<vector>
#include<algorithm>
using namespace std;
#define far(i,t,n) for(int i=t;i<n;++i)
#define pk(a) push_back(a)
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int inf=0x3f3f3f3f;

char a[200020];

int main()
{
    int n;
    cin>>n;
    scanf("%s",a);
    int cntl=0,cntr=0,suml=0,sumr=0;
    int mid=n/2;
    far(i,0,mid)
    {
        if(a[i]=='?')
            ++cntl;
        else
            suml+=a[i]-'0';
    }
    far(i,mid,n)
    {
        if(a[i]=='?')
            ++cntr;
        else
            sumr+=a[i]-'0';
    }
    int user=1;
    while(cntl||cntr)
    {
        if(user&1)//先手
        {
            if(cntl==0)
            {
                --cntr;
                if(suml-sumr<9)
                    sumr+=9;
            }
            else if(cntr==0)
            {
                --cntl;
                if(sumr-suml<9)
                suml+=9;
            }
            else if(suml==sumr)
            {
                if(cntl>=cntr)//和一样时先手放个数少的
                {
                    --cntr;
                    sumr+=9;
                }
                else
                {
                    --cntl;
                    suml+=9;
                }
            }
            else if(suml<sumr)//先手每次往大的放
            {
                --cntr;
                sumr+=9;
            }
            else
            {
                --cntl;
                suml+=9;
            }
        }
        else//后手
        {
            if(cntl==0)
            {
                --cntr;
                if(suml>sumr)
                    sumr+=min(9,suml-sumr);
            }
            else if(cntr==0)
            {
                --cntl;
                if(sumr>suml)
                    suml+=min(9,sumr-suml);
            }
            else if(suml==sumr)
            {
                if(cntl>=cntr)//和一样时后手放个数多的补0
                    --cntl;
                else
                    --cntr;
            }
            else if(suml<sumr)//后手每次往小的补
            {
                --cntl;
                suml+=min(9,sumr-suml);
            }
            else
            {
                --cntr;
                sumr+=min(9,suml-sumr);
            }
        }
        ++user;
    }
    if(suml==sumr)
        puts("Bicarp");
    else
        puts("Monocarp");
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值