本篇可能会借用很多其他大佬博客的内容,最后会一并贴上链接。
最近开始学习博弈,基础的题目做的也算差不多了。然而稍微一变形就不会了= =,先大概总结一下最近的学习成果,不过还是有很多题目还没理解的。之后会慢慢更新吧。
首先就是博弈的分类吧,acm里的博弈就是双方进行轮流的操作,当双方都采用最优操作时判断当前局面的输赢。
基础的博弈分为三大类:
1.巴什博弈:只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。
这个问题其实稍加思索就可以得出答案,后手可以保证每一次 先手和后手取的物品个数都为m+1个,那么可以得出结论 若 n=(m+1)*r+s 那么我先手拿走s个,无论怎么拿先手最后都可以赢。反之同理
个人感觉这是三大基础博弈里面从模型上来看最简单的一种了。
2.威佐夫博弈:有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
这个博弈我是一直都不理解,这里贴下大神博客的讲解。
这种情况下是颇为复杂的。我们用(ak,bk)(ak ≤ bk ,k=0,1,2,…,n)表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。
可以看出,a0=b0=0,ak是未在前面出现过的最小自然数,而 bk= ak + k,奇异局势有如下三条性质:
1。任何自然数都包含在一个且仅有一个奇异局势中。
由于ak是未在前面出现过的最小自然数,所以有ak > ak-1 ,而 bk= ak + k > ak-1 + k-1 = bk-1 > ak-1 。所以性质1。成立。
2。任意操作都可将奇异局势变为非奇异局势。
事实上,若只改变奇异局势(ak,bk)的某一个分量,那么另一个分量不可能在其他奇异局势中,所以必然是非奇异局势。如果使(ak,bk)的两个分量同时减少,则由于其差不变,且不可能是其他奇异局势的差,因此也是非奇异局势。
3。采用适当的方法,可以将非奇异局势变为奇异局势。
假设面对的局势是(a,b),若 b = a,则同时从两堆中取走 a 个物体,就变为了奇异局势(0,0);如果a = ak ,b > bk,那么,取走b – bk个物体,即变为奇异局势;如果 a = ak , b < bk ,则同时从两堆中拿走 ak – ab + ak个物体,变为奇异局势( ab – ak , ab – ak+ b – ak);如果a > ak ,b= ak + k,则从第一堆中拿走多余的数量a – ak 即可;如果a < ak ,b= ak + k,分两种情况,第一种,a=aj (j < k)从第二堆里面拿走 b – bj 即可;第二种,a=bj (j < k),从第二堆里面拿走 b – aj 即可。
从如上性质可知,两个人如果都采用正确操作,那么面对非奇异局势,先拿者必胜;反之,则后拿者取胜。
那么任给一个局势(a,b),怎样判断它是不是奇异局势呢?我们有如下公式:
ak =[k(1+√5)/2],bk= ak + k (k=0,1,2,…,n 方括号表示取整函数)
奇妙的是其中出现了黄金分割数(1+√5)/2 = 1。618…,因此,由ak,bk组成的矩形近似为黄金矩形,由于2/(1+√5)=(√5-1)/2,可以先求出j=[a(√5-1)/2],若a=[
j(1+√5)/2],那么a = aj,bj = aj + j,若不等于,那么a = aj+1,bj+1 = aj+1+ j + 1,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异局势。
3.尼姆博奕:有k堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
这我个人感觉是最神奇的一个博弈模型,nim博弈的神奇之处在于最后结论异常的简单,但是其推导的过程确是在任何博弈问题里面非常适用的一种思路,即由小情况到大情况慢慢推导,这里推导过程推荐取看一篇国家集训队论文——《由感性认识到理性认识透析一类搏弈游戏的解答过程》里面的讲解非常详细。
这里只说下结论把每堆石子的个数异或起来,若答案为0则后手胜,否则先手胜。
4.阶梯博弈:阶梯博弈的模型大概就是 有1到n个格子,每个格子内有一定数量的石子。每次在一个格子内选择一些数量(大于1)的石子移动到它前一个的格子内,注意可以移动到为0的格子,即移出游戏。谁不能操作谁输。
阶梯博弈可以通过某种神奇的方式转换为只对奇数格子上的石子做nim和,为什么可以这样转化呢。
首先我们可以考虑,当我们目前如果必胜的话, 如果我们移动奇数堆的石子到偶数堆,那就相当于nim博弈中从这些堆中拿走一些石子,如果对手选择移动偶数堆的石子到奇数堆,那么我们可以从他移动的奇数堆中和他移动相同的石子到下一个偶数堆,这样对于整个游戏来说其实奇数堆的个数并没有影响。所以整个游戏最后就相当于对奇数堆的石子个数做nim和。剩下的就和nim博弈一样了。
5.sg值。
SG值:一个点的SG值就是一个不等于它的后继点的SG的且大于等于零的最小整数。
后继点:也就是按照题目要求的走法(比如取石子可以取的数量,方法)能够走一步达到的那个点。
先贴上概念,sg值是博弈里面个人感觉很重要的一个内容,因为我现在还属于入门阶段,所以sg值在解题中用的不多,主要的可能还是用来打表找规律,但是毫无疑问sg是非常重要的。
接下来贴一些题目,因为可能题目较多,所以就不贴题目链接了,可以自己去找,或者vjudge搜索 [kuangbin]ACM博弈基础题目 即可。
hdu-1079:Calendar Game
这个题目主要是找规律吧,但是不知道是我没看懂还是什么,题目为给你一个日期,每次可以把月份往后推1或者把日期往后推1,谁先达到2001 11 4谁就赢。
关于这个题主要的解法是找规律,网上有很多的代码,利用月份+天数的奇偶来判断先手和后手的胜负,其中只有两个特殊日期 9.30 11.30除外,然后判断我先手如果能赢的话我肯定不会选择进入这两个日期,其次如果后手能赢他也不会选择进入这两个特殊日期,那么需要判断的就是这两个日期作为初始日期的情况。
但是我有个疑问,就是题解普遍都说只有9.30和11.30为特殊日期,即向后移一天不会改变奇偶性,但是我感觉这样的有很多啊 例如 2.28-3.1号,这两个日期的奇偶性不都是偶数嘛,反正很费解诶= = 可能是我题目哪理解错了
Ac代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
const ll INF=2e9+7;
int y,m,d;
int main()
{
//2001 11 4
int QAQ;
scanf("%d",&QAQ);
while(QAQ--)
{
scanf("%d%d%d",&y,&m,&d);
if((m+d)%2==0||(m==9&&d==30)||(m==10&&d==30)) //注意特殊日期
printf("YES\n");
else
printf("NO\n");
}
//system("pause");
}
hdu-1525: Euclid's Game
给两个数字,每个人每次操作只能将大的数字减去小数字的整数倍,谁先任意把一个数字减到0谁赢。
首先分析,如果出现了a%b==0的情况,则胜负已经出来了。
其次我感觉这道题有一点猜测的成分在里面,例如对于一个局面a,b 肯定会有一个必胜或者必败的状态,因为每个人都是最优策略,所以操作的人一定知道a%b b的胜负态,如果
a%b,b是必败态,我先手直接操作到该状态,如果是必胜态我就操作至a%b+b,b 这样后手只能将当前状态操作至必胜态。但是这要求我们的a>2*b,如果不符合那只能进行a=a-b
的操作了。大概的过程差不多就是这样。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int MOD=1e9+7;
const int INF=1e9+7;
const double eps=1e-18;
typedef long long ll;
int a,b,flag;
int main()
{
while(scanf("%d%d",&a,&b)!=EOF)
{
flag=0;
if(a==0&&b==0) break;
while(1)
{
flag^=1; //记录胜负
if(a<b) swap(a,b);
if(a%b==0||a>2*b) break; //如果到可以结束的状态
a=a-b;
}
if(flag) printf("Stan wins\n");
else printf("Ollie wins\n");
}
//system("pause");
return 0;
}
hdu-1564:Play a game
这个题目,算找规律吧。之前队友在校赛出过一道类似的题目,印象比较深刻。但是证明不是很会,这里贴一个别人的博客:点击打开链接
代码就不贴了。
hdu-1846:Brave Game
简单的巴什博弈
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int MOD=1e9+7;
const int INF=1e9+7;
const double eps=1e-18;
typedef long long ll;
int n,m;
int main()
{
int QAQ;
scanf("%d",&QAQ);
while(QAQ--)
{
scanf("%d%d",&n,&m);
if(n%(m+1)==0) printf("second\n");
else printf("first\n");
}
//system("pause");
return 0;
}
hdu-1847:Good Luck in CET-4 Everybody!
这个就用到了sg找规律,但是直接裸跑sg也能过,可以用这个题目学习一下sg的用法,继而了解一下sg的概念。
打完表后规律很好找,输出看一下即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
const int MOD=1e9+7;
const int INF=1e9+7;
const double eps=1e-18;
typedef long long ll;
int n,f[20],sg[1005];
void init()
{
f[1]=1;
for(int i=2;i<=15;i++)
f[i]=f[i-1]*2;
}
int mex(int x) //打sg表 记忆化搜索
{
if(sg[x]) return sg[x];
bool vis[maxn];
memset(vis,0,sizeof vis);
for(int i=1;i<=15;i++)
{
int sk=x-f[i];
if(sk<0) break;
sg[sk]=mex(sk);
vis[sg[sk]]=1;
}
for(int i=0;i<maxn;i++)
if(!vis[i]) return sg[x]=i;
}
int main()
{
init();
while(scanf("%d",&n)!=EOF)
{
memset(sg,0,sizeof sg);
if(mex(n)) printf("Kiki\n");
else printf("Cici\n");
}
//system("pause");
return 0;
}
hdu-2516:取石子游戏
这个题目 怎么说呢 推了一下前几个,发现是斐波那契数列,就很简单了,但是这个题目还有个加强版的,现在还不会= =
代码就不贴了。
hdu-2876:邂逅明下
巴什博弈模型,只是转换成了谁最后拿谁输,其实差不多,n=r*(p+q)+s。
我们可以发现其实跟之前的差不多,先手如果可以拿小于s个的物品,那么之后一直保持(p+q)的循环,一定就会赢。反之如果必须拿大于s个物品,那恰好相反。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int MOD=1e9+7;
const int INF=1e9+7;
const double eps=1e-18;
typedef long long ll;
int n,p,q;
int main()
{
while(scanf("%d%d%d",&n,&p,&q)!=EOF)
{
int g=n%(p+q);
if(g==0||g>p) printf("WIN\n"); //跟分析的情况一样
else printf("LOST\n");
}
//system("pause");
return 0;
}
hdu-3032:Nim or not Nim?
正常nim博弈,但是增加了一个操作就是可以把一堆分为两个小堆。然后想了好久,不会做,看别人的题解发现是打表找规律。。。
继续学习sg的打表hhhh
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+5;
typedef long long ll;
int n,a[maxn],sg[maxn];
int mex(int x)
{
if(sg[x]>=0) return sg[x];
bool vis[maxn]={0};
for(int i=1;i<=x;i++)
vis[mex(x-i)]=1;
for(int i=1;i<x/2+1;i++)
vis[mex(x-i)^mex(i)]=1;
for(int i=0;i<maxn;i++)
if(!vis[i]) return sg[x]=i;
}
int vs(int x)
{
if(x==0) return 0;
if(x%4==3) return x+1;
if(x%4==0) return x-1;
return x;
}
int main()
{
//memset(sg,-1,sizeof sg); //打表看规律
//sg[0]=0;sg[1]=1;
//for(int i=0;i<=100;i++)
//printf("---%d %d\n",i,mex(i));
int QAQ;
scanf("%d",&QAQ);
while(QAQ--)
{
int win=0;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),win^=vs(a[i]); //根据规律进行正常nim和
if(win==0) printf("Bob\n");
else printf("Alice\n");
}
return 0;
}
hdu-3389:Game
这个题目目前还没有搞懂 暂时搁置= =
hdu-3354:Alice's Game
这个题目 有点厉害说实话,看了好久,根本不知道从哪里开始入手,后来看了别人的题解= = 大佬是真的强。
这个题目主要就在于对于每个巧克力算出每个巧克力对两人的贡献,最后比较 贴一个别人的博客 讲解非常的详细:点击打开链接
代码仅当参考
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const ll INF=1e18+7;
const int mod=998244353;
int n,x[maxn],y[maxn];
ll ans1,ans2;
int main()
{
int QAQ,kase=0;
scanf("%d",&QAQ);
while(QAQ--)
{
ans1=0,ans2=0;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&x[i],&y[i]);
for(int i=1;i<=n;i++)
{
while(x[i]>1&&y[i]>1) x[i]=x[i]>>1,y[i]=y[i]>>1;
if(y[i]==1) ans1+=(x[i]-1);
if(x[i]==1) ans2+=(y[i]-1);
}
printf("Case %d: ",++kase);
if(ans1<=ans2) printf("Bob\n");
else printf("Alice\n");
}
//system("pause");
}
HDU - 2177 威佐夫博弈:
输出也有若干行,如果最后你是败者,则为0,反之,输出1,并输出使你胜的你第1次取石子后剩下的两堆石子的数量x,y,x<=y。如果在任意的一堆中取走石子能胜同时在两堆中同时取走相同数量的石子也能胜,先输出取走相同数量的石子的情况.
Ac代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
typedef long long ll;
int a,b;
map<int,int> ma;
int main()
{
while(scanf("%d%d",&a,&b)!=EOF)
{
ma.clear();
if(a==0&&b==0) break;
if(a>b) swap(a,b);
double pa=(sqrt(5.0)+1)/2.0;
int mi=(b-a)*pa;
if(a==mi) printf("0\n");
else
{
printf("1\n");
if(mi<=a) printf("%d %d\n",mi,mi+(b-a)); //同时取
for(int i=1;i<=b;i++) //每一堆单取
{
int bb=(b-i);
int sj=pa*(max(bb,a)-min(bb,a));
if(min(bb,a)==sj) ma[min(bb,a)]=max(bb,a);
}
for(int i=1;i<=a;i++)
{
int aa=a-i;
int sj=pa*(max(aa,b)-min(b,aa));
if(min(b,aa)==sj) ma[min(b,aa)]=max(b,aa);
}
for(auto it=ma.begin();it!=ma.end();it++)
printf("%d %d\n",it->first,it->second);
}
}
return 0;
}
HDU - 2176:m堆石子游戏
nim博弈 问你如果能赢应该如何去取石子
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
typedef long long ll;
int n,a[maxn];
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==0) break;
int win=0;
for(int i=1;i<=n;i++) scanf("%d",&a[i]),win^=a[i];
if(win==0) printf("No\n");
else
{
printf("Yes\n");
for(int i=1;i<=n;i++)
{
int now=win^a[i];
if(now<a[i])
printf("%d %d\n",a[i],now);
}
}
}
return 0;
}
有点累了= = 以后继续再更,