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