(一)、巴什博奕 (Bash Game)
问题描述: 一堆n个物体,两个人轮流从这些物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。
问题分析:
显然,如果 n = m + 1 , 那么一次最多只能取m个,所以,无论先取者拿走多少个,后取者都能够一次拿走剩余的物品,后取者胜.
因此我们发现了如何取胜的法则,每个回合 m + 1 个,如果 n = (m + 1)r + s,(r为任意自然数,s<=m) 那么先取者要拿走s个物品,如果后取者拿走k个物品,那么先取者再拿走 m + 1 - k 个,结果剩下 (m + 1)(r - 1) 个,以后保持这样的取法,
那么先取者肯定获胜。总之要保持给对手留下 (m + 1) 的倍数,就能最后获胜。
这个游戏还有一个变相的玩法:两个人轮流报数,每次至少报一个,最多报10个,谁能报到100者胜。
经典例题 HUD 1846,2147,2149,2188
HDU: 1846. HDU: 2147.HDU: 2149.HDU: 2188.
P/N点分析
一、所有的终结点都是必败点P
二、只要下一个能到达的状态中出现一个P,那么当前的状态就是N
三、下一个状态全部为N时,当前的状态为P.
HDU 2147 Ac代码
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int main(void)
{
int t, m, n;
while(cin>>n>>m){
if (n == 0 && m == 0) break;
if(n&1 && m&1) puts("What a pity!");
else puts("Wonderful!");
}
return 0;
}
(二)、斐波那契博弈 (Fibonacci’s Game)
问题描述:
有一堆个数为n的石子,游戏双方轮流取石子,满足:
1) 先手不能再第一次把所有的石子取完;
2) 之后每次可以取的石子数介于1到对手刚好取得石子数的2倍之间(包含1和对手刚取得石子数的2倍)
问题结论:
当石子为斐波那契数的时候,先手必败,反之,先手必胜
支持的定理: 齐肯多夫定理:任何正整数都可以表示为若干个不连续的斐波那契数之和
证明:
我们假设现在有n个石子,并用第二数学归纳法进行归纳证明
1) 当石子数 = F(1)、F(2)、F(3)的时候,命题成立(先手必败),这个可以手动模拟推出来
2) 当石子的数目大于3的时候,我们设石子的数目为n。
并且由数学归纳法步骤,先假设石子的数目小于n的时候,结论命题成立
①给出给出一个小性质: F(n-1) = F(n-2) + F(n-3)< 2 * F(n-2)----->F(n) = F(n-1) + F(n-2)< 2 * F(n-1)
②因此我们可以将 F(n) 分成 F(n-1) + F(n-2) 两堆石子时是成立的。
③假设先手取走的石子数目>=F(n-2)的时候 根据 F(n-1) < 2*F(n-2) 后手一定可以将剩下的 F(n-1) 取走,后手胜
④所以先手必须要在F(n-2)范围里面取 那么由于 n-2 < n 根据我们的假设 后手一定可以取完 F(n-2)这一堆 (这个博弈很诡异的一点就是 2个人都是世界上最聪明的人,他们都会想方设法的让自己赢)
⑤那么又根据定义 n-1 < n , F(n-1)也是先手必败
那么当后手取完 F(n-2) 的时候,先手如果不能一下子把 F(n-1)取完的话,那么这个问题就转化为 F(n-1) 的问题了 先手必败
⑥那么先手只能眼睁睁的看着 F(n-2) 被后手取完。 先手一定会想尽办法让自己一次性取完剩下的 F(n-1)。
那么先手想让自己一次性取完 F(n-1),那么也就是说前面后手最后一次取完的时候取得尽量多(先手可以取得是2*后手取得),
这样先手自己取得空间才大,那么很容易的到,后手最多只能取 2/3F(n-2) ps:因为在取得时候一定会把F(n-2)取完,
把他等分成3份,后手最多取两份.
1.假设先后先取 1/3F(n-2) 那么后手可以用2/3F(n-2)来结束F(n-2)堆
2.假设先手取得比 1/3F(n-2)少,那么后手一次性是不能把 F(n-2)取完的,最后由后手取完F(n-2)的石子的时,石子数目
一定是比2/3F(n-2)少的(因为多了取F(n-2)的回合,石子的数目是越来越少的)
3.假设先手取得比 1/3F(n-2)多,虽然后手可以一次性把F(n-2)取完,但是因为先手取了大于1/3F(n-2),那么后手取得
石子的数目就比2/3F(n-2)少了
⑦因此综上所诉,后手最多取得2/3F(n-2) 那么先手再取F(n-1)的时候,最多可取(2/3F(n-2))*2--->4/3F(n-2)
但是事实上,这个数字是比F(n-1)要小的
根据性质 F(n-1) = F(n-2) + F(n-3)< 2 * F(n-2)----->F(n) = F(n-1) + F(n-2)< 2 * F(n-1)
4/3F(n-2) < F(n-1)---->
4F(n-2) < 3F(n-1)----->
4F(n-2) < 3(F(n-2) + F(n-3))---->
F(n-2) < 3F(n-3) ---->这个式子是成立的
那也就是说,先手再取F(n-1)的时候是不能一次性把F(n-1)取完的,那么这个游戏就转化成了F(n-1)个石子
独立游戏, n-1 < n 根据假设可得先手必败
⑧综上所有,F(n)时,先手必败
下面我们证明当石子数目不是斐波那契数的时候,先手必胜
这里我们需要引入一个定理 齐肯多夫定理:任何正整数都可以表示为若干个不连续的斐波那契数之和
假设 n = F(N1) + F(N2) + F(N3) + …
先手如何获胜?
先手只需要先取F(N1)个, 因为N2>N1 且N1、N2不连续 所以 F(N1) <= F(n-2) 因为 (F(N2) = F(N2 - 1) + F(N2 - 2))
那么 F(N2) > 2*F(N1) 所以后手不是能一次性把F(N2)取完的,那么后手变为了拿F(N2)的先手,根据之前的证明,先手拿斐波那契
数 必败.
所以说不是斐波那契数的时候 先手必胜(再取F(N2)的时候成为后手了)
(三)、威佐夫博弈 (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个,此时取完,后手胜利。
由此可得,先手必输。
是不是觉得这个后手好厉害,无论先手怎么取,后手都会胜利。
在学习威佐夫博弈之前,我也是这样认为的。不过,当你继续看完这篇博客,你也会轻松获得胜利。
为了让大家更好地理解威佐夫博弈,我们继续来进行具体分析。
假设现在的局势是(3,5),首先根据上面分析的经验,我们知道先手肯定不能把任意一堆物品取完,
这是因为每次可以从任意一堆取走任意个物品,那么后手就可以直接把另一堆取完,所以后手获胜。所以我们这里就不分析那些情况,来分析其他的情况。
先看在一堆中取的情况:
(1) 假设先手在“3”中取1个,后手就可以在“5”中取走4个,这样就变成了(1,2)的局势,根据上面的分析,我们知道是先手输,后手获胜。
(2) 假设先手在“3”中取2个,后手就可以在 “5” 中取走3个,这样也变成了(1,2)的局势了,还是先手输,后手获胜。
(3)假设先手在“5”中取1个,后手就在 “3”和“5” 中各取走2个,这样又成了(1,2)的局势了,先手输,后手赢。
(4)假设先手在“5”中取2个,后手就在 “3”和“5” 中各取走3个,这样变成了(0,0)的局势,先手输,后手赢。
(5)假设先手在“5”中取3个,后手就在 “3”和“5” 中各取走1个,也变成了(1,2)的局势,先手输,后手胜利。
(6)假设先手在“5”中取4个,后手在“3”中取走1个,还是(1,2)的局势,先手输,后手赢。
我们发现上面列举的这几种局势,无论先手怎么取都是后手赢。
我们可以来找找那些先手必输局势的规律
第一个(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是强制类型转换,int也可以写成
floor(地板 向下取整) 注意这不是简单的四舍五入,假如后面的值是3.9,转换以后得到的不是4而是3,
也就是说强制int类型转换得到的是不大于这个数值的最大整数。在编程题中,有些题目要求精度较高,我们可以用下述式子来表示这个值
1.618 = (sqrt(5.0) + 1) / 2
具体题目:
poj 1067
取石子游戏
有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,
一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。
现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。 Input输入包含若干行,表示若干种石子的初始情况,其中每一行包含两个非负整数a和b, 表示两堆石子的数目,a和b都不大于1,000,000,000。
Output输出对应也有若干行,每行包含一个数字1或0,如果最后你是胜者,则为1,反之,则为0。
Sample Input2 1
8 4
4 7
Sample Output0
1
0
Source
Ac代码:
#include <iostream>
#include <cstdio>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
int main(void)
{
int a, b, c;
while(cin>>a>>b)
{
if(a>b) swap(a, b);
c = floor((b-a)*((sqrt(5.0) + 1)/2));
if(a == c) puts("0");
else puts("1");
}
return 0;
}
(四)、尼姆博弈 (Nimm Game)
问题描述:
有若干堆石子,每堆石子的数量是有限的,二个人依次从这些石子堆中拿取任意的石子,至少一个(不能不取),最后一个拿光石子的人胜利。
问题分析:
1、我们首先以一堆为例: 假设现在只有一堆石子,你的最佳选择是将所有石子全部拿走,那么你就赢了。
2、如果是两堆:假设现在有两堆石子且数量不相同,那么你的最佳选择是取走多的那堆石子中多出来的那几个,
使得两堆石子数量相同,这样,不管另一个怎么取,你都可以在另一堆中和他取相同的个数,这样的局面你就是必胜。
比如有两堆石子,第一堆有3个,第二堆有5个,这时候你要拿走第二堆的三个,然后两堆就都变成了3个,
这时你的对手无论怎么操作,你都可以“学”他,比如他在第一堆拿走两个,你就在第二堆拿走两个,这样你就是稳赢的3、如果是三堆 ,我们用(a,b,c)表示某种局势,首先(0,0,0)显然是奇异局势,无论谁面对奇异局势,
都必然失败。第二种奇异局势是(0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一
下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情型。 从中我们要明白两个理论:一个状态是必败状态当且仅当它的所有后继都是必胜状态 一个状态是必胜状态当且仅当它至少有一个后继是必败状态
有了这两个规则,就可以用递推的方法判断整个状态图的每一个结点都是必胜还是必败状态。
这里引入L . Bouton在1902年给出的定理:状态(x1,x2,x3)为必败状态当且仅当x1 XOR x2 XOR x3=0,
这里的XOR是二进制的逐位异或操作,也成Nim和。也就是当Nim和!= 0时,先手胜利,否则失败
计算机算法里面有一种叫做按位模2加,也叫做异或的运算,我们用符号(+)表示这种运算。
这种运算和一般加法不同的一点是1+1=0。先看(1,2,3)的按位模2加的结果:1 =二进制01 2 =二进制10 3 =二进制11 (+) ——————— 0 =二进制00 (注意不进位)
对于奇异局势(0,n,n)也一样,结果也是0。 任何奇异局势(a,b,c)都有a(+)b(+)c =0。
如果我们面对的是一个非奇异局势(a,b,c),要如何变为奇异局势呢?假设 a < b< c,我们只要将 c 变为 a(+)b,即可,
因为有如下的运算结果: a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0。要将c 变为a(+)b, 只要从
c中减去 c-(a(+)b)即可。也就是取走(a(+)b)个石子。这里我们要了解异或运算的一个特点 a(+)c(+)c = a例1。(14,21,39),14(+)21=27,39-27=12,所以从39中拿走12个物体即可达到奇异局势(14,21,27)。 例2。(55,81,121),55(+)81=102,121-102=19,所以从121中拿走19个物品就形成了奇异局势(55,81,102)。 例3。(29,45,58),29(+)45=48,58-48=10,从58中拿走10个,变为(29,45,48)。 例4。我们来实际进行一盘比赛看看: 甲:(7,8,9)->(1,8,9)奇异局势 乙:(1,8,9)->(1,8,4) 甲:(1,8,4)->(1,5,4)奇异局势 乙:(1,5,4)->(1,4,4) 甲:(1,4,4)->(0,4,4)奇异局势 乙:(0,4,4)->(0,4,2) 甲:(0.4,2)->(0,2,2)奇异局势 乙:(0,2,2)->(0,2,1) 甲:(0,2,1)->(0,1,1)奇异局势 乙:(0,1,1)->(0,1,0) 甲:(0,1,0)->(0,0,0)奇异局势 甲胜。
第一个类型:就是让其判断胜利的人,对n堆石子求异或和,根据当Nim和!= 0时,先手胜利,否则失败就能判断出来。
第二个类型:先取完者判输,统计一下所有数一下大于1的个数,并将所有数字异或一遍,若大于1的个数为0&&异或和为0||大于1的个数大于0&&异或和不为零,则先手胜,否则后手胜。第三个类型:限制最多取的个数,例如第i堆石子共有m个,最多取r个,先对m=m%(r+1);然后在进行异或求和。再根据异或和判断输赢。
第四种类型:先手的人想赢,第一步有多少种选择。当先手必输时,很显然是0。如果先手赢,那么先手必须努力创造奇异局势,
即让其剩余的石子量异或和为0,上面已经讲了当面对非奇异局势是如何转化成奇异局势。当nim游戏的某个位置:(x1,x2,x3),
当且仅当其各部分的nim - sum = 0(即x1(+)x2(+)x3 = 0(也就是各部分的异或为0)) 当前位置为必败点,
这对于多个堆的情况同样适用。我们首先求出所有堆异或后的值sum,再用这个值去对每一个堆进行异或, 令res =
x1(+)sum(sum为所有堆的异或和)。如果res < x1的话,当前玩家就从x1中取走(x1-res)个,
使x1乘下res这样必然导致所有的堆的异或值为0,也就是必败点(达到奇异局势),这就是一种方案。遍历每一个堆,
进行上面的断判就可以得到总的方案数。res =
x1(+)sum;其实就是除了x1之外的n-1堆异或和,a(+)b(+)c=sum;sum(+)c=a(+)b(+)c(+)c=a(+)b;ps:注意一个必败点不可能导致另一个必败点,因为如果这样的话当前这个必败点就不是必败点了,所以这里对于每个堆的操作至多只有一种方法
可以导败必败点,如果res > x1的话就无论从这个堆取走多少都不可能导致必败点!!!
具体题目:
下面是一个二人小游戏:桌子上有M堆扑克牌;每堆牌的数量分别为Ni(i=1…M);两人轮流进行;
每走一步可以任意选择一堆并取走其中的任意张牌;桌子上的扑克全部取光,则游戏结束;最后一次取牌的人为胜者。
现在我们不想研究到底先手为胜还是为负,我只想问大家:
——“先手的人如果想赢,第一步有几种选择呢?”
Input
输入数据包含多个测试用例,每个测试用例占2行,首先一行包含一个整数M(1<M<=100),表示扑克牌的堆数,紧接着一行包含M个整数Ni(1<=Ni<=1000000,i=1…M),分别表示M堆扑克的数量。M为0则表示输入数据的结束。
Output
如果先手的人能赢,请输出他第一步可行的方案数,否则请输出0,每个实例的输出占一行。
Sample Input
3
5 7 9
0
Sample Output
1
Ac代码:
#include <iostream>
#include <cstdio>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
int arr[109];
int main(void)
{
int m;
while(cin>>m&&m)
{
int ans = 0;
for(int i=0;i<m;i++){
cin>>arr[i];
ans ^= arr[i];
}
if(ans == 0) puts("0");
else{
int cnt = 0;
for(int i=0;i<m;i++){
int k = ans^arr[i];
if(k < arr[i])
cnt++;
}
cout<<cnt<<endl;
}
}
return 0;
}
HDU: 1847.
Ac代码:
//求sg值
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int arr[15], SG[1024];
int mex(int x) //这个方法是按照递归写的
{
if(SG[x]!=-1) return SG[x]; // 如果现在的点的SG值不是-1,说明访问过了
bool vis[1024]; //定义一个数组 用来找点前的点的最小值
memset(vis, false, sizeof(vis));
for(int i=0;i<=10;i++){
int temp = x - arr[i]; //看他可以到哪那些点
if(temp<0) break; //超过的话直接break掉
SG[temp] = mex(temp); //再去找他的sg值
vis[SG[temp]] = true; //标记这个sg值我已经访问过了
}
for(int i=0;i<=10;i++){
if(!vis[i]){ SG[x] = i; break;} //找最小的那个值
}
return SG[x];
}
int main(void)
{
int t, m, n;
arr[0] = 1;
for(int i=1;i<=10;i++){
arr[i] = arr[i-1]*2;
}
while(~scanf("%d", &n)){
memset(SG, -1, sizeof(SG));
if(mex(n)) puts("Kiki");
else puts("Cici");
}
return 0;
}
(五)、组合博弈
HDU: 1536.
Ac代码:
//sg打表
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 10007;
const int mod = 1e9 + 7;
int n, m, k, sum, ans, cnt, l;
int sg[maxn];
int arr[109];
int get_sg(int x)
{
if(sg[x] != -1) return sg[x];
bool vis[maxn];
memset(vis, false, sizeof(vis));
for(int i=0;i<k;i++){
int temp = x - arr[i];
if(temp<0) break;
sg[temp] = get_sg(temp);
vis[sg[temp]] = true;
}
for(int i = 0;i<maxn;i++)
if(!vis[i]){ sg[x] = i; break;}
return sg[x];
}
int main(void)
{
//ios::sync_with_stdio(false);
while(scanf("%d", &k) && k)
{
memset(sg, -1 ,sizeof(sg));
for(int i=0;i<k;i++){
cin>>arr[i];
}
sort(arr, arr+k);
for(int i=1;i<=10000;i++){
get_sg(i);
}
cin>>m;
for(int i=0;i<m;i++){
ans = 0;
cin>>l;
int num ;
for(int i = 0 ; i < l ;i++){
cin>>num;
ans ^= sg[num];
}
if(ans) cout<<"W";
else cout<<"L";
}
cout<<endl;
}
return 0;
}