详尽版:博弈论 SG函数
1.Nim博弈
给定n堆物品,第 i 堆物品有
a
i
a_i
ai个。两名玩家轮流行动,每次可以任选一堆
,取走任意多个物品
,可把一堆取光,但不能不取
。取走最后一件
物品者获胜。两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为Nim博弈。把游戏过程中面临的状态
称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略
是指,若在某一局面下存在某种行动,使得行动后
对面面临必败局面
,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
Nim博弈不存在平局
,只有先手必胜和先手必败两种情况。
定理:
Nim博弈先手必胜,当且仅当
a
1
⊕
a
2
⊕
.
.
.
⊕
a
n
≠
0
a_1\oplus a_2\oplus...\oplus a_n≠0
a1⊕a2⊕...⊕an=0
公平组合游戏ICG
若一个游戏满足:
1. 由两名玩家交替行动;
2. 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
3. 不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
Nim博弈属于公平组合游戏
,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
证明:
(1) 0 ⊕ 0 ⊕ . . . ⊕ 0 = 0 0\oplus 0\oplus...\oplus 0=0 0⊕0⊕...⊕0=0
(2) a 1 ⊕ a 2 ⊕ . . . ⊕ a n = x ( ≠ 0 ) a_1\oplus a_2\oplus...\oplus a_n=x(≠0) a1⊕a2⊕...⊕an=x(=0)
x的二进制表示最高位1在第k位,说明
a
1
−
a
n
a_1-a_n
a1−an中,必然至少存在一个数
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
=
(
a
i
−
(
a
i
−
a
i
⊕
x
)
)
=
a
i
⊕
x
a_i=(a_i-(a_i-a_i\oplus x))=a_i\oplus x
ai=(ai−(ai−ai⊕x))=ai⊕x
此时就有 a 1 ⊕ a 2 ⊕ . . . ⊕ ( a i ⊕ x ) . . . ⊕ a n = x ⊕ x = 0 a_1\oplus a_2\oplus...\oplus(a_i\oplus x)...\oplus a_n=x\oplus x=0 a1⊕a2⊕...⊕(ai⊕x)...⊕an=x⊕x=0
说明从
a
i
a_i
ai中取走
a
i
−
(
a
i
⊕
x
)
a_i-(a_i\oplus x)
ai−(ai⊕x)个石子之后,就会变成先手必败局面,所以原局面是先手必胜
。
(3) a 1 ⊕ a 2 ⊕ . . . ⊕ a n = 0 a_1\oplus a_2\oplus...\oplus a_n=0 a1⊕a2⊕...⊕an=0
不论怎么去拿剩下数的异或值都不会是0
反证法:
假设从
a
i
a_i
ai中拿走一些石子,变成
a
i
′
a_i'
ai′,仍然有
a
1
⊕
a
2
⊕
.
.
.
⊕
a
i
′
.
.
.
⊕
a
n
=
x
⊕
x
=
0
a_1\oplus a_2\oplus...\oplus a_i'...\oplus a_n=x\oplus x=0
a1⊕a2⊕...⊕ai′...⊕an=x⊕x=0
让原式左部和上述左部异或, ( a 1 ⊕ a 2 ⊕ . . . ⊕ a i . . . ⊕ a n ) ⊕ ( a 1 ⊕ a 2 ⊕ . . . ⊕ a i ′ . . . ⊕ a n ) = a i ⊕ a i ′ = 0 (a_1\oplus a_2\oplus...\oplus a_i...\oplus a_n)\oplus (a_1\oplus a_2\oplus...\oplus a_i'...\oplus a_n)=a_i\oplus a_i'=0 (a1⊕a2⊕...⊕ai...⊕an)⊕(a1⊕a2⊕...⊕ai′...⊕an)=ai⊕ai′=0
a i ⊕ a i ′ = 0 a_i\oplus a_i'=0 ai⊕ai′=0,只能有 a i ′ = a i a_i'=a_i ai′=ai
所以从任意堆中取走任意数量石子,都会导致
a
1
⊕
a
2
⊕
.
.
.
⊕
a
n
≠
0
a_1\oplus a_2\oplus...\oplus a_n≠0
a1⊕a2⊕...⊕an=0,即先手必胜局面,说明原局面为先手必败
。
例题1:
输入
2
2 3
输出
Yes
思路
先手必胜状态:可以走到一个必败状态
先手必败状态:走不到任何一个必败状态
先手必败:
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
n
≠
0
a_1\oplus a_2\oplus...\oplus a_n≠0
a1⊕a2⊕...⊕an=0
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=105;
const int mod=1e9+7;
int main()
{
int n;
cin>>n;
int xr=0;//0^x=x
for(int i=0;i<n;i++)
{
int x;
cin>>x;
xr^=x;
}
if(xr)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
return 0;
}
例题2:Nim游戏变形
输入
3
2 1 3
输出
Yes
思路
需要找到这样一个规律,奇数台阶上石子数目异或和不为0则是先手必胜
,
a
1
⊕
a
3
⊕
.
.
.
⊕
a
n
≠
0
(
n
%
2
=
1
)
a_1\oplus a_3\oplus...\oplus a_n≠0\ (n\%2=1)
a1⊕a3⊕...⊕an=0 (n%2=1)
结论证明:
a
1
⊕
a
3
⊕
.
.
.
⊕
a
n
=
x
a_1\oplus a_3\oplus...\oplus a_n=x
a1⊕a3⊕...⊕an=x
1.x≠0
(1)如果我从奇数阶
a
i
a_i
ai上取走
a
i
−
(
a
i
⊕
x
)
a_i-(a_i\oplus x)
ai−(ai⊕x)个石子放到下一层后,就会使得
a
1
⊕
a
3
⊕
.
.
.
⊕
a
n
=
0
a_1\oplus a_3\oplus...\oplus a_n=0
a1⊕a3⊕...⊕an=0,抛给对手时x=0。
(2)如果对手从偶数阶
上
a
2
i
a_{2i}
a2i上取k个石子,我们就可以对应的从她所放石子的那一层取相同数量的放到下一层,就能保持奇数阶石子数不变。
2.x=0
(1)奇数层上取走任意个后都会使得异或值不再为0。
(2)对手拿偶数阶
不影响奇数阶石子数,也不会影响异或值。
#include <iostream>
#include <cmath>
#include <vector>
#include <unordered_set>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e4+5;
const int mod=1e9+7;
int main()
{
int n;
cin>>n;
int ans=0;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
if(i%2)
ans^=x;
}
if(ans)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
return 0;
}
2.SG函数
有向图游戏
给定一个有向无环图
,图中有一个唯一的起点
,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边
进行移动,每次可以移动一步,无法移动者判负
。该游戏被称为有向图游戏。
任何一个公平组合游戏
都可以转化为有向图游戏
。具体方法是,把每个局面
看成图中的一个节点
,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边
。
Mex运算
设S表示一个非负整数集合
。定义mex(S)
为求出不属于
集合S的最小非负整数
的运算,即:
mex(S) = min{x}
, x∈N自然数
,且x∉S
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(S)运算的结果,即: S G ( x ) = m e x ( S G ( y 1 ) , S G ( y 2 ) , . . . , S G ( y k ) ) SG(x) = mex({SG(y_1), SG(y_2), ..., SG(y_k)}) SG(x)=mex(SG(y1),SG(y2),...,SG(yk))
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值
,即SG(G) = SG(s)
。
有向图游戏的和
设
g
1
,
g
2
,
.
.
.
,
g
m
g_1, g_2, ..., g_m
g1,g2,...,gm 是m个有向图游戏。定义有向图游戏
g
g
g,它的行动规则是任选
某个有向图游戏
g
i
g_i
gi,并在
g
i
g_i
gi 上行动一步。
g
g
g 被称为有向图游戏
g
1
,
g
2
,
.
.
.
,
g
m
g_1, g_2, ..., g_m
g1,g2,...,gm 的和。
有向图游戏的和
的SG函数值等于它包含的各个子游戏
SG函数值的异或和
,即:
S
G
(
g
)
=
S
G
(
g
1
)
⊕
S
G
(
g
2
)
⊕
.
.
.
⊕
S
G
(
g
m
)
SG(g) = SG(g_1)\oplus SG(g_2) \oplus ... \oplus SG(g_m)
SG(g)=SG(g1)⊕SG(g2)⊕...⊕SG(gm)
定理
(1)有向图游戏的某个局面必胜
,当且仅当该局面对应节点的SG函数值大于0
。
(2)有向图游戏的某个局面必败
,当且仅当该局面对应节点的SG函数值等于0
。
例题1:
输入
2
2 5
3
2 4 7
输出
Yes
代码
时间复杂度:
最多100堆,记忆化搜索每个状态计算一次,一个状态最多k个后继状态,sg计算次数最多为104次,所以总时间复杂度不超过106,O(mk),m为每堆的石子个数
#include <iostream>
#include <cmath>
#include <vector>
#include <unordered_set>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e4+5;
const int mod=1e9+7;
int s[110],sg[maxn];
int k,n;
int SG(int x)//用记忆化搜索来求SG值
{
if(sg[x]!=-1)//保证每个状态只会被算一次,避免了时间复杂度不为指数级别
return sg[x];
//用一个哈希表来存当前局面所能到达的局面
unordered_set<int> st;
for(int i=0;i<k;i++)//遍历可以取的石子数
{
if(x>=s[i])
st.insert(SG(x-s[i]));//把这个状态加进去并且求这个状态的SG值
}
//找到当前最小的自然数
for(int i=0;;i++)
if(st.count(i)==0)//当前状态中没有i,所以最小的自然数就是它了
return sg[x]=i;
}
int main()
{
cin>>k;
for(int i=0;i<k;i++)
cin>>s[i];
cin>>n;
memset(sg,-1,sizeof(sg));//不需要多次初始化sg,因为x值相等对应的SG值是一定的
int ans=0;//注意初始化
for(int i=0;i<n;i++)
{
int x;
cin>>x;
ans^=SG(x);
}
if(ans)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
return 0;
}
例题2:
输入
2
2 3
输出
Yes
思路
首先游戏是可结束
的,因为即使堆数可能会不断增多,但是最大值不断在减小,当有一些堆减少到1时,取走这堆就不能再添加堆了,所以所有堆都是会被取走的。
这个问题依然是求每一堆的SG值的异或
,但是普通版的能够知道当前状态能够转移到的所有状态,只要调用mex()去算即可,但是这里SG值应该怎么求呢?
由有向图游戏的和
:
S
G
(
g
)
=
S
G
(
g
1
)
⊕
S
G
(
g
2
)
⊕
.
.
.
⊕
S
G
(
g
m
)
SG(g) = SG(g_1)\oplus SG(g_2) \oplus ... \oplus SG(g_m)
SG(g)=SG(g1)⊕SG(g2)⊕...⊕SG(gm) 知如果有两堆(即两个独立的图),则
不同游戏状态转移时的SG计算方式不同,要根据题目去具体分析,本题sg的计算方式是st.insert(SG(i)^SG(j));
,上题的计算方式是st.insert(SG(x-s[i]));
#include <iostream>
#include <cmath>
#include <vector>
#include <unordered_set>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e4+5;
const int mod=1e9+7;
int sg[maxn];
int n;
int SG(int x)//用记忆化搜索来求SG值
{
if(sg[x]!=-1)//保证每个状态只会被算一次,避免了时间复杂度不为指数级别
return sg[x];
//用一个哈希表来存当前局面所能到达的局面
unordered_set<int> st;
for(int i=0;i<x;i++)//遍历所有可放组合的石子数
for(int j=0;j<=i;j++)//避免重复,规定i<=j
st.insert(SG(i)^SG(j));//把这个状态加进去并且求这个状态的SG值
//mex操作,找到当前最小的自然数
for(int i=0;;i++)
if(st.count(i)==0)//当前状态中没有i,所以最小的自然数就是它了
return sg[x]=i;
}
int main()
{
cin>>n;
memset(sg,-1,sizeof(sg));
int ans=0;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
ans^=SG(x);
}
if(ans)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
return 0;
}