博弈论与SG函数
巴什博奕:
两个顶尖聪明的人在玩游戏,有n个石子,每人可以随便拿1−m个石子,不能拿的人为败者,问谁会胜利
结论:
设当前的石子数为\(n=k∗(m+1)\)即\(n%(m+1)==0\)时先手一定失败
HDU1846
#include<iostream>
using namespace std;
int main() {
int C,N,M;
scanf("%d",&C);
while(C--) {
scanf("%d%d",&N,&M);
if(N%(M+1)==0) printf("second\n");
else printf("first\n");
}
return 0;
}
HDU4764
#include<cstdio>
#include<algorithm>
const int MAXN=1e6+10,INF=1e9+10;
int main() {
int x,y;
while(scanf("%d%d",&x,&y)) {
if(!x&&!y) break;
if( (x-1)%(y+1)==0 ) puts("Jiang\n");
else puts("Tang\n");
}
return 0;
}
nim游戏:
有两个顶尖聪明的人在玩游戏
有n堆石子,两个人可以从任意一堆石子中拿任意多个石子(不能不拿),没法拿的人失败。问谁会胜利
结论:
当\(n\)堆石子的数量异或和等于\(0\)时,先手必败,否则先手必胜
简证:
定义当前状态为
P-position:在当前的局面下,先手必败
N-position:在当前的局面下,先手必胜
对于面对局面为
\(0\ xor\ 0\ xor\ ...\ 0 = 0\)称它为p局面
若当前局面为
\(a_1\ xor\ a_{2}\ ...a_n = k\) 来说
我们可以通过改变一个\(a_i\)的比如\(a_i\ xor \ k\)值来使得上式的值为0,此时修改该操作的人为必败局面,由于\(xor\)计算的特殊性,我们知道一定有一个\(a_i\)最高位与\(k\)最高位的1是相同的,那么必然有\(a_i\ xor\ k\ <a_i\)的,所以操作可为减法
对于局面
\(a_1\ xor\ a_{2}\ ...a_n = 0\)
那么此时对于任何一种操作都会使得上式的值发生改变,也就是不为0
对与xor的性质,当每个位置的1都存在偶数个时,异或和为0,只改变一个\(a_i\),一定会使得某个位置的1的个数发生改变
所以只有当先手面临\(xor\)和为0是,必胜
luogu P2197 nim游戏
#include<cstdio>
#include<iostream>
#include<algorithm>
int main() {
int T;
//scanf("%d",&T);
std::cin>>T;
for(int n;T--;) {
std::cin>>n;
int tmp=0;
for(int q,i=1;i<=n;++i) {
std::cin>>q;tmp^=q;
}
if(!tmp)puts("No");
else puts("Yes");
}
//char a[10];scanf("%s",a);puts(a);
return 0;
}
SG函数
SG函数内容的理解借鉴自zyf学长,鸣谢!QUQ
对于nim游戏,你已经知道怎么做了
但是,要是稍加改变游戏规则,那么恐怕无法很快找出必胜策略
我们可以吧游戏抽象为一张图,每一个点代表一个状态,对于每个状态与他的子状态连一条有向边。那么,我们在这张有向无环图上引入SG函数
定义
定义运算\(mex()\),这是对于一个集合的运算,表示最小的不属于该集合的非负整数
例如\(mex\{0,1,2,3\}=4;mex\{2,3,5\}=0\)。
那么对于每个顶点的SG值为:
后继中未出现过的最小值
y为x的后继
SG函数的性质
首先对于出度为0的点,他的SG值为0
对于一个SG值为0的点x,他的后继y都满足sg(y)!=0,y的后继中一定有存在一个点的SG值为0
诶,是不是和刚才nim游戏很像
所以当SG(x)=0时,x代表了p局面
扩展
考虑有向图上有n个棋子,每次可以移动一颗,不能移动者lose,这时候怎样找到必胜策略呢?
我们来考虑每个顶点的SG值得含义,当\(sg(x)=k\)时,x的后继y的\(sg(y)=1\ ->\ k\)也就是说,某个棋子进行移动后sg值是一定会发生改变的
类比普通的nim游戏,Nim游戏的一种必胜策略就是把\(a_i\)变为k,
那么,把第i个棋子移动到一个SG值为k的点上就是一种必胜策略
hhhh,也就是说Nim游戏的一种必胜策略对应着这n个棋子的必胜策略
- 那么这个游戏P局面就棋子所在位置的SG函数异或和为0的时候。
经典的博弈问题
再来说一下几类经典的博弈问题
Anti-Nim游戏和SJ定理
有两个顶尖聪明的人在玩游戏(他们一个是崩崩崩玩家一个是舰娘玩家),游戏规则是这样的:
有n堆石子,两个人可以从任意一堆石子中拿任意多个石子(不能不拿),拿走最后一个石子的人失败。问谁会胜利?
结论
先手必胜当且仅当:
所有堆的石子数都为1且游戏的SG值为0
有些堆的石子数大于1且游戏的SG值不为0
证明
1)游戏有三种情况
每堆只有一个石子
- 异或和为0先手必胜
- 异或和不为0先手必败
2)只存在一堆石子数>1,则先手必胜
可以发现,在这种情况下异或值一定 != 0
并且先手一定可以使剩下的奇数个石子变为个数为1的堆
3)存在2堆以上的石子数>1
-异或和=0时先手必败
-异或和!=0先手必胜
那么
考虑nim游戏,当异或和=0时,洗衣不操作都会使异或和!=0,相反同样....符合NP状态的转换
不断进行转换会在一个时刻变为 ‘只有一堆大于1' 的局面,而该局面一定是有 ‘两堆>1且异或和=0’ 的局面 转来的,所以 ‘两堆>1且异或和=0’ 的局面一定会在某一时刻转化为 ‘只有一堆大于1’ 的 先手必胜局面 ,也就是说是先手必败态;
其他同理可证
bzoj 1022: [SHOI2008]小约翰的游戏John
#include<cstdio>
#include<algorithm>
inline int read() {
int x=0,f=1;
char c=getchar() ;
while(c<'0'||c>'9') {
if(c=='-')f=-1;
c=getchar();
}
while(c<='9'&&c>='0')x=x*10+c-'0',c=getchar();
return x*f;
}
int a;
int main() {
for(int n,T=read();T--;) {
n=read();bool op=1;int tmp=0;
for(int i=1;i<=n;++i) {
a=read();if(a!=1)op=0;
tmp^=a;
}
if((op&&!tmp) || (!op&&tmp))puts("John");
else puts("Brother");
}
return 0;
}
SJ定理
定理:SJ定理
对于任意一个Anti-SG游戏,如果我们规定当局面中所有的单一游戏的SG值为0时,游戏结束,则先手必胜当且仅当:
(1)游戏的SG函数不为0且游戏中某个单一游戏的SG函数大于 1; (2)游戏的SG函数为0且游戏中没有单一游戏的SG函数大于1。
Multi-SG
有n堆石子,两个人可以从任意一堆石子中拿任意多个石子(不能不拿)或把一堆数量不少于2石子分为两堆不为空的石子,没法拿的人失败。问谁会胜利
分析
用sg函数来解决
操作1与nim游戏无异,对与一个石子数为k的点来说,后继可以为1..k
操作二实际上是将一个游戏分解为两个游戏,根据SG定理,两个有的的和为两个游戏的SG函数值得异或,我们可以通过异或运算把两个单一游戏连接到一起,作为一个后继状态
要是数据范围很大,SG函数就不能用了,有结论
hhhhhhhhhhh
定义
根据上面的游戏,定义Multi-SG游戏
Multi-SG 游戏规定,在符合拓扑原则的前提下,一个单一游戏的后继可以为多个单一游戏。
- Multi-SG其他规则与SG游戏相同。注意在这里要分清楚后继与多个单一游戏
例题
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1001;
int read() {
char c=getchar();int x=0,f=1;
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int a[maxn],sg[maxn];
int main() {
int T=read();
while(T--) {
int n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=n;i++)
if(a[i] % 4 == 0) sg[i] = a[i]-1;
else if(a[i]%4==1||a[i]%4==2) sg[i] = a[i];
else sg[i] = a[i]+1;
int ans=0;
for(int i=1;i<=n;i++)
ans^=sg[i];
if(ans)puts("Alice");
else puts("Bob");
}
return 0;
}
#include<cstdio>
#include<cstring>
const int maxn=3824;
int read() {
char c=getchar();int x=0,f=1;
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int sg[maxn][maxn];//当前剩余i行 j列的sg函数
int vis[maxn];
int main() {
int n=201,m=201;
for(int i=2;i<=n;i++)
for(int j=2;j<=n;j++) {
memset(vis,0,sizeof(vis));
for(int k=2;k<=i-2;k++) vis[sg[k][j]^sg[i-k][j]]=1;
for(int k=2;k<=j-2;k++) vis[sg[i][k]^sg[i][j-k]]=1;
for(int k=0;;k++) if(!vis[k]) {sg[i][j]=k;break;}
}
while(scanf("%d%d",&n,&m)!=EOF)
puts(sg[n][m] ? "WIN" : "LOSE");
return 0;
}
Every-SG
给定一张无向图,上面有一些棋子,两个顶尖聪明的人在做游戏,每人每次必须将可以移动的棋子进行移动,不能移动的人输
分析
这样的话,能赢得游戏必须赢
为了赢得最后的胜利
当你知道某一个单一游戏一定会输的话,你需要尽力缩短游戏的时间,相反的当你知道某一个游戏一定会赢的话,那么就需要尽力延长游戏的时间
定义:
- 对于还没有结束的单一游戏,游戏者必须对该 游戏进行一步决策;
- Every-SG游戏的其他规则与普通 SG游戏相同
Every-SG游戏与普通游戏最大的不同就是他多了一维时间
对于SG的值为0,我们需要周到最少走多少步才能结束,对于SG值不为0的点我们需要最多走多少步结束
我们用step变量来记录这个步数
定理
对于Every-SG游戏先手必胜当且仅当单一游戏中最大的step为奇数。
定理是显然的:
对于Every-SG游戏先手必胜当且仅当单一游戏中最大的step为奇数。
定理是显然的:最大的单一游戏步数如果是奇数的话那么肯定是先手取得进行最后一步,否则一定是对手取走最后一个棋子。
HDU 3595 GG and MM
balabala//我还没写QUQ