博弈论基础模板

博弈论(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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值