目录
一、巴什博弈(Bash Game)
只有一堆n个物品,两个人轮流从这堆物品中取物,规 定每次至少取一个,最多取m个。最后取光者得胜。
如果n=m+1,先取者只能取1~m个,所以不论先取者拿走多少个,后取者总能一次性拿走剩余物品。
制胜法则为:令n=k*(m+1)+r(r<=m);若r不为0,那么先取者获胜;否则,后取者胜。
(显然,先取者拿走r个后,剩下n=k*(m+1),不论后取者拿走多少个设为s,先取者只需拿走(m+1-s)个,保证剩下的为(m+1)的倍数,即能确保取胜。)
巴什博弈的前提是:确保每次拿的数量是从1-m的不间断整数开始的。注意这一点很重要。无此条件巴什博弈不成立!
相关练习:
1. http://acm.hdu.edu.cn/showproblem.php?pid=1846 (brave game)
2. http://acm.hdu.edu.cn/showproblem.php?pid=2147 (kiki's game)
3. http://acm.hdu.edu.cn/showproblem.php?pid=2149 (public sale)
4. http://acm.hdu.edu.cn/showproblem.php?pid=2188 (选拔志愿者)
---------------------
题二解析参考:https://blog.csdn.net/oliver233/article/details/49336581
---------------------
下面介绍分析此类题目的通用方法:P/N分析:
P点: 即必败点,某玩家位于此点,只要对方无失误,则必败;
N点: 即必胜点,某玩家位于此点,只要自己无失误,则必胜。
三个定理:
定理:
一、 所有终结点都是必败点P(上游戏中,轮到谁拿牌,还剩0张牌的时候,此人就输了,因为无牌可取);
二、所有一步能走到必败点P的就是N点;
三、通过一步操作只能到N点的就是P点;
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
int main()
{
int m,n;
while(cin>>n>>m)
{
if(n%(m+1)==0) cout<<"lose"<<endl;
else cout<<"win"<<endl;
}
return 0;
}
---------------------
参考博客:https://www.cnblogs.com/locojyw/p/3405825.html
二、威佐夫博奕(Wythoff Game)
有两堆各若干个物品,两个人轮流从某一堆或同 时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
我们用(a[k],b[k]) (a[k] ≤ b[k] ,k=0,1,2,......n)来表示两堆物品的数量,并且称这个为局势。
如果现在的局势是(0,0),很明显此时已经没有办法再取了,所以肯定是之前的人在上一局中取完了。
假设现在的局势是(1,2),那么先手只有四种取法。
(1) 如果先手取走“1”中的1个,那么后手就从“2”中取出2个,此时取完,所以后手胜利。
(2)如果先手取走“2”中的2个,那么后手取走“1”中的1个,此时取完,后手胜利。
(3)如果先手取走“2”中的1个,那么后手就在两堆中各取走1个,此时取完,后手胜利。
(4)如果先手在“1”和“2”各取走了1个,那么后手取走“2”中的1个,此时取完,后手胜利。
由此可得,先手必输。
是不是觉得这个后手好厉害,无论先手怎么取,后手都会胜利。
我们可以来找找那些先手必输局势的规律
第一个(0,0)
第二个(1,2)
第三个(3,5)
第四个(4 ,7)
第五个(6,10)
第六个 (8,13)
第七个 ( 9 , 15)
第八个 ( 11 ,18)
第n个(a[k],b[k])
我们把这些局势称为“奇异局势”
我们会发现他们的差值是递增的,分别是0,1,2,3,4,5,6,7......n
我们用数学方法分析发现这些局势的第一个值是未在前面出现过的最小的自然数。
继续分析我们会发现,每种奇异局势的第一个值总是等于当前局势的差值乘上1.618
我们都知道0.618是黄金分割率。而威佐夫博弈正好是1.618,这就是博弈的奇妙之处!
即 a[k] = (int) ((b[k] - a[k])*1.618) 注:这里的int是强制类型转换,注意这不是简单的四舍五入,假如后面的值是3.9,转换以后得到的不是4而是3,也就是说强制int类型转换得到的是不大于这个数值的最大整数。
在编程题中,有些题目要求精度较高,我们可以用下述式子来表示这个值
1.618 = (sqrt(5.0) + 1) / 2
相关练习:http://acm.hdu.edu.cn/showproblem.php?pid=1527
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
double r=(sqrt(5.0)+1)/2;
int main()
{
ll m,n;
while(cin>>n>>m)
{
ll temp=(int)abs(m-n)*r;
if(min(m,n)==temp) cout<<0<<endl;
else cout<<1<<endl;
}
return 0;
}
---------------------
参考博客:https://blog.csdn.net/qq_41311604/article/details/79980882
三、尼姆博奕(Nimm Game)
有三堆各若干个物品,两个人轮流从某一堆取任意多的 物品,规定每次至少取一个,多者不限,最后取光者得胜。
1、首先自己想一下,就会发现只要最后剩两堆物品一样多(不为零),第三堆为零,那面对这种局势的一方就必败
那我们用(a,b,c)表示某种局势,首先(0,0,0)显然是必败态,无论谁面对(0,0,0) ,都必然失败;第二种必败态是(0,n,n),自己在某一堆拿走k(k ≤ n)个物品,不论k为多少,对方只要在另一堆拿走k个物品,最后自己都将面临(0,0,0)的局势,必败。仔细分析一下,(1,2,3)也是必败态,无论自己如何拿,接下来对手都可以把局势变为(0,n,n)的情形
那这种奇异局势有什么特点呢?
也不知谁这么牛逼,竟然能把这种局势和二进制联系在一起
这里说一种运算符号,异或'^',a^b=a'b+ab'(a'为非a)
我们用符号XOR表示这种运算,这种运算和一般加法不同的一点是1 XOR 1 = 0。先看(1,2,3)的按位模2加的结果:
1 = 二进制01
2 = 二进制10
3 = 二进制11 XOR
———————
0 = 二进制00 (注意不进位)
对于奇异局势(0,n,n)也一样,结果也是0
任何奇异局势(a,b,c)都有a XOR b XOR c = 0
如果我们面对的是一个非必败态(a,b,c),要如何变为必败态呢?
假设 a < b < c,我们只要将 c 变为a XOR b,即可。因为有如下的运算结果:
a XOR b XOR (a XOR b)=(a XOR a) XOR (b XOR b) = 0 XOR 0 = 0
要将c 变为a XOR b,只要对 c进行 c-(a XOR b)这样的运算即可
2、推广(1):有n堆若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
策略:A1xor A2 xor ……xor An==0时,先手输后手胜;否则,先手胜。
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
int main()
{
int m,n;
int temp=0;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>m;
temp^=m;
}
if(temp!=0) cout<<"win"<<endl;
else cout<<"lose"<<endl;
return 0;
}
推广(2):有n堆若干个物品,两个人轮流从某一堆取任意多的物品,规定每次取1~m个 (只能在一堆中取) ,最后取光者得胜。
策略:(A1%(m+1))xor (A2%(m+1)) xor…… xor (An%(m+1))==0,后手胜;否则,先手胜。
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
int main()
{
int m;
int a[MAXSIZE];
while(cin>>m&&m!=0)
{
int sum=0,count=0;
for(int i=0;i<m;i++)
{
cin>>a[i];
sum^=a[i];
}
for(int i=0;i<m;i++)
{
int res=sum^a[i];
if(res-a[i]<0) count++;
}
cout<<count<<endl;
}
return 0;
}
推广(3):有n堆若干个物品,两个人轮流从某一堆取任意多的物品,规定每次取1~m个 (只能在一堆中取) ,最后取光者输。
策略:统计所有Ai>1的个数,并将所有数字异或一遍,若大于一的个数为0&&异或之后为0||大于一的个数大于0&&异或之后不为零,则先手胜,否则后手胜。
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
int main()
{
int m,n,sum=0,count=0;
cin>>m>>n;
int a[MAXSIZE];
for(int i=0;i<n;i++)
{
cin>>a[i];
if(a[i]>1)
{
count++;
}
sum^=a[i];
}
if((count==0&&sum==0)&&(count>0&&sum!=0)) cout<<"win"<<endl;
else cout<<"lose"<<endl;
return 0;
}
推广(4):有n堆若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。“先手的人如果想赢,第一步有几种选择呢?”
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
int main()
{
int m,n,sum=0;
cin>>m>>n;
int a[MAXSIZE];
for(int i=0;i<n;i++)
{
cin>>a[i];
int temp=a[i]%(m+1);
sum^=temp;
}
if(sum==0) cout<<"lose"<<endl;
else cout<<"win"<<endl;
return 0;
}
策略:令sum=:A1xor A2 xor ……xor An;
res=sum xor A1;相当于 A2 xor…… xor An
若res > A1: 当前玩家就从Ai中取走(A1-res)个,使A1乘下res这样必然导致所有的堆的异或值为0,也就是必败点(达到奇异局势)
ps:注意一个必败点不可能导致另一个必败点,因为如果这样的话当前这个必败点就不是必败点了,所以这里对于每个堆的操作至多只有一种方法.
若res < A1:无论从这个堆取走多少都不可能导致必败点.
相关练习:http://acm.hdu.edu.cn/showproblem.php?pid=1850
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
using namespace std;
int main()
{
int m;
int a[MAXSIZE];
while(cin>>m&&m!=0)
{
int sum=0,count=0;
for(int i=0;i<m;i++)
{
cin>>a[i];
sum^=a[i];
}
for(int i=0;i<m;i++)
{
int res=sum^a[i];
if(res-a[i]<0) count++;
}
cout<<count<<endl;
}
return 0;
}
四、SG函数
在介绍SG函数和SG定理之前我们先介绍介绍必胜点与必败点吧.
必胜点和必败点的概念:
P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。
N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。
必胜点和必败点的性质:
1、所有终结点是 必败点 P 。(我们以此为基本前提进行推理,换句话说,我们以此为假设)
2、从任何必胜点N 操作,至少有一种方式可以进入必败点 P。
3、无论如何操作,必败点P 都只能进入 必胜点 N。
我们研究必胜点和必败点的目的时间为题进行简化,有助于我们的分析。通常我们分析必胜点和必败点都是以终结点进行逆序分析。我们以hdu 1847 Good Luck in CET-4 Everybody!(http://acm.hdu.edu.cn/showproblem.php?pid=1847)为例:
当 n = 0 时,显然为必败点,因为此时你已经无法进行操作了
当 n = 1 时,因为你一次就可以拿完所有牌,故此时为必胜点
当 n = 2 时,也是一次就可以拿完,故此时为必胜点
当 n = 3 时,要么就是剩一张要么剩两张,无论怎么取对方都将面对必胜点,故这一点为必败点。
以此类推,最后你就可以得到;
n : 0 1 2 3 4 5 6 ...
position: P N N P N N P ...
你发现了什么没有,对,他们就是成有规律,使用了 P/N来分析,有没有觉得问题变简单了。
现在我们就来介绍今天的主角吧。组合游戏的和通常是很复杂的,但是有一种新工具,可以使组合问题变得简单————SG函数和SG定理。
Sprague-Grundy定理(SG定理):
游戏和的SG函数等于各个游戏SG函数的Nim和。这样就可以将每一个子游戏分而治之,从而简化了问题。而Bouton定理就是Sprague-Grundy定理在Nim游戏中的直接应用,因为单堆的Nim游戏 SG函数满足 SG(x) = x.
SG函数:
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样 集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。
【实例】取石子问题
有1堆n个的石子,每次只能取{ 1, 3, 4 }个石子,先取完石子者胜利,那么各个数的SG值为多少?
SG[0]=0,f[]={1,3,4},
x=1 时,可以取走1 - f{1}个石子,剩余{0}个,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;
x=2 时,可以取走2 - f{1}个石子,剩余{1}个,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;
x=3 时,可以取走3 - f{1,3}个石子,剩余{2,0}个,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;
x=4 时,可以取走4- f{1,3,4}个石子,剩余{3,1,0}个,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;
x=5 时,可以取走5 - f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;
以此类推.....
x 0 1 2 3 4 5 6 7 8....
SG[x] 0 1 0 1 2 3 2 0 1....
由上述实例我们就可以得到SG函数值求解步骤,那么计算1~n的SG函数值步骤如下:
1、使用 数组f 将 可改变当前状态 的方式记录下来。
2、然后我们使用 另一个数组 将当前状态x 的后继状态标记。
3、最后模拟mex运算,也就是我们在标记值中 搜索 未被标记值 的最小值,将其赋值给SG(x)。
4、我们不断的重复 2 - 3 的步骤,就完成了 计算1~n 的函数值。
//f[N]为可改变状态的方式,N为种类;SG[]为SG值集合;S[]为x后继状态集合
int f[N],SG[MAXSIZE],S[MAXSIZE];
void getSG(int n)
{
memset(SG,0,sizeof(SG));
for(int i=1;i<=n;i++) //SG[0]始终为0,故i从1开始
{
memset(S,0,sizeof(S));
for(int j=0;f[j]<=i&&j<=N;j++)
{
S[SG[i-f[j]]]=1;
}
for(int j=0;;j++)
{
if(!S[j])
{
SG[i]=j;
break;
}
}
}
}
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1848
#include <iostream>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define ll long long
#define MAXSIZE 100050
#define N 100
using namespace std;
int f[N],SG[MAXSIZE],S[MAXSIZE];//f[N]为可改变状态的方式,N为种类;SG[]为SG值集合;S[]为x后继状态集合
void getSG(int n)
{
memset(SG,0,sizeof(SG));
for(int i=1;i<=n;i++) //SG[0]始终为0,故i从1开始
{
memset(S,0,sizeof(S));
for(int j=0;f[j]<=i&&j<N;j++)
{
S[SG[i-f[j]]]=1;
}
for(int j=0;;j++)
{
if(!S[j])
{
SG[i]=j;
break;
}
}
}
}
int main()
{
f[0]=f[1]=1;
for(int i=2;i<N;i++)
{
f[i]=f[i-1]+f[i-2];
}
getSG(1000);
int m,n,p;
while(cin>>m>>n>>p&&(m!=0||n!=0||p!=0))
{
// cout<<SG[m]<<' '<<SG[n]<<" "<<SG[p]<<endl;
// cout<<(SG[m]^SG[n]^SG[p])<<endl;
if((SG[m]^SG[n]^SG[p])==0) cout<<"Nacci"<<endl;
else cout<<"Fibo"<<endl;
}
return 0;
}
---------------------
作 者:Angel_Kitty
出 处:https://www.cnblogs.com/ECJTUACM-873284962/
关于作者:阿里云ACE,目前主要研究方向是Web安全漏洞以及反序列化。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。