博弈论的入门——nim游戏&&sg函数浅谈
csp2020 rp++。
没错我要努力抱佛脚了。
博弈论
博弈论已经成为经济学的标准分析工具之一。在金融学、证券学、生物学、经济学、国际关系、计算机科学、政治学、军事战略和其他很多学科都有广泛的应用。——百度百科
我听说过的也比较感兴趣的名词是纳什平衡点,有兴趣的同学可以去了解一下。
以下,包括以后,我/你们所要掌握的主要是信息学范畴的博弈论内容,在这里我们从简单的nim取石子游戏入手。
nim游戏:
给你n堆石子,每次一个人最多取其中一堆石子,不能选择不取,问先取的人(即先手)是否有必胜策略。
从头开始,考虑方案转移。
考虑1堆石子的情况:
以下内容有些废话,了解意思的直接跳到加粗字体。
我们可以设一个节点,编号为0,代表此时还剩下0个石子。因为先取完的人获胜,那么先取完的人就取走了最后一堆石子,“0”这个状态是胜的那个人取完后形成的,此时另外一个人想要取,很明显他已经失败了(无石子可取)。
于是“0”就代表必败状态。
之后,我们可以尝试往前推,比如此时还剩下2个石子,5个石子的状态。由于取的石子数是任意的,因为两个人都足够聪明,那么我们可以确定,此时轮到取石子的那个人可以选择直接取完,接下来的状态就是0状态了。状态转移:x--0,那么这些状态就是必胜状态。
容易想到:必胜状态和必败状态可以从后往前推得,并且是交替出现的。
根据题目描述,我们可以确定与题目相合的状态转移方案。
对于当前节点x:
(1)如果它的后继节点(也就是它能转移到的状态)均是必败态,那么根据必败态和必胜态是交替出现的以及这个人是绝顶聪明的原则,当前状态一定为必胜态。
(2)如果它的后继节点均是必胜态,那么根据必败态和必胜态是交替出现的以及这个人是绝顶聪明的原则,当前状态一定是必败态。
(3)如果它的后继节点必败态和必胜态都存在,那么根据这个人绝顶聪明原则,它一定会选择必败态,并以此使当前状态为必胜态。
请细细品味第三个转移原则。
but,这只是逻辑上的推导,如果你要将状态转移用代码打出来,我们将要引入一个性质相似的函数:
sg函数。
当然了,你也可以选择设0/1状态来表示,但是sg应用更广,更加推荐。
sg函数
1.引入:mex()运算
mex()运算的定义是这样的:
这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态的SG函数值的集合。
也就是说,抽象成图的话就是将当前节点的后继节点的mex的值作为一个集合,并从中寻找一个没有在集合中出现过的最小的非负整数。
2.sg与状态转移原则的联系性:
对于sg(x):
(1)如果它的后继节点(也就是它能转移到的状态)的sg值均是0,那么sg函数定义,当前节点的sg值sg(x)=1。
(2)如果它的后继节点(也就是它能转移到的状态)的sg值均非0,那么sg函数定义,当前节点的sg值sg(x)=0。
(3)如果它的后继节点的sg值0和大于0的数同时出现,那么根据sg函数定义,当前节点的sg值sg(x)!=0.
很熟悉是吗qwq?
通过对比上面的那三条,我们可以得出结论:
sg函数的转移与必胜必败态的转移是一一对应的,其中sg(x)=0代表x节点必败,sg(x)>0代表当前节点必胜。
至此,我想你已经或者大概能够理解nim游戏为什么要和sg函数相联系了吧qwq
毕竟当年理解这玩意耗了我不少时间。。。
求sg函数的代码附上:
void getSG(int n) { sort(f+1,f+1+n); memset(sg,0,sizeof(sg)); for (int i=1; i<=n; i++) { memset(vis,0,sizeof(vis)); for (int j=1; f[j]<=i; j++) vis[sg[i-f[j]]]=1; for (int j=0; j<=n; j++) { if (vis[j]==0) { sg[i]=j; break; } } } }
其中,vis是个桶,sg[i]是状态i的sg值,f数组与题目中的转移法则有关,i-f[j]代表当前节点的后继状态。在此题中,由于可以任意取石子,我们设f[i]=i,来保证在j的循环下i可以转移到小于i的任意值。
sg定理
——游戏和的SG函数
等于各个游戏SG函数的异或和
。
上面我们讨论的是一堆的情况,那么我们在计算出每一堆的sg值后,将其异或起来就是整个游戏的sg函数值了。判断一下是否为0即可输出答案。
例题时间
T1,nim游戏
代码:
#include<cstdio> #include<algorithm> #include<cmath> #define maxn 10010 using namespace std; int t,n,a[maxn]; int main(){ scanf("%d",&t); while(t--){ scanf("%d",&n);int ans=0; for(int i=1;i<=n;i++)scanf("%d",&a[i]),ans^=a[i]; if(ans==0)puts("No");else puts("Yes"); } return 0; }
由于当前状态的后继状态可以取到小于等于i的任意值,那么每个节点sg(x)值必为x。
一堆石子总的sg值即为此堆石子数量,异或起来就好了嘛。
T2,取火柴游戏
题意容易读懂:nim游戏+输出其中一条方案。
nim游戏可以c v一下,代码就过来了,这里要思考的是如何输出一种方案;
我们设第i堆火柴的大小为a[i],答案就是a1^a2^a3......^an
因为必胜时要求输出方案,则a1^a2^a3......^an=k,k不为0。必胜状态与必败状态交替出现,易知我们在第一次取石子后,所有堆石子的异或值变成了0.我们要思考的就是使减去1个数使得异或值变成0。
解决方案就是,枚举一个特殊情况使得异或值为0。原来的异或值为k,k^k=0。我们于是可以对原式进行如下变换:
a1^a2^a3......^an=k
k^a1^a2^a3......^an=k^k
(k^a1)^a2^a3......^an=0
a^a2^a3......^an=0
把a1(你也可以选择其他的a)变成a就可以了。
a=a1^k=a1-(a1-a1^k);
我们只需要从头到尾检验每个数异或k的值是否比它小(因为是要减少),遇到小的直接输出ai-ai^k即可。
code:
#include<cstdio> #include<cstring> #include<iostream> #define N 500007 using namespace std; int a[N],n,node; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); } node=a[1]; for(int i=2;i<=n;i++) { node^=a[i]; } if(node==0) { printf("loser!");//hhh管云鹏狂喜 return 0; } for(int i=1;i<=n;i++) { if((a[i]^node)>a[i])continue; printf("%d %d\n",(a[i]-(a[i]^node)),i); a[i]^=node; break; } for(int i=1;i<=n;i++) { printf("%d ",a[i]); } return 0; }
祝人均省一。
完结撒花✿✿ヽ(°▽°)ノ✿。