博弈论入门
博弈论入门
博弈
巴什博弈 Bash Game
模型
只有一堆n个物品,两个人从轮流中取出(1~m)个;最后取光者胜。
思路
考虑到 若n=m+1 那么 第一个人不论如何取都不能取胜。
进一步我们发现 若 n=k*(m+1)+r; 先取者拿走 r 个,那么后者再拿(1~m)个
n=(k-1)*(m+1)+s; 先取者再拿走s 个 最后总能造成 剩下n=m+1 的局面。
因此,此时先手有必赢策略。相对应的,若n=k*(m+1) 那么先取者必输。
因此我们就可以写出对应程序(n,m>0)
int Bash_Game(int n,int m)//是否先手有必赢策略
{
if (n%(m+1)!=0) return 1;
return 0;
}
尼姆博弈 Nim Game
模型
当前有n堆每堆\(M_i>0\)个物品,两个人从轮流中取出若干个;最后取光者胜。
思路
从Bash Game中我们知道一个情形对应的一种状态,而从一个状态只能变成另一个状态时能很轻易的判定是否先手必胜
那么我们怎么把这样一个思想应用到Nim中呢
换句话说怎样才能在Nim中找到这样一个可以转化的状态
如果我们把n堆转化为n个整数,再将这n个整数用二进制表示,然后我们对这n个二进制数按位相加(不进位),若每一位相加都为偶数
那么我们称这个状态为偶状态,否则为奇状态;
可以证明:任何一个偶状态在其中一个数变小后一定会成为奇状态,一个奇状态一点可以通过改变一个数变为偶状态;
前一点很显然,因为一个数变小至少有一位发生变化,那么这一位就会改变原来的偶状态;
对于后一点,我们考虑一个从高位到低位某一位和为奇数的奇状态,必然有一个数的二进制此位为1,那么对于后面的较低位和为奇数的情况,只需要将这一位取反就可以得到一个偶状态;
那么现在我们就达到了成功的第一步——构造两个可以互相转化的状态并且显然存在奇状态为必胜态;偶状态为必败态;
那么我们对于n堆物品只需要判断他是否为奇状态就可以判定是否先手必胜;
但是对于每个数都二进制拆分非常麻烦,但是我们可以用神奇的位运算来完成这个过程
神奇的XOR与判断
如果有奇数个二进制数在第k为等于1那么显然在这一位上的和为奇数,同样的若有偶数个1则和为偶。
很明显位运算XOR满足我们的要求,偶数个1异或和为0,奇数个1异或和为1;
美滋滋这样不就搞完了
int Nimm_Game(int n)//假设n个数存在数组f[]中,有必胜策略返回1
{
int flag=0;
for(int i=1;i<=n;i++)
flag^=f[i];
if(flag) return 1;
return 0;
}
一些小情况
但是当你遇到n非常大,并且每一堆的物品数是连续的整数的时候
我们就不能直接枚举n了
我们需要考虑连续非负整数的异或和问题
记\(f(x,y)\)为\(x\)到\(y\)的所有整数的异或和。
f[1,n]=f[0,n];
当存在\(n=2^k-1\)时\(,f(1,n)=f(0,n)=0,(k\geq2)\)
证明:
我们先来考虑\(f(2^k,2^{k+1}-1)\)
从\(2^k\)到\(2^{k+1}-1\)这\(2^k\)个数,最高位的一个数为\(2^k\);
若有\(k>=1\),则\(2^k\)为偶数,将这\(2^k\)个数的最高位去掉,异或和不变
因此\(f(2^k,2^{k+1}-1)=f(2^k-2^k,2^{k+1}-2^k-1)=f(0,2^{k}-1)\)
因而存在\(f(0,2^{k+1}-1)=f(0,2^k-1) xor f(2^k,2^{k+1}-1)=0\)
即\(f(0,2^k-1)=0\)
对于\(,f(0,n),n\geq4\)设n二进制表示的最高位1在第k位k>=2;
\(f(0,n)=f(0,2^k-1) xor f(2^k,n)=f(2^k,n)\)
对于\(2^k\)到\(n\)这\(n-2^k+1\)个数,最高位共有\(m=n-2^k+1\)个1,去除最高位的1
当n为奇数时,m为偶数此时有\(f(0,n)=f(2^k,n)=f(0,n-2^k)|2^k\)
由于\(n-2^k\)与n奇偶性相同,递推上面的公式可得\(f(0,n)=f(0,n-2^k-2^{k-1}-2^{k-2}\cdots -2^2)=f(0,n\%4)\)
当\(n\%4=1\)时\(f(0,n)=f(0,1)=1\)
当\(n\%4=3\)时\(f(0,n)=f(0,3)=0\)
当n为偶数时,m为奇数,因而\(f(0,n)=f(2^k,n)=f(0,n-2^k)xor2^k\)
也相当于最高位不变,递推公式可得
\(f(0,n)=f(0,n\%4)xor 2^kxor\) \(n[k]*2^k-1 xor\cdots\) \(n[2]*2^2\)
n[k]表示n的二进制表示的第k位
显然当n为偶数时 \(f(0,n)\)的二进制从最高位到第3位和n的二进制表示相同
此时我们只需要判断第二位
当\(n\%4=0\)时\(f(0,n)=n\)
当\(n\%4=2\)时\(f(0,n)=n+1\)
综上所述:
代码给出来吧
//读入n,表示有从物品数分别1到n的n组物品,假设n个数存在数组f[]中
int xor_n(int n)//从1到n的异或和
{
int t = n & 3;
if (t & 1) return t / 2 ^ 1;
return t / 2 ^ n;
}
int Nimm_Game(int n)//有必胜策略返回1
{
int flag=0;
for(int i=1;i<=n;i++)
flag^=xor_n(f[i]);
if(flag) return 1;
return 0;
}
新Nim Game
在第一个回合中,第一个游戏者可以直接拿走若干个整堆的火柴。可以一堆都不拿,但不可以全部拿走。第二回合也一样,第二个游戏者也有这样一次机会。从第三个回合(又轮到第一个游戏者)开始,规则和Nim游戏一样。
如果你先拿,怎样才能保证获胜?如果可以获胜的话,还要让第一回合拿的火柴总数尽量小。
题解
我们第一次拿完后,要使得剩下的火柴中不存在异或和为0的子集,否则对方会将先手必败的状态留给我们。
所以我们可以使用贪心算法确定最优解。因此我们采用在线维护线性基的方法判断当前的数能否加入集合。
威佐夫博弈 Wythoff Game
模型
有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
思路
这种情况是比较复杂的
我们还是延续前面的思路:构造出两个可以互相转化的状态然后对于每个状态判断先手必胜或者后手必胜。
然后我们来开始构造状态我也不知道为什么这样构造
我们用\(,(a_k,b_k),a_k<b_k\)表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了我们设这样一个状态为奇异状态。那么我们的前几个奇异状态为:(0,0),(1,2),(3,5),(4,7),(6,10),(8,13),(9,15)
根据数学归纳法可以得到
\(a_0=b_0=0\),\(a_k\)是未在前面出现过的最小的自然数,\(b_k=a_k+k\),奇异状态有以下三条性质:
1.任何自然数都包含在一个唯一的奇异状态中。
2.任意合法操作都可以将一个奇异状态变为一个非奇异状态。
3.采用适当的方法可以将非奇数异状态转化为一个奇异状态
从如上性质可知,两个人如果都采用正确操作,那么面对非奇异局势,先拿者必胜
;反之,则后拿者取胜。
那么任给一个局势(a,b),怎样判断它是不是奇异局势呢?我们有如下公式:
然后我们这样做
bool Wythoff Game (n,m) {
int a=min(n,m),b=max(n,m);
double r=(sqrt(5.0)+1.0)/2,k=(double)(b-a);
int temp=(int)(k*r);
if(temp==a) return 0;
return 1;
}
Anti-Nim Game
规则
桌上有n堆石子,游戏双方轮流取石子,每次只能从一堆中取出任意数目的石子,不能不取,取走最后一个石子者失败。
游戏规则同Nim相同,只是最后取完石子的人输。
结论
必胜点
- 有偶数堆,且所有堆的石子数都为1.
- 至少有一堆石子数大于1,且石子堆的异或和不为0.
证明
一.当所有堆的石子数均为1时
- 石子异或和(t)=0,即有偶数堆。此时显然先手必胜。
- t≠0,即有奇数堆。此时显然先手必败。
二.当有一堆的石子数大于1时,显然t≠0
- 总共有奇数堆石子,此时把大于1的那堆取至1个石子,此时便转化为上面的第一种情况,先手必胜。
- 总共有偶数堆石子,此时把大于1的那堆取完,此时便转化为上面的第一种情况,先手必胜。
三.当有两堆及以上的石子数大于1时
-
t=0,那么只可能转化为以下两个子状态:
- 至少有两堆及以上的石子数大于1且t≠0,即后面的t≠0的状态
- 至少一堆石子数大于1,由二可知此时先手必胜。
-
t≠0,根据Nim Game 我们总有一种方法让t=0,且有两堆以上石子大于
观察三我们发现,三.(2)能把三.(1)扔给对面,而对面只能扔给你三.(2)或必胜态。所以当三.(2)时先手必胜。
综上,所有堆的石子数均=1且t=0/至少有一个堆的石子数>1且t≠0时,先手必胜。
另一种思路SG函数
结论
先手必胜,当且仅当:
①、所有堆的石子数都为1,且游戏的SG值为0。
②、存在堆的石子数大于1,且游戏的SG值不为0。
例题 Be the Winner
题面
n堆苹果,每次从一堆中拿任意个,最后拿的输,问先拿者是否能赢
题解
例题例题
代码
#include<bits/stdc++.h>
using namespace std;
int n;
int gg,a;
int main(){
while(scanf("%d",&n)!=EOF){
res=0;
int f=0;
for(int i=1;i<=n;i++) {
scanf("%d",&a);
if(a>1) f=1;
gg^=a;
}
if(f) printf(gg?"Yes\n":"No\n");
else printf(n&1?"No\n":"Yes\n");
}
return 0;
}