公平组合游戏ICG
若一个游戏满足:
1.由两名玩家交替行动;
2.在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
3.不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏
必胜状态和必败状态
必胜状态,先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态 。即先手可以走到某一个必败状态。
必败状态,先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态 。即先手走不到任何一个必败状态。
-
**如果当前局面所有石堆的异或和(Nim和)为0,那么无论如何操作,当前玩家都处于必败态;**如果当前局面是必败态,则无论如何操作,另一方总有必胜的策略。
-
**如果当前局面所有石堆的异或和(Nim和)不为0,那么当前玩家可以通过正确的策略使对手进入必败态,因此当前玩家处于必胜态。**如果当前局面是必胜态,玩家可以通过正确操作将局面转化为必败态,从而保证胜利。
异或运算三个性质:
①任何数和 0做异或运算,结果仍然是原来的数
②任何数和其自身做异或运算,结果是 0
③异或运算满足交换律和结合律
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n; cin >> n;
int res = 0;
for(int i = 0; i < n; i++){
int x; cin >> x;
res^=x;
}
if(res) cout << "Yes\n";
else cout <<"No\n";
return 0;
}
台阶-Nim游戏
与经典Nim的区别
- 在经典的Nim游戏中,石子堆是独立的,而在台阶-Nim中,台阶之间可能存在关联。例如,某个台阶上的石子数量可能会影响其他台阶的策略
结论:
此时我们需要将奇数台阶看做一个经典的Nim游戏,如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜。
证明:
先手时,如果奇数台阶异或非0,根据经典Nim游戏,先手总有一种方式使奇数台阶异或为0,于是先手留了奇数台阶异或为0的状态给后手,此时先手必赢。
于是轮到后手:
①当后手移动偶数台阶上的石子到奇数台阶上时,先手只需将对手移动的石子继续移到下一个台阶,这样奇数台阶的石子相当于没变,奇数异或和仍是非0, 于是留给后手的又是奇数台阶异或为0的状态
②当后手移动奇数台阶上的石子到偶数台阶上时,留给先手的奇数台阶异或非0,根据经典Nim游戏,先手总能找出一种方案使奇数台阶异或为0
因此无论后手如何移动,先手总能通过操作把奇数异或为0的情况留给后手,当奇数台阶全为0时,只留下偶数台阶上有石子。
因此如果先手时奇数台阶上的值的异或值为非0,则先手必胜,反之必败!
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n; cin >> n;
int res = 0;
for(int i = 1; i <= n; i++){
int x; cin >> x;
if(i & 1)
res^=x;
}
if(res) cout << "Yes\n";
else cout <<"No\n";
return 0;
}
集合-Nim游戏:
1.Mex运算:
设S表示一个非负整数集合.定义mex(S)为求出不属于集合S的最小非负整数运算
例如:S={0,1,2,4},那么mes(S)=3;
2.SG函数
- 对于一个游戏状态 x,SG函数值 表示从该状态出发,所有可能的后续状态的SG值所能达到的"最小非负整数"(Mex)。
- 具体来说,若从状态 x通过合法操作可以到达的所有状态的SG值分别为 那么 就是一个不在这些SG值集合中的最小非负整数。
SG函数的性质:类似于经典的Nim游戏
- 必败态 (Losing Position):对于某个状态 x,如果 那么当前处于该状态的玩家将无法避免失败(即在对方采取最优策略的情况下无论如何都会输)。
- 必胜态 (Winning Position):对于某个状态 x,如果 ,那么当前处于该状态的玩家有策略可以使对手进入必败态。也就是先手一定会胜利。
递归计算:
例如s = [2,5],x = 10时递归得到
初始状态:
f[x]
是用来存储每个x
的SG值的数组,初始时f[x]
的所有值都被设置为-1
(表示尚未计算过)。x = 10
是我们要计算的状态,s = [2, 5]
是可供选择的石子数。
计算 sg(10)
的过程:
-
调用
sg(10)
:- 首先检查
f[10]
,因为f[10] = -1
,说明还没有计算过,所以继续计算。 - 初始化集合
S
。
- 首先检查
-
遍历
s[i]
:- 对
s[0] = 2
:- 因为
10 >= 2
,所以我们递归调用sg(10 - 2) = sg(8)
。
- 因为
- 对
s[1] = 5
:- 因为
10 >= 5
,所以我们递归调用sg(10 - 5) = sg(5)
。
- 因为
- 对
-
递归调用
sg(8)
:- 首先检查
f[8]
,因为f[8] = -1
,所以继续计算。 - 初始化集合
S
。 - 遍历
s[i]
:- 对
s[0] = 2
:- 因为
8 >= 2
,所以我们递归调用sg(8 - 2) = sg(6)
。
- 因为
- 对
s[1] = 5
:- 因为
8 >= 5
,所以我们递归调用sg(8 - 5) = sg(3)
。
- 因为
- 对
- 首先检查
-
递归调用
sg(6)
:- 首先检查
f[6]
,因为f[6] = -1
,所以继续计算。 - 初始化集合
S
。 - 遍历
s[i]
:- 对
s[0] = 2
:- 因为
6 >= 2
,所以我们递归调用sg(6 - 2) = sg(4)
。
- 因为
- 对
s[1] = 5
:- 因为
6 >= 5
,所以我们递归调用sg(6 - 5) = sg(1)
。
- 因为
- 对
- 首先检查
-
递归调用
sg(4)
:- 首先检查
f[4]
,因为f[4] = -1
,所以继续计算。 - 初始化集合
S
。 - 遍历
s[i]
:- 对
s[0] = 2
:- 因为
4 >= 2
,所以我们递归调用sg(4 - 2) = sg(2)
。
- 因为
- 对
s[1] = 5
:- 因为
4 < 5
,所以跳过。
- 因为
- 对
- 首先检查
-
递归调用
sg(2)
:- 首先检查
f[2]
,因为f[2] = -1
,所以继续计算。 - 初始化集合
S
。 - 遍历
s[i]
:- 对
s[0] = 2
:- 因为
2 >= 2
,所以我们递归调用sg(2 - 2) = sg(0)
。
- 因为
- 对
s[1] = 5
:- 因为
2 < 5
,所以跳过。
- 因为
- 对
- 首先检查
-
递归调用
sg(0)
:sg(0)
是一个基本情况,因为没有石子可以取走,故f[0] = 0
,返回0
。
-
接下来如上继续.....
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 110, M = 10010;
int n,m;
int s[N],f[M];//s存储的是可供选择的石子数, f存储的是所有可能情况的SG值
int sg(int x){
if(f[x] != -1) return f[x]; // 如果x的SG值已经计算过,直接返回
set<int> S;
//S是一个有序集合,用来存储当前状态的所有可能的下一步的SG值
// 循环每一次可以选择拿走的石头数
for(int i = 0; i < m; i++){
// 递归计算x减去s[i]后的状态的SG值,并插入到集合S中
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() {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin >> m;
for(int i = 0; i < m; i++) cin >> s[i];
cin >> n;
memset(f,-1,sizeof f);
f[0] = 0; //当x为0的时候sg函数返回0
// 类似于经典的Nim游戏
int res = 0;
while(n--){
int x; cin >> x;
res ^= sg(x);
}
if(res) cout << "Yes\n";
else cout <<"No\n";
return 0;
}
拆分-Nim游戏
相比于集合-Nim,这里的每一堆可以变成小于原来那堆的任意大小的两堆
即a[i]可以拆分成(b[i],b[j]),为了避免重复规定b[i]>=b[j] 即:a[i]>b[i]>=b[j]
相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。SG(i^j) = SG(i) ^SG(j)
因此需要存储的状态就是sg(b[i])^sg(b[j]),与集合-Nim的唯一区别
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 110, M = 10010;
int n,m;
int s[N],f[M];//s存储的是可供选择的石子数, f存储的是所有可能情况的SG值
int sg(int x){
if(f[x] != -1) return f[x]; // 如果x的SG值已经计算过,直接返回
set<int> S;//这里可以改为unordered_set<int> S
//S是一个有序集合,用来存储当前状态的所有可能的下一步的SG值
for(int i = 0; i < x; i++){
for(int j = 0; j <= i; j++){
S.insert(sg(i) ^ sg(j));
}
}
//找到不存在的最小自然数
for(int i = 0; ; i++){
if(!S.count(i)) return f[x] = i;
}
}
int main() {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin >> n;
memset(f,-1,sizeof f);
f[0] = 0;
// 类似于经典的Nim游戏
int res = 0;
while(n--){
int x; cin >> x;
res ^= sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}