1.
博弈
给定 堆物品,第
堆物品有
个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可以把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否能胜。
定理
博弈先手必胜,当且仅当
博弈先手必败,当且仅当
2. 公平组合游戏 
若一个游戏满足:
- 有两名玩家交替行动。
- 在游戏过程的任意时刻,可以执行的合法行动与轮到哪名玩家无关。
- 不能行动的玩家判负。
则称该游戏为一个公平组合游戏。 博弈属于公平组合游戏,而常见的棋类游戏,如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件
和条件
。
3. 有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法:把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
4.
运算
设 表示一个非负整数集合。定义
为求出 不属于 集合
的最小非负整数的运算,即:
5.
函数
在有向图游戏中,对于每个节点 ,设从
出发共有
条有向边,分别到达节点
,定义
为
的后继节点
的
函数值构成的集合 再执行
运算的结果,即:
特别地,整个有向图游戏 的
函数值被定义为有向图游戏起点
的
函数值,即
。
求 函数使用到 记忆化搜索
int sg(int x){
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=1;i<=m;i++){
if(x>=s[i]) S.insert(sg(x-s[i]));
}
for(int i=0;;i++)
if(!S.count(i))
return f[x]=i;
}
5. 有向图游戏的和
设 是
个有向图游戏。定义为有向图游戏
,它的行动规则是任选某个有向图游戏
,并在
上行动一步。
被称为有向图游戏
的和
有向图游戏的和的 函数值等于它包含的每个子游戏
函数值的异或和,即:
定理
-
有向图游戏的某个局面必胜,当且仅当该局面对应节点的
函数值大于
-
有向图游戏的某个局面必败,当且仅当该局面对应节点的
函数值等于
例题:
1. AcWing 891. Nim游戏
游戏模板题
#include<bits/stdc++.h>
using namespace std;
int n;
int main(){
scanf("%d", &n);
int ans=0;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
ans^=x;
}
if(ans) printf("Yes\n");
else printf("No\n");
return 0;
}
2. AcWing 893. 集合-Nim游戏
有向图游戏模板题
运用到 函数,此题
堆石子,对于每一堆石子求其
的值,然后将
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int n,m;
int s[N],f[N];
int sg(int x){
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=1;i<=m;i++){
if(x>=s[i]) S.insert(sg(x-s[i]));
}
for(int i=0;;i++)
if(!S.count(i))
return f[x]=i;
}
int main(){
scanf("%d",&m);
for(int i=1;i<=m;i++) scanf("%d",&s[i]);
scanf("%d",&n);
memset(f,-1,sizeof(f));
int ans=0;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
ans^=sg(x);
}
if(ans) printf("Yes\n");
else printf("No\n");
return 0;
}
3. AcWing 892. 台阶-Nim游戏
对于偶数台阶上的石子,可以看作如果第一个人将偶数台阶上的石子移动 个到奇数台阶,那么第二个人可以相反,将
奇数台阶上的石子移动
个石子到偶数台阶上。当到最后的一号台阶时,可以看作第一个人移动
个石子到一号台阶,第二个人可以将
个石子移到地面上,所以偶数台阶对答案无影响。
对于奇数台阶来说,让他们全部异或一下,当 时,先手必胜,反之必败。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
scanf("%d",&n);
int ans=0;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
if(i%2) ans^=x;
}
if(ans) printf("Yes\n");
else printf("No\n");
return 0;
}
4. AcWing 894. 拆分-Nim游戏
题目大意:每一堆可以变成小于原来那堆的任意大小的两堆
函数理论,多个独立局面的
值,等于这些局面
值的异或和。因此
,对于一堆
个的石子,其
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int f[N];
int sg(int x){
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=0;i<x;i++)
for(int j=0;j<=i;j++)
S.insert(sg(i)^sg(j));
// mex 操作
for(int i=0;;i++)
if(!S.count(i))
return f[x]=i;
}
int main(){
int n,ans=0;
memset(f,-1,sizeof(f));
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
ans^=sg(x);
}
if(ans) printf("Yes\n");
else printf("No\n");
return 0;
}
5. AcWing 1319. 移棋子游戏
有向图游戏模板题
求出每个点的 值,然后将有棋子的点的
值
在一起就是答案
#include<bits/stdc++.h>
using namespace std;
const int N=2010;
struct node{
int to,nex;
}e[N*3];
int cnt,head[N];
int n,m,k,f[N];
void add(int u,int v){
e[++cnt].nex=head[u];
e[cnt].to=v;
head[u]=cnt;
}
int sg(int x){
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=head[x];i;i=e[i].nex){
int y=e[i].to;
S.insert(sg(y));
}
for(int i=0;;i++)
if(!S.count(i))
return f[x]=i;
}
int main(){
memset(f,-1,sizeof(f));
scanf("%d%d%d",&n,&m,&k);
while(m--){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
int ans=0;
for(int i=1;i<=k;i++){
int x;
scanf("%d",&x);
ans^=sg(x);
}
if(ans) printf("win\n");
else printf("lose\n");
return 0;
}
6. AcWing 1321. 取石子
需要推结论,不太会
#include<bits/stdc++.h>
using namespace std;
const int N=55,M=50050;
int f[N][M];
int dp(int a,int b){
int &v=f[a][b];
if(v!=-1) return v;
if(!a) return b%2;
if(b==1) return dp(a+1,0);
if(a&&!dp(a-1,b)) return v=1;
if(b&&!dp(a,b-1)) return v=1;
if(a>=2&&!dp(a-2,b+(b?3:2))) return v=1;
if(a&&b&&!dp(a-1,b+1)) return v=1;
return 0;
}
int main(){
int T;
scanf("%d",&T);
memset(f,-1,sizeof(f));
while(T--){
int n,a=0,b=0;
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
if(x==1) a++;
else b+=b?x+1:x;
}
if(dp(a,b)) printf("YES\n");
else printf("NO\n");
}
return 0;
}
7. 1322. 取石子游戏
很难,用到区间 和推结论,不太会
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int l[N][N],r[N][N],n,a[N];
int T;
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int len=1;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(len==1) l[i][j]=r[i][j]=a[i];
else{
int L=l[i][j-1],R=r[i][j-1],X=a[j];
if(R==X) l[i][j]=0;
else if((X<L&&X<R)||(X>L&&X>R)) l[i][j]=X;
else if(L>R) l[i][j]=X-1;
else l[i][j]=X+1;
L=l[i+1][j]; R=r[i+1][j]; X=a[i];
if(L==X) r[i][j]=0;
else if((X<L&&X<R)||(X>L&&X>R)) r[i][j]=X;
else if(R>L) r[i][j]=X-1;
else r[i][j]=X+1;
}
}
}
if(n==1) printf("1\n");
else printf("%d\n",l[2][n]!=a[1]);
}
return 0;
}