博弈论(Game Theory)
又称为对策论或赛局理论,是研究理性决策者之间战略互动的数学模型,主要用于分析具有斗争或竞争性质的现象。
定义:博弈论是研究在特定规则下,个人或团体如何根据各自掌握的信息和对他人行为的预测,选择最优策略以最大化自身利益的数学理论和方法。
题目:891. Nim游戏 - AcWing题库
思路:
本题里是否先手赢得意义就是第一位出手的选手是否能赢得比赛(假设不考虑其它因素外)
先手必胜状态:可以走到某一个必败状态(先手的人将这个状态走到一个必败状态,那么对手执行此状态时为必败)
先手必败状态:走不到任何一个必败状态(先手的人将这个状态走不到任何一个必败状态,那么对手执行此状态时必胜)
^:抑或:不进位加法,即两者相同时为0、不同时为1
回归到本题:
当一开始a1^a2^a3......an=0时,为先手必败状态
当一开始a1^a2^a3......an=x !=0时,为先手必胜状态
当一开始a1^a2^a3......an = x != 0时,为先手必胜状态
证明:
因为x!=0,假设x在二进制下的最高位1在第k位
那么在a1~an中必然存在一个数ai,ai的第k位是1
(反证:假设不存在一个数,那么a1~an中的所有第k位全为0,那么x的第k位也应该为0,与假设矛盾)
所有ai^x<ai(第k位经过抑或后变成0,那么转化为十进制下,值一定会变小,因为第k位是1且是x在二进制下的最高位)
那么ai > ai-ai^x > 0,我们可以从ai里取出ai-ai^x个石子,那么第ai堆里所剩下的石子即为:ai - (ai - ai^x) = ai^x
那么所剩下的石子的抑或值为:a1^a2^a3^...^ ai^x ^a(i+1)^...^an = a1^a2^a3^...ai^a(i+1)^...
^an^x 而a1^a2^a3......an = x ,所以上式= x^x = 0,那么只要先手从第ai堆里取出ai-ai^x个石子,那么剩下的石子堆里就会变成先手必败状态,那么对面必败,己方必胜。
证毕
当一开始假设a1^a2^a3......an=0时,为先手必败状态
证明:
反证法:假设a1^a2^a3......an = x != 0 ①,那么就有a1^a2^a3^...^ ai^x ^a(i+1)^...^an = 0 ②(上述论证过程,不再赘述)
那么将①式与②式的左边进行异或处理得:a1^a2^a3^...^ ai^x ^a(i+1)^...^an^a1^a2^a3......an = x = 0,与假设矛盾,所以证明成立
那么只要先手从第ai堆里取出ai-ai^x个石子,那么状态就会变成先手必胜状态,那么对面必胜,己方必败。
经过上述证明,只要一开始的状态为先手必胜状态,那么只要从第ai堆里取出ai-ai^x个石子
那么状态就会变成先手必败状态,那么对面出时则必败,那么己方必胜;此时对方从石子堆里取出石子,那么状态就会成先手必胜状态,那么己方出手时必胜,那么对面必败......
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int a[N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++ )
cin >> a[i];
int res=a[0];
for(int i = 1; i < n; i ++ )
res ^= a[i];
if(res) puts("Yes");
else puts("No");
}
题目:AcWing 892. 台阶-Nim游戏 - AcWing
思路 :
当奇数台阶上的所有值抑或!=0时,先手必胜。反之必败
证明:
可以参考这位友友的题解:AcWing 892. 台阶-Nim游戏 - AcWing
懒得证了,说说我的理解。当先手一开始从奇数台阶上移动石子放入下一个偶数台阶时,此时奇数台阶上所有的异或值一定=0,那么倘若后手移动偶数台阶上的值,先手就可以将后手所以移动的偶数台阶上的石子以相同的数量移动到下一阶偶数上去,那么后手将会一直面临奇数台阶异或值为0的状态;倘若移动奇数台阶上的值,那么先手也同理将偶数台阶上的值原数移动到奇数台阶上去,那么后手又会面临奇数台阶异或值为0的状态,那么后手一直面临奇数台阶异或值为0的状态,那么后手将一直面临必败的状态。
代码:
/*
当奇数台阶上的值抑或值!=0时,为先手必胜状态;否则为先手必败状态
*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int a[100050];
int main()
{
int n;
cin >> n;
int ans = 0;
for(int i = 0; i < n; i ++ )
{
cin >> a[i];
if(i % 2 == 0)//取奇数台阶上的值即可
ans ^= a[i];
}
if(ans) puts("Yes");
else puts("No");
return 0;
}
补充:
1.Mex运算:
设S表示一个非负整数集合.定义mex(S)为求出不属于集合S的最小非负整数运算,即:
mes(S)=min{x}; 例如:S={0,1,2,4},那么mes(S)=3;
2.SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1,y2,····yk,定义SG(x)的后记节点y1,y2,····yk的SG函数值构成的集合在执行mex运算的结果
即:SG(x)=mex({SG(y1),SG(y2)····SG(yk)}),特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即 SG(G)=SG(s)。如左图
3.有向图游戏的和
设G1,G2,····,Gm是m个有向图游戏.定义有向图游戏G,他的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步.G被称为有向图游戏G1,G2,·····,Gm的和.有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数的异或和,即:SG(G)=SG(G1)^SG(G2)^···^ SG(Gm)。
如右图中的SG(G) = SG(G1)^SG(G2)^SG(G3) = 2 ^ 2 ^ 3 = 3
题目:893. 集合-Nim游戏 - AcWing题库
思路:
当对于单个图时(左图),SG(G) = 0,为先手必败状态;SG(G) != 0,为先手必胜状态。证明:只要SG(G) != 0那么就可以走到一个必败状态,使得对手处于必败状态进而先手必胜。如上图中的左图选择第 4 条路径即可;右图中的G1除第一条路径都可以...
当对于多个图,即多种选择时,SG(G)=SG(G1)^SG(G2)^···^ SG(Gm) = 0时为先手必败;SG(G)=SG(G1)^SG(G2)^···^ SG(Gm) != 0时为先手必胜状态。对于多个图,由于SG(Gi)是属于集合S的最小非负整数,我们可以选择(0, SG(Gi) - 1)中的任意一步,那么就可以转化为第一题中的NIM游戏。如G1,SG(G1) = 2,那么我们可以选择0,1中的任意一步;SG(G3) = 3,那么我们可以选择0,1,2中的任意一步进而转化为普通的NIM游戏。
样例:
代码:
#include<bits/stdc++.h>
using namespace std;
//堆数、石子数
const int N = 110, M = 10010;
int n, m;
int s[N], sg[M];//s[N]来表示集合S,sg[M]来表示sg的值
int SG(int x)
{
if(sg[x] != -1) return sg[x];//如果已经被计算过,直接返回即可
unordered_set<int> S;
for(int i = 0; i < m; i ++ )//遍历s[]数组
{
int sum = s[i];
/*
记忆化搜索:在上一层的基础上进行拓展
7->5->0、7->5->3->1、7->2->0。
*/
if(x >= sum) S.insert(SG(x - sum));
}
for(int i = 0; ; i ++ )
if(!S.count(i))
return sg[x] = i;//整个有向图不属于集合S的最小非负整数i即为sg[x]的值
}
int main()
{
cin >> m;
for(int i = 0; i < m; i ++ ) cin >> s[i];
cin >> n;
//记忆化搜索
memset(sg, -1, sizeof sg);
int res = 0;
for(int i = 0; i < n; i ++ )
{
int x;
cin >> x;
res ^= SG(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
题目:894. 拆分-Nim游戏 - AcWing题库
思路:
将一个数拆分成任意两个小于本身的数,拆分之后的sg值 = 拆分的两个数值得异或。
即:sg[ x ] = sg[ i ] ^ sg[ j ]。
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
int sg[105];
int SG(int x)
{
if(sg[x] != -1) return sg[x];
unordered_set<int> S;
//枚举每一个可能出现的情况
for(int i = 0; i < x; i ++ )//拆分的两个数必须小于x
for(int j = 0; j <= i; j ++ )
S.insert(SG(i)^SG(j));//性质
for(int i = 0; ; i ++ )
if(!S.count(i))
return sg[x] = i;
}
int main()
{
cin >> n;
memset(sg, -1, sizeof sg);
int res = 0;
for(int i = 0; i < n; i ++ )
{
int x;
cin >> x;
res ^= SG(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}