先来总结一些基础博弈吧,毕竟我也是博弈小白.
此类问题一般有如下特点:
1、博弈模型为两人轮流决策的非合作博弈。即两人轮流进行决策,并且两人都使用最优策略来获取胜利。
2、博弈是有限的。即无论两人怎样决策,都会在有限步后决出胜负。
3、公平博弈。即两人进行决策所遵循的规则相同。
在此先强调一下,博弈重在寻找必胜态和必败态.一般找到必胜态必败态许多问题就迎刃而解了.
必败态: 毫无疑问就是当前状态无论怎么操作都是必输.(也就是当前步无论怎么走到达的全是必胜态,即把必胜态扔给对方)
必胜态: 下一步的操作中存在至少一个必败态.(即我可以走这一步操作,把必败态给了对方)
几种基本的博弈类型: (只给出结论,证明见:点击打开链接)
1.巴什博奕
问题模型:只有一堆n个物品,两个人轮流从这堆物品中取物品,规定每次至少取一个,最多取m个,最后取光者得胜。)
结论:n%(m+1)==0先手必败.否则先手必胜
变形:条件不变,改为最后取光的人输。 结论:(n-1)%(m+1)==0 先手必败,否则必胜
2. 威佐夫博奕
问题模型:有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,
最后取光者得胜。
结论: k=(b-a) (a>b) s = (double )( k * ( sqrt(5.0) + 1 ) / 2 ); s==a 先手必败,否则必胜
3.尼姆博弈
问题模型:有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
结论: 当石子堆数为n堆时,则推广为当对每堆的数目进行亦或之后值为零是必败态。
4.sg函数
对于ICG游戏,我们可以将游戏中每一个可能发生的局面表示为一个点。并且若存在局面i和局面j,且j是i的后继局面
(即局面i可以转化为局面j),我们用一条有向边,从i出发到j,连接表示局面i和局面j的点。则整个游戏可以表示成为
一个有向无环图:
根据ICG游戏的定义我们知道,任意一个无法继续进行下去的局面为终结局面,即P局面(先手必败)。在上图中我们
可以标记所有出度为0的点为P点。
接着根据ICG游戏的两条性质,我们可以逆推出所有点为P局面还是N局面:
因此,对于任意一个ICG游戏,我们可以采取逆推的方法,标记出所有局面是N局面还是P局面。
但仅仅只是标记N、P,所能得到的信息太少,于是我们定义了Sg(Sprague-Grundy)函数:
对于一个游戏可能发生的局面x,我们如下定义它的sg值:
(1)若当前局面x为终结局面,则sg值为0。
(2)若当前局面x非终结局面,其sg值为:sg(x) = mex{sg(y) | y是x的后继局面}。
mex{a[i]}表示a中未出现的最小非负整数。举个例子来说:
mex{0, 1, 2} = 3, mex{1, 2}=0, mex{0,1,3}=2
我们将上图用sg函数表示后,得到:
可以发现,若一个局面x为P局面,则有sg(x)=0;否则sg(x)>0。同样sg值也满足N、P之间的转换关系:
若一个局面x,其sg(x)>0,则一定存在一个后续局面y,sg(y)=0。
若一个局面x,其sg(x)=0,则x的所有后续局面y,sg(y)>0。
由上面的推论,我们可以知道用N、P-Position可以描述的游戏用sg同样可以描述。并且在sg函数中
还有一个非常好用的定理,叫做sg定理:
对于多个单一游戏,X=x[1..n],每一次我们只能改变其中一个单一游戏的局面。则其总局面的sg值
等于这些单一游戏的sg值异或和。
即:
sg(X) = sg(x[1]) xor sg(x[2]) xor … xor sg(x[n])
要证明这一点我们只要证明:
(1) 假设sg(x[1]) xor sg(x[2]) xor … xor sg(x[n]) = A,对于任意一个0 <= B < A,总存在一个X的后续局面Y,
使得sg(Y) = B。
(2) 假设sg(x[1]) xor sg(x[2]) xor … xor sg(x[n]) = A,不存在一个X的后续局面Y,使得sg(Y) = A。
下先证明(1):
假设M = A xor
B,设M表示为二进制之后最高位的1为第k位。所以A的第k位为1,B的第k位为0。又因为A的第k位为1,
至少存在一个i,sg(x[i])的第k位也为1。那么一定有sg(x[i]) xor M < sg(x[i]),即一定通过某个操作使x[i]变为x[i’],
且sg(x[i’]) = sg(x[i]) xor M。那么:
sg(x[i’]) xor Other = sg(x[i]) xor M xor Other = M xor A = B
下证明(2):
若sg(X) = A,sg(Y) = A。不妨设我们改变的游戏为x[i],则X=x[1..n], Y=x[1…i’…n]。有sg(x[i]) = sg(x[i’]),
产生矛盾,所以sg(Y)不可能等于A。
5.阶梯博弈
博弈在一列阶梯上进行...每个阶梯上放着自然数个点..两个人进行阶梯博弈...每一步则是将一个集体上的
若干个点( >=1 )移到前面去..最后没有点可以移动的人输..
阶梯博弈也是可以转化成尼姆博弈的.
把所有奇数阶梯看成N堆石子..做nim..把石子从奇数堆移动到偶数堆可以理解为拿走石子..就相当于几个
奇数堆的石子在做Nim..
6.Chomp!博弈(巧克力游戏)
有一个n*m的棋盘,每次可以取走一个方格并拿掉它右边和上面的所有方格。拿到左下角的格子(1,1)者输,如下图是8*3的
棋盘中拿掉(6,2)和(2,3)后的状态。
结论:答案是除了1*1的棋盘,对于其他大小的棋盘,先手总能赢。
分析:有一个很巧妙的证明可以保证先手存在必胜策略,可惜这个证明不是构造性的,也就是说没有给出先手怎么下才能赢。
证明如下:
如果后手能赢,也就是说后手有必胜策略,使得无论先手第一次取哪个石子,后手都能获得最后的胜利。那么现在假设先手
取最右上角的石子(n,m),接下来后手通过某种取法使得自己进入必胜的局面。但事实上,先手在第一次取的时候就可以和
后手这次取的一样,进入必胜局面了,与假设矛盾。
巧克力游戏的变形:
约数游戏:有1~n个数字,两个人轮流选择一个数字,并把它和它的约数擦去。擦去最后一个数的人赢,问谁会获胜。
分析:类似巧克力游戏,得到结论就是无论n是几,都是先手必胜。
翻棋子游戏:
题意:一个棋盘上每个格子有一个棋子,每次操作可以随便选一个朝上的棋子(x,y),代表第i行第j列的棋子,选择一个形
如(x,b)或(a,y)(其中b < y,a < x)的棋子,然后把它和(x,y)一起翻转,无法操作的人输。
分析:把坐标为(x,y)的棋子看成大小分别为x和y的两堆石子,则本题转化为了经典的Nim游戏,如果难以把棋子看作石
子,可以先把Nim游戏中的一堆石子看成一个正整数,则Nim游戏中的每次操作是把其中一个正整数减小或者删除。
题目:
POJ1704 阶梯博弈变形
题意:
从左到右有一排石子,给出石子所在的位置。规定每个石子只能向左移动,且不能跨过前面的石子。最左边的石子最多只能移动
到1位置。每次选择一个石子按规则向左移动,问先手是否能赢。
思路:
首先,按照我上面说的,先来寻找必胜态和必败态。什么时候会是必败态,小范围的,当只有两个石子的时候,先手必胜的. 也就是说
这时候谁先移动前面那颗石子谁就输了,另一个人就一直移动后面那个石子贴着前面那个,一直这样下去必输.
那么如果棋盘上有多对石子怎么办,我们根据上面那个想象一下,每对石子的前一个和上一个石子的后一个的距离对结果无影响.
对于每对石子之间,什么时候能紧贴着,那就没法走了只能移动前面那个石子,这个过程可以看成是一堆石子取空了.一对点的距离就是
石子数量
所以我们对这些石子排序,从后往前每两个一对,如果是奇数第一个数自己一堆.
#include
#include
#include
using namespace std;
const int maxn = 1e3+10;
int n;
int a[maxn];
int main()
{
int _;
cin>>_;
while(_--)
{
cin>>n;
for(int i = 1;i <= n;++i)
{
cin>>a[i];
}
int ans = 0;
sort(a+1,a+1+n);
if(n %2 == 0)
{
for(int i = n;i >= 2;i -= 2)
{
ans ^= (a[i] - a[i-1] - 1);
}
}
else
{
for(int i = n;i >= 1;i -= 2)
ans ^= (a[i] - a[i -1] - 1);
}
if(ans != 0)
puts("Georgia will win");
else
puts("Bob will win");
}
return 0;
}
HDU - 4315
题意:
有n个人爬山,山顶坐标为0,其他人按升序给出,不同的坐标只能容纳一个人(山顶不限),Alice和Bob轮流选择一个人让他移动任意步,
但不能越过前面的人,且不能和前面一个人在相同的位置。现在有一个人是king,给出king是哪个人(id),谁能将国王移动到山顶谁胜。
思路:
这个题和上面那个很像,首先考虑不存在国王的情况,全是普通人,那就是上面那种情况了.还是相邻一组,考虑奇偶。
但是现在有国王了,1.当国王在第一个,先手必胜.2.当数量为奇数,并且国王在第二个的时候需要把第一个的距离减1,因为他不能直接移走,否则
又成了国王在第一个的情况必胜.3.其余的情况就把国王当成普通人处理就好.
#include
#include
#include
using namespace std;
const int maxn = 1e3+10;
int n,k;
int a[maxn];
int main()
{
while(cin>>n>>k)
{
for(int i = 1;i <= n;++i)
{
cin>>a[i];
}
if(k==1)
{
puts("Alice");
continue;
}
int ans;
ans = 0;
if(n % 2 == 0 || k != 2)
a[0] = -1;
else
a[0] = 0;
for(int i = n;i >= 1;i -= 2)
{
ans ^= (a[i] - a[i-1] - 1);
}
if(ans != 0)
puts("Alice");
else
puts("Bob");
}
return 0;
}
题意;
Alice和Bob这一次准备玩一个关于硬币的游戏:N枚硬币排成一列,有的正面朝上,有的背面朝上,从左到右依次编号为1..N。
现在两人轮流翻硬币,每次只能将一枚正面朝上的硬币翻过来,并且可以随自己的意愿,在一枚硬币翻转后决定要不要将该硬币左边
的任意一枚硬币也翻一次(正面翻到背面或背面翻到正面)。翻最后一枚正面向上的硬币的人获胜。同样的,这次游戏里面Alice仍然先
手,两人均采取最优的策略,对于给定的初始局面,Alice会获胜还是Bob会获胜?
思路:
首先,我们先将局面分解一下,每一次我们只考虑一枚硬币。
不妨设所有硬币全部背面朝上的局面为局面0
假设现在N枚硬币,只有第1枚是正面朝上的。该局面只能转化为全部硬币背面朝上的局面。我们假定该局面为 局面1,则局面1可以
转化为局面0。
假设只有第2枚是正面朝上的。该局面可以转化为:只有硬币1正面朝上;全部硬币背面朝上。我们假定该局面为 局面2,局面2可以
转化为局面1和局面0。
同理我们可以推定,第i枚硬币正面朝上的局面为局面i,局面i可以转化为局面0..i-1。
现在,我们考虑把给定的局面拆成单个硬币的局面集合,比如给定了{HHTHTTHT},其中H表示正面朝上,T表示背面朝上。那么就
是当前局面={局面1,局面2,局面4,局面7}。每一次我们可以改变其中个一个局面,当出现局面0时就从集合中删去。
这样一看是不是就变成了Nim游戏了?然而事实并没有那么简单。
进一步分析,若同时存在i,j(j
在反转i的时候我们考虑从局面i转移到局面j,那么我们会有两个局面j。
表示第j枚被反转了2次,也就是回到了背面朝上的状态。
那么我们得到这个游戏一个性质:当出现两个同样的局面时,等价于这两个局面合并变成了局面0。
这种情况在Nim游戏中是没有的,那么它会对Nim游戏的状态造成影响么?
我们想一想,在Nim游戏中,如果出现两个数量相同的堆时,比如A[i]=A[j]。在计算Nim游戏状态时我们采用的xor操作,xor有交换律
和结合律。则我们可以变成:
(A[i] xor A[j]) xor Other
因为A[i] = A[j],所以A[i] xor A[j] = 0。上式实际就是:
0 xor Other
也就是说在原Nim游戏中,若出现了两个数量相同的堆时,实际上这两堆已经不对总局面造成影响了,也就可以认为这两对合并为了
一个数量为0的堆。
到此,我们可以发现这个硬币游戏完全满足Nim游戏的规则,其解答也满足Nim游戏的性质,这题也就很简单的转化为了普通的Nim
游戏。在实际的博弈游戏中会发现很多都是可以转化为Nim游戏模型。如何正确的建立模型和转化游戏模型也就是解决博弈游戏一个
很重要的手段。
#include
const int maxn = 1e4 + 10;
using namespace std;
char s[maxn];
int main()
{
int n;
int x;
int ans = 0;
cin>>n;
scanf("%s",s);
for(int i = 0;i < n ;i++)
{
if(s[i] == 'H')
ans ^= (i+1);
}
if(ans == 0)
puts("Bob");
else
puts("Alice");
return 0;
}
hihocoder 1173
HDU - 5795
题意:
在这一次游戏中Alice和Bob决定在原来的Nim游戏上增加一条规则:每一次行动时,不仅可以选择一堆取走任意数量的石子(至少取1
颗,至多取出这一堆剩下的所有石子),还可以选择将一堆石子分成两堆石子,但并不取走石子。比如说有一堆石子为k个,当Alice或
者Bob行动时,可以将这一堆石子分成两堆,分别为x,y。满足x+y=k,x,y>0。那么增加了这一条规则后,在Alice总先手的情况下,请
你根据石子堆的情况判断是Alice会获胜还是Bob会获胜?
思路:
这两个题目基本一样,根据sg函数的定理:
对于多个单一游戏,X=x[1..n],每一次我们只能改变其中一个单一游戏的局面。则其总局面的sg值等于这些单一游戏的sg值
异或和。
即:
sg(X) = sg(x[1]) xor sg(x[2]) xor … xor sg(x[n])
我们只需要算单个的就好. 由于数据比较大,我们不可能把所有的sg函数值处理出来,所以打表找找规律.
#include
const int maxn = 2e4+10;
using namespace std;
int n;
int a[111];
bool vis[maxn];
int sg[maxn];
void init()
{
sg[0] = 0;
for(int i = 1;i <= 100;i++)
{
for(int j = 1;j <= 100;j++) vis[j] = 0;
for(int j = 1;j < i ;j++)
{
vis[sg[j]] = 1;
vis[sg[j] ^ sg[i - j]] = 1;
}
for(int j = 1;j <= 100;j++)
{
sg[i] = j;
if(!vis[j])
{
break;
}
}
}
for(int i = 1;i <= 100;i++)
printf("%d %d\n",i,sg[i]);
return ;
}
int main()
{
//init();
while(cin>>n)
{
int ans = 0;
for(int i = 1;i <= n;i++)
{
int x;
scanf("%d",&x);
if(x % 4 == 0)
{
ans ^= (x - 1);
}
else if( (x+1) % 4 == 0)
{
ans ^= (x + 1);
}
else
ans ^= x;
}
if(ans == 0)
puts("Bob");
else
puts("Alice");
}
return 0;
}
#include
typedef long long ll;
const int maxn = 2e4+10;
using namespace std;
int n;
ll a[111];
bool vis[maxn];
ll sg[maxn];
void init()
{
sg[0] = 0;
for(int i = 1;i <= 100;i++)
{
for(int j = 1;j <= 100;j++) vis[j] = 0;
for(int j = 1;j < i ;j++)
vis[sg[j]] = 1;
for(int j = 1;j < i;j++)
for(int k = 1;k < i - j;k++)
vis[sg[j] ^ sg[k] ^ sg[i-j-k]] = 1;
for(int j = 1;j <= 100;j++)
{
sg[i] = j;
if(!vis[j])
{
break;
}
}
}
for(int i = 1;i <= 100;i++)
printf("%d %d\n",i,sg[i]);
return ;
}
int main()
{
// init();
int _;
cin>>_;
while(_--)
{
cin>>n;
ll ans = 0;
for(int i = 1;i <= n;i++)
{
ll x;
scanf("%lld",&x);
if(x % 8 == 0)
{
ans ^= (x - 1);
}
else if( (x+1) % 8 == 0)
{
ans ^= (x + 1);
}
else
ans ^= x;
}
if(ans == 0)
puts("Second player wins.");
else
puts("First player wins.");
}
return 0;
}
zoj 3599
K倍动态减法游戏. 当k为2时,为斐波那契博弈.
这个东西比较高深啊...看了好久也只是懂了一点.
#include
#include
using namespace std;
long long a[3000000], b[3000000];
//a[] 所有的必败态的数
//b[] 保存 a[0...i] 组合能够构造的最大数字
int main (void)
{
int t;
scanf("%d", &t);
while(t --)
{
int m;
long long n;
scanf("%d %lld", &m, &n);
a[0] = b[0] = 1;
int i = 0, j = 0;
while(n > a[i])
{
i ++;
a[i] = b[i - 1] + 1;
while(a[j + 1] * m < a[i])
j++;
if(a[j] * m < a[i])
b[i] = b[j] + a[i];
else
b[i] = a[i];
}
long long ans;
if(n == a[i]) //a 数组存的全部是必败态的数
ans = (long long)n - i - 1;//因为n不能取 1 所以要减1
else
ans = (long long)n - i;//i是a的下标,也正好是比N小的不能取的数的个数 (包括1)
printf("%lld\n", ans);
}
return 0;
}
除法游戏:
题目:http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2959
题意:有一个n*m的矩阵,每个元素均为2~10000之间的正整数。两个游戏者轮流操作。每次可以选一行中的1个或者多个大于1的整数,把
它们中的每个数都变成它的某个真因子,比如12可以变成1,2,3,4或者6,不能操作的输(换句话说,如果在谁操作之前,
矩阵中的所有数都是1,则他输)。
分析:考虑每个数的素因子个数,比如12包含3个素因子,则让一个数变成它的真因子等价于拿掉它的一个或者多个素因
子。这样,每行对应于一个石堆,每个数的每个素因子看成是一颗石子,则本题就和Nim游戏完全等价了。