博弈论
P891 Nim游戏
⊕
\oplus
⊕表示xor
设石子数量分别为
a
1
,
a
2
⋅
⋅
⋅
a
n
a_1,a_2···a_n
a1,a2⋅⋅⋅an,当且仅当
a
1
⊕
a
2
⊕
⋅
⋅
⋅
a
n
≠
0
a_1\oplus a_2 \oplus ···a_n\neq0
a1⊕a2⊕⋅⋅⋅an=0,先手必胜;反之先手必败
证明:
-
所有物品都被取光是一个必败局面(无法进行下一步),此时显然 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ ⊕ a n = 0 a_1\oplus a_2 \oplus ···\oplus a_n=0 a1⊕a2⊕⋅⋅⋅⊕an=0
-
对于任意一个局面,如果 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ ⊕ a i ⋅ ⋅ ⋅ ⊕ a n = x ≠ 0 a_1\oplus a_2 \oplus ···\oplus \bm a_\bm i···\oplus a_n=x\neq0 a1⊕a2⊕⋅⋅⋅⊕ai⋅⋅⋅⊕an=x=0,设x的二进制表示法下最高位的1在第k位,那么至少存在一堆石子 a i a_i ai,它的第k位是1(反证法),此时显然 a i ⊕ x < a i a_i\oplus x<a_i ai⊕x<ai,那么我们可以在 a i a_i ai堆石子里面拿走( a i − a i ⊕ x a_i-a_i\oplus x ai−ai⊕x),让这一堆变为 a i ⊕ x a_i\oplus x ai⊕x,那么原式变为 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ a i ⊕ x ⊕ ⋅ ⋅ ⋅ a n = x ⊕ x = 0 a_1\oplus a_2 \oplus··· \bm a_\bm i\bm \oplus \bm x\oplus ···a_n=x\oplus x=0 a1⊕a2⊕⋅⋅⋅ai⊕x⊕⋅⋅⋅an=x⊕x=0,所以存在一种行动可以让对手面临各堆石子异或值为0
-
对于任意一个局面,如果 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ ⊕ a i ⋅ ⋅ ⋅ ⊕ a n = 0 a_1\oplus a_2 \oplus ···\oplus \bm a_\bm i···\oplus a_n=0 a1⊕a2⊕⋅⋅⋅⊕ai⋅⋅⋅⊕an=0,那么无论如何取石子,都无法改变异或值(反证法)
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int n;
scanf("%d",&n);
int res=0;
while(n--){
int a;
scanf("%d",&a);
res^=a;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
P892 台阶-Nim游戏
分析: 偶数级台阶上的石子一定要经过偶数次才能到达地面,所以对于先手方来说,最后偶数级台阶上的石子全部被移动到地面上时,一定正好轮到先手方走,那么我们不妨假设一开始偶数级台阶上就没有石子,这样只用分析奇数级台阶上的石子
设奇数级台阶上石子个数分别为
a
1
,
a
2
⋅
⋅
⋅
a
n
a_1,a_2···a_n
a1,a2⋅⋅⋅an
若
a
1
,
a
2
⋅
⋅
⋅
a
n
=
x
≠
0
a_1,a_2···a_n=x\neq0
a1,a2⋅⋅⋅an=x=0,先手必胜
若
a
1
,
a
2
⋅
⋅
⋅
a
n
=
0
a_1,a_2···a_n=0
a1,a2⋅⋅⋅an=0,先手必输
当奇数级台阶异或值不是0时,我们一定有种拿法使异或值为0抛给对手(NIM证明过),接下来对手如果拿偶数级台阶,我们就在这级台阶下一级拿一样多石子,这样能保证奇数级台阶异或值不变。如果对手拿奇数级台阶,此时奇数级台阶异或值一定不等于0(NIM证明过),那么又回到开始时的状态了,这样能保证我们一直有石子拿
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int n;
scanf("%d",&n);
int res=0;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
if(i&1) res^=x;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
P1318 取石子游戏
也叫作K-Nim游戏
核心思路: 每一轮都保证拿k+1个石子,即无论对方拿多少石子,我们都可以拿一些石子,使得本轮我们拿的石子的和为k+1,那么一旦石子总数模(k+1)不为0的话,我们可以最后拿完
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int n,k;
scanf("%d%d",&n,&k);
if(n%(k+1)) puts("1");
else puts("2");
return 0;
}
P893 集合-Nim游戏
mex运算: 设S表示一个非负整数集合,定义mex(S)为求出不属于集合S的最小非负整数,例如S={0,2,3},那么mex(S)=1
SG函数:
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点
y
1
,
y
2
,
⋅
⋅
⋅
⋅
y
k
y_1,y_2,····y_k
y1,y2,⋅⋅⋅⋅yk,定义SG(x)为x的的后继节点
y
1
,
y
2
,
⋅
⋅
⋅
⋅
y
k
y_1,y_2,····y_k
y1,y2,⋅⋅⋅⋅yk的SG函数值构成的集合再进行mex运算的结果即:SG(x)=mex({SG(
y
1
y_1
y1),SG(
y
2
y_2
y2)····SG(
y
k
y_k
yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即 SG(G)=SG(s).
且SG(终点)=0
重要性质: 若SG(x)=k,则x最大能到达的点的SG值为k−1
对于一个图而言:
设起点为x
当SG(x)=0时,x不管怎么走都走不到函数值为0的点(SG函数定义),先手必败
当SG(x)!=0时,x一定有种拿法能走到函数值为0的点(SG函数定义),先手必胜
对于多个图而言:
设起点为
x
1
,
x
2
⋅
⋅
⋅
x
n
x_1,x_2···x_n
x1,x2⋅⋅⋅xn
当
S
G
(
x
1
)
⊕
S
G
(
x
2
)
⊕
⋅
⋅
⋅
⊕
S
G
(
x
n
)
=
0
SG(x_1)\oplus SG(x_2)\oplus···\oplus SG(x_n)=0
SG(x1)⊕SG(x2)⊕⋅⋅⋅⊕SG(xn)=0,先手必败
当
S
G
(
x
1
)
⊕
S
G
(
x
2
)
⊕
⋅
⋅
⋅
⊕
S
G
(
x
n
)
≠
0
SG(x_1)\oplus SG(x_2)\oplus···\oplus SG(x_n)\neq0
SG(x1)⊕SG(x2)⊕⋅⋅⋅⊕SG(xn)=0,先手必胜
证明同NIM游戏
#include<iostream>
#include<algorithm>
#include<unordered_set>
#include<cstring> //memset函数
using namespace std;
const int N=110,M=10010;
int s[N],f[M];
int n,k;
int sg(int x){
if(f[x]!=-1) return f[x];//防止重复搜索,维护时间复杂度
unordered_set<int> S;
for(int i=0;i<n;i++){
int sum=s[i];
if(x>=sum) S.insert(sg(x-sum));
}
for(int i=0;;i++){
if(!S.count(i)){
return f[x]=i; //找到没在集合中出现过的最小自然数
}
}
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&s[i]);
memset(f,-1,sizeof(f));
scanf("%d",&k);
int res=0;
for(int i=0;i<k;i++){
int x;
scanf("%d",&x);
res^=sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
P4193 S-Nim
sg函数板子题
#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cstring> //memset函数
#include<unordered_set>
using namespace std;
const int N=110,M=10010;
int s[N],f[M];
int k,m,cnt;
int sg(int x){
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=0;i<k;i++){
int sum=s[i];
if(x>=sum) S.insert(sg(x-sum));
}
for(int i=0;;i++){
if(!S.count(i)) return f[x]=i;
}
}
int main(){
while(scanf("%d",&k)&&k!=0){
cnt=0;
for(int i=0;i<k;i++) scanf("%d",&s[i]);
scanf("%d",&m);
char ans[m];
memset(f,-1,sizeof(f));
for(int j=0;j<m;j++){ //不能写while(m--),因为m后面遍历会用
int n;
scanf("%d",&n);
int res=0;
while(n--){
int x;
scanf("%d",&x);
res^=sg(x);
}
if(res) ans[cnt++]='W';
else ans[cnt++]='L';
}
for(int i=0;i<m;i++) printf("%c",ans[i]);
printf("\n");
}
return 0;
}