博弈论(1) - Nim游戏
#include <iostream>
using namespace std;
// 先手必胜状态:可以走到某一个必败状态
// 先手必败状态:走不到任意一个必败状态
/*
给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数n。
第二行包含n个数字,其中第i个数字表示第i堆石子的数量。
输出格式
如果先手方必胜,则输出"Yes"。
否则,输出"No"。
数据范围
1 ≤ n ≤ 10^5,
1 ≤ 每堆石子数 ≤ 10
*/
// 定理:如果石子堆分别有a1,a2,a3,……,an, 若 a1^a2^a3^……^an = 0 的话,先手必败;不等于0,先手必胜
/* 证明:先假定 a1 ^ a2 ^ a3 ^ …… ^ an = x (x != 0) 为先手必胜状态,
a1 ^ a2 ^ a3 ^ …… ^ an = 0 为先手必败状态
证明:(1)通过改变a1~an中某个数值,一定可以将先手必胜状态转换为先手必败状态
(2)改变a1-an中某个值,一定会将先手必败状态转换为先手必胜状态
(1) x的二进制中最高位在第k位,则a1-an中必定存在一个数 ai 的第k位为1
则 ai^x < ai (把ai的第k位变为0了)
在 ai 堆中拿走 ai-(ai^x) 个石子,此时原ai堆石子数量为(ai^x)
拿完之后各石堆石子数量为:a1, a2, a3, ……, ai^x, ……, an
将它们异或起来:a1 ^ a2 ^ …… ^ (ai ^ x) ^ …… ^ an = x ^ x = 0
得证
(2) 改变某个值ai,让其变为 aii
假设 a1 ^ a2 ^ …… ^ ai ^ …… ^ an = 0
又因为 a1 ^ a2 ^ …… ^ aii ^ …… ^ an = 0
上面2式子左边异或左边,右边异或右边,得到:ai ^ aii = 0,即 ai = aii
*/
int main()
{
int n;
int res = 0;
cin >> n;
while (n--)
{
int x;
cin >> x;
res ^= x;
}
if (res != 0)
{
cout << "Yes" << endl;
}
else
{
cout << "No" << endl;
}
return 0;
}
博弈论(2) - 台阶Nim游戏
#include <iostream>
using namespace std;
// http://t.csdn.cn/3Dks1
int main()
{
int n;
int res = 0;
cin >> n;
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
if (n % 2 == 1)
{
res ^= x;
}
}
if (res != 0)
{
cout << "Yes" << endl;
}
else
{
cout << "No" << endl;
}
return 0;
}
博弈论(3) - 集合Nim游戏
#include <iostream>
#include <unordered_set>
using namespace std;
/*
给定n堆石子以及一个由k个不同正整数构成的数字集合S。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合S,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数k,表示数字集合S中数字的个数。
第二行包含k个整数,其中第i个整数表示数字集合S中的第i个数si。
第三行包含整数n。
第四行包含n个整数,其中第i个整数表示第i堆石子的数量hi。
输出格式
如果先手方必胜,则输出"Yes"。
否则,输出"No"。
数据范围
1≤n,k≤100,
1≤si,hi≤10000
*/
const int M = 10010; // 集合中元素最大有多大、石堆中石子的数量
const int N = 110; //集合有多少个元素、有多少个石堆
int k, n;
int s[N]; // 存储集合中元素元素值
int sg[M]; // 存储每个值的SG值
/*
mex运算:设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:mex(S) = min(x), x属于自然数,且x不属于S
如 mex({3, 4, 5}) = 0
*/
int mex(unordered_set<int>& S)
{
for (int i = 0;; i++)
{
if (S.count(i) == 0)
{
return i;
}
}
}
/*
SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1,y2,..yk,
定义 SG(x)为x的后继节点y1,y2,....yk的SG函数值构成的集合再执行mex(S)运算的结果即:SG(x) = mex((SG(y1), SG(y2). .., SG(yk)))
定义终点状态的SG为0
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
设G1、G2、……、Gm为m个有向图游戏,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。(在本题中,每个石堆对应一个有向图,要求出每个石堆起点的SG值)
G被称为有向图G1、G2、……、Gm的和。有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值(起点s的SG函数值)的异或和
即:SG(G) = SG(G1) ^ SG(G2) ^ …… ^ SG(Gm)
定理: 有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
*/
int SG(int x)
{
// 如果 x值 的SG值被算过,直接返回
// 通过sg数组,避免重复运算
if (sg[x] != -1)
{
return sg[x];
}
// x值的SG值没有被算过,根据定义计算
unordered_set<int> S;
for (int i = 0; i < k; i++)
{
// 如果元素小于等于x才可以计算
if (s[i] <= x)
{
// (x - s[i]) 大于等于 0,需要插入它的SG值
if (sg[x - s[i]] != -1)
{
// 如果(x - s[i])的SG值已经被算过,直接插入
S.insert(sg[x - s[i]]);
}
else
{
// 未被算过,计算后插入
S.insert(SG(x - s[i]));
}
}
}
// 将计算出来 x的SG值 赋给数组并返回
sg[x] = mex(S);
return sg[x];
}
int main()
{
cin >> k; // 集合中元素个数
for (int i = 0; i < k; i++)
{
cin >> s[i];
}
memset(sg, -1, sizeof(sg)); // 如果 sg[i] 为-1,则说明 数i 的 SG值 还未被算过
cin >> n; // 石堆数
int res = 0; // 存储结果值
for (int i = 0; i < n; i++)
{
int x; // 当前石堆中石子数
cin >> x;
res ^= SG(x);
}
if (res != 0)
{
cout << "Yes" << endl;
}
else
{
cout << "No" << endl;
}
return 0;
}