数学之博弈论

博弈论

P891 Nim游戏

⊕ \oplus 表示xor
设石子数量分别为 a 1 , a 2 ⋅ ⋅ ⋅ a n a_1,a_2···a_n a1,a2an,当且仅当 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ a n ≠ 0 a_1\oplus a_2 \oplus ···a_n\neq0 a1a2an=0,先手必胜;反之先手必败
证明:

  1. 所有物品都被取光是一个必败局面(无法进行下一步),此时显然 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ ⊕ a n = 0 a_1\oplus a_2 \oplus ···\oplus a_n=0 a1a2an=0

  2. 对于任意一个局面,如果 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 a1a2aian=x=0,设x的二进制表示法下最高位的1在第k位,那么至少存在一堆石子 a i a_i ai,它的第k位是1(反证法),此时显然 a i ⊕ x < a i a_i\oplus x<a_i aix<ai,那么我们可以在 a i a_i ai堆石子里面拿走( a i − a i ⊕ x a_i-a_i\oplus x aiaix),让这一堆变为 a i ⊕ x a_i\oplus x aix,那么原式变为 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 a1a2aixan=xx=0,所以存在一种行动可以让对手面临各堆石子异或值为0

  3. 对于任意一个局面,如果 a 1 ⊕ a 2 ⊕ ⋅ ⋅ ⋅ ⊕ a i ⋅ ⋅ ⋅ ⊕ a n = 0 a_1\oplus a_2 \oplus ···\oplus \bm a_\bm i···\oplus a_n=0 a1a2aian=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,a2an
a 1 , a 2 ⋅ ⋅ ⋅ a n = x ≠ 0 a_1,a_2···a_n=x\neq0 a1,a2an=x=0,先手必胜
a 1 , a 2 ⋅ ⋅ ⋅ a n = 0 a_1,a_2···a_n=0 a1,a2an=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,x2xn
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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值