事先说明:这不是好题解,这只是思考记录
同机房的xp都学博弈了、、 感觉还是学学看吧
先来看一下简单的nim:
题目:
Alice和Bob放置了N堆不同的石子,编号1..N,第i堆中有A[i]个石子。
每一次行动,Alice和Bob可以选择从一堆石子中取出任意数量的石子。至少取1颗,至多取出这一堆剩下的所有石子。
Alice和Bob轮流行动,取走最后一个石子的人获得胜利。
假设每一轮游戏都是Alice先行动,请你判断在给定的情况下,如果双方都足够聪明,谁会获得胜利?
题目太长了!画个图压压惊:
如果你有博弈恐惧症,那你可以看一下比题目还短的代码来提振信心:
#include <bits/stdc++.h>
using namespace std;
int n, p, a;
int main() {
while (cin >> n) {
for (int i = 0; i < n; ++i) {
cin >> a;
if (i == 0) p = a;
else p ^= a;
}
if (p == 0) cout << "Bob" << endl;
else cout << "Alice" << endl;
}
return 0;
}
好了,开始学习预备知识:
首先认识一下公平组合游戏(ICG):
它的规则是:
1、有两名玩家(p1、p2)
2、每一个玩家可以动 游戏里的每个棋子(没有红方、黑方的棋子之分、 、“动”指和原来的局面不一样)
3、不许作弊、、没有buff、、没人开黑
4、动不了的选手输(所以游戏就是比谁拿走最后一个、、)
只有两个玩家非常机智才能叫博弈
当然,如果两个ZZ玩游戏,那就叫菜鸡互啄、、
所以两个玩家都做出最优的决定;;(你知道我的套路,我知道你的套路,如果我进入你的套路我就输了)
所以先手有主动权,可以一步使对方进入自己的套路
而有时,游戏一开始的局面就是对方的套路,那就相当于对方先把你套路了,然后你就没办法了、、
所以对于这类游戏,胜负取决于初始局面、、
研究这种局面的胜负 就叫狭义的 博弈论、、
所以局面分两类:
P-position:在当前的局面下,先手必败。
N-position:在当前的局面下,先手必胜。
他们有如下性质:
1.合法操作集合为空的局面是P-position;
2.可以移动到P-position的局面是N-position;
3.所有移动都只能到N-position的局面是P-position。
对于1:动不了的肯定输
对于2:这里的先手是相对的 这一回合你是先手,下一回合他是先手,如果你冰雪聪明是可以把下一个先手(对方,也就是这一个后手)套路的
如果你不聪明,那你玩游戏就不能叫博弈
对于3:无论如何下一个先手都赢了,你就只能gg了
根据性质3是可以递推的、、但一般写记搜+递归
所以,我们将p写作0,n写作1
所以 0前面都是1
1后面一定有0 、、囧
就像这个:
如果
对于上图的路径特点我们可以想到什么?
就是二进制的 异或^
但为什么要用每一堆的石子数 异或?
证明(来自百度百科):
我个人感觉这应该只是异或的性质和nim游戏的博弈法则不谋而合
所以等量代换了、、
sg函数:
它是博弈的核心;;;
它存的是每个状态(石子数);;
先来看几个简单的知识:
游戏的和:
如果每次游戏可以分成n个子游戏,每次只能操作其中1个子游戏:
f(总)=f(第一个子游戏)^f(第二个子游戏)^f(第三个子游戏)…………^f(第n个子游戏)
结合nim很好证明
下面几个图帮助理解:
由于每个子游戏相同,堆中数相同时输赢相同,可以简化为:
注意是两个棋子在起点
这就可以递归判断当前局面
有了这些知识和模型,平等博弈类的问题已经可以解答了
对于这个题:
我们先划分子游戏,以便合为这个分裂游戏:
每个堆的转移是会互相影响的(++--)
但是每个石子的转移相互独立(一个石子的转移不会影响另一个石子的转移)
所以每个石子都是一个游戏、、
在每一堆里,每个石子都有不同的sg值
一颗在i被取走的石子会在j、k加入一颗石子
我们注意到只有最后一个有石子的情况是P
也就是说所有的石子随着时间推移都会被扔到最后一个
所以当枚举堆数到了最后一堆时,就是terminal position(末端情况)
所以取走当前的石子游戏可以看成是往右的j、k位置的方案
另外我们注意到石子的具体个数是没有关系的,只与奇偶性有关(所有>1的偶数都早晚要被互拿转移为0,所以就当它是0,所有>1的奇数都会被拿为1,即交换了一次先后手,所以会对答案造成影响)
所以需要计算玩i这个石子(虽然一点都不好玩),也一定要玩到j、k 一直玩到n,,就一路异或即可
所以玩这个石子的情况=初始局面 -i的1 +j的1 +k的1、、
由于你可以玩石子数为奇数的堆数颗石子的取一+2游戏
所以总游戏就是 取这些堆的子游戏异或和^sg(i)^sg(j)^sg(k)(sg是一大串异或,是初始局面 由于异或的性质,+和-都可以用^表示)
码:
#include<iostream>
#include<cstdio>
using namespace std;
#include<cstring>
int n,j,i,k,f[30],T,x,ans,cnt;
bool vis[40];
int getsg(int x)
{//cout<<x;
int j;
if(x==n)return 0;//cout<<f[x]<<endl;
if(f[x]!=-1)return f[x];
memset(vis,0,sizeof(vis));
for(int j=x+1;j<=n;j++)
for(int k=j;k<=n;k++)
{
vis[getsg(j)^getsg(k)]=1;
// cout<<getsg(j)<<" "<<getsg(k)<<endl;
}
for(j=0;vis[j];j++);//不要脑残的写: for(int j;vis[j],j++);
return f[x]=j;
}
int main()
{
scanf("%d",&T);
while(T--)
{
ans=cnt=0; memset(f,-1,sizeof(f));//一开始没清0
scanf("%d",&n);
for(i=1;i<=n;i++)
{
scanf("%d",&x);
if(x&1)ans^=getsg(i);//统计初始局面
// cout<<ans<<endl;
}
for(i=1;i<n;i++)
for(j=i+1;j<=n;j++)
for(k=j;k<=n;k++)
{
if(!(ans^getsg(i)^getsg(j)^getsg(k)))if((++cnt)==1)printf("%d %d %d\n",i-1,j-1,k-1);
}
if (!cnt) puts("-1 -1 -1"); printf("%d\n",cnt); //输出-1 -1 -1 隔开 输出方案总数
}
}