博弈论 sg函数

在这里插入图片描述

sg函数(全称Sparague—Grundy)

它是博弈论和组合游戏理论中的一个重要概念,用于分析组合游戏的输赢情况,特别是那些具有明确终止条件和有限状态空间的游戏。

定义

对于一个给定的组合游戏,其sg函数G(n)是一个非负整数,用于表示游戏位置n的"输赢状态";两个游戏位置的sg函数值相同,意味着从这两个位置开始,对于任何一个玩家来说,都存在相同的赢或输的策略。

性质

终止位置:

终止位置的sg函数值通常定义为0(表示当前玩家无法移动,即输掉游戏,但这也取决于具体的游戏规则和胜负判断)

非终止位置:

对于非终止位置n,其sg函数数值G(n)是后续所有可能位置m的sg函数值集合中不包含的最小非负整数(即mex函数)

周期性:

在某些游戏中,sg函数值可能呈现出周期性,这意味着当游戏状态达到某个特定大小时,其输赢情况会开始重复

计算方法

确定终止位置

首先确定所有可能的终止位置,并给它们赋予适当的sg值(通常为0)

逆向递归

从终止位置开始,逆向归纳计算每一个非终止位置的sg值。涉及递归地考虑所有可能的移动和后续位置

应用mex函数

对于每个非终止位置,使用mex函数来找到其sg值。mex函数返回的是不包含在给定集合中的最小非负整数。

示例

给一个简单的Nim游戏的变体:其中有一堆n个石子,两个玩家轮流取,每次可以取1~k个石子,取走最后一个石子的人获胜。这个游戏的sg函数计算:
1、对于n=0,这是一个失败的问题,因为没有石子可以取了,所以G(0)=0(但注意,有些定义中可能不给终止位置赋SG值,或者视为特殊值)。
2、对于n&gt=0,玩家会尝试取走一定数量的石子,使得剩下的石子数量m的sg函数值G(m)!=0。
3、通过计算可以发现,这个游戏的SG函数值呈现一种周期性模式,即当n足够大时,G(n)的值会开始重复。具体来说,周期的长度通常与k+1有关(但这也取决于具体的游戏规则)。

巴什博弈

题目描述

十年前读大学的时候,中国每年都要从国外引进一些电影大片,其中有一部电影就叫《勇敢者的游戏》(英文名称:zatiura),一直到现在,我依然对于电影中的部分电脑特技印象深刻。
今天,大家选择上机考试,就是一种勇敢(brave)的选择;这个短学期,我们讲的是博弈(game)专题;所以,大家现在玩的也是"勇敢者的游戏”,这也是我命名这个题目的原因。当然,除了“勇敢",我还希望看到"诚信”,无论考试成绩如何,希望看到的都是一个真实的结果,我也相信大家一定能做到的~
各位勇敢者要玩的第一个游戏是什么呢?很简单,它是这样定义的:
1、本游戏是一个二人游戏:
2、有一堆石子一共有n个;
3、两人轮流进行:
4、每走一步可以取走1…m个石子;
5、最先取光石子的一方为胜;
如果游戏的双方使用的都是最优策略,请输出哪个人能赢。

输入

输入数据首先包含一个正整数C(C<-100),表示有C组测试数据。
每组测试数据占一行,包含两个整数n和m(1<-n.m<-1000),n和m的含义见题目描述

输出

如果先走的人能赢,请输出“frst”,否则请输出“second”,每个实例的输出占一行。

输入数据

2
23 2
4 3

输出数据

first
second

题解思路

当m=4时,石头数为0时前者为必胜态,1时也为必胜态,2时也为必胜态,3也为必胜态,4也为必胜态,5也为必败态…,依次类推可以得出当n%(m+1)==0时为必败态。

code:

#include<iostream>
using namespace std;
void solve(){
   
    int n,m;
    cin>>n>>m;
    if(n%(m+1)) cout<<"first"<<'\n';
    else cout<<"second"<<'\n';
}
int main(){
   
    int n;
    cin>>n;
    while(n--){
   
        solve();
    }
    return 0;
}

尼姆博弈

【模板】Nim 游戏

题目描述

甲,乙两个人玩 nim 取石子游戏。

nim 游戏的规则是这样的:地上有 n n n 堆石子(每堆石子数量小于 1 0 4 10^4 104),每人每次可从任意一堆石子里取出任意多枚石子扔掉,可以取完,不能不取。每次只能从一堆里取。最后没石子可取的人就输了。假如甲是先手,且告诉你这 n n n 堆石子的数量,他想知道是否存在先手必胜的策略。

输入格式

本题有多组测试数据。

第一行一个整数 T T T T ≤ 10 T\le10 T10),表示有 T T T 组数据

接下来每两行是一组数据,第一行一个整数 n n n,表示有 n n n 堆石子, n ≤ 1 0 4 n\le10^4 n104

第二行有 n n n 个数,表示每一堆石子的数量.

输出格式

T T T 行,每行表示如果对于这组数据存在先手必胜策略则输出 Yes,否则输出 No

样例 #1

样例输入 #1
2
2
1 1
2
1 0
样例输出 #1
No
Yes

解题思路:

(x,0,0,0)||(0,x,0,0)——>必败态(0,0,0,0)。
异或等于零为必败态。
异或和等于零必然由异或和不等于零推出来的,异或和不等于零一定有某种方法可以推出异或和等于零。
例子:
1,4,2(A拿)异或和不等于零
——>1,3,2(B拿)异或和等于零
——>0,3,2(A拿)异或和不等于零
——>0,2,2(B拿)异或和等于零
——>0,1,2(A拿)异或和不等于零
——>0,1,1(B拿)异或和等于零
——>0,0,1(A拿)异或和不等于零
——>A胜
它总可以把一个异或和不为零的状态转化为异或和为零的状态,也总可以把一个异或和为零的状态转化为异或和不为零的状态。

code:

#include<iostream>
using namespace std;
void solve(){
   
    int n;
    cin>>n;
    int ans=0;
    for(int i=1;i<=n;i++){
   
        int x;
        cin>>x;
        ans^=x;
    }
    cout<<(ans ? "Yes":"No")<<'\n';
}
int main(){
   
    int t;
    cin>>t;
    while(t--){
   
        solve();
    }
    return 0;
} 

题目描述

栗酱特别喜欢玩石子游戏,就是两个人玩,有n堆石子,每堆有ai个,每次一个人可以轮流选择任意一堆,取走任意多的石子(但不能不取),谁先不能取谁输。
栗酱觉得这个游戏很有趣,知道有一天,小太阳告诉她,其实如果两个人足够聪明,游戏的结局一开始就已经注定。
栗酱是一个冰雪聪明的女孩子,她不相信,希望你演示给她看。

输入描述

多组数据,数据第一行T表示数据组数
每组数据第一行一个n,k表示一共有n堆石子,接下来你试图从第k堆开始取,从第二行开始,每隔一个空格一个
第i堆石子的数量ai.
n<=10 ^ 5,ai<=10 ^ 9

输出描述

输出“Yes"或“No"代表从该堆开始取是否可以必胜(如果足够聪明)。

输入

2
3 2
1 2 3
2 1
2 1

输出

No
Yes

说明

小太阳哥哥说,如果想赢,就试图把每堆石子数量的异或和变为0,最终便可以获得胜利,不相信自己证下。

解题思路

要改变一个数使其异或和为零,可以将一个数改为其余两个数的异或运算的答案。
分析No的数据:

3 2
1 2 3

要将1,2,3改变一个数使其异或和为零并且只可以改变第二个数,应该将其改为(1,3,3),但是本题只能拿走,不能增加,所以先手应该为必败态。
分析Yes的数据:

2 1
2 1

要将2,1改变一个数使其异或和为零并且只能改变第一个数,应将其改为(1,1),可以在第一堆中拿走一个所以先手必胜。
本题主要思路为现将除了第k位的数剩下的位数都进行 ^ 运算,然后需要地、第k位的数为剩下的数进行 ^ 运算的数,因为只有这样才可以让它异或和为零,如果这个数比原来的数大的话,不符合题意,则先手必败,如果这个数比原来的数小的话,符合题意,则先手必胜。

code:

#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N];
void solve(){
   
    int n,k;
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>a[i];
    int ans=0;
    for(int i=1;i<=n;i++){
   
        if(i!=k) ans^=a[i];
    }
    cout<<(ans<a[k] ? "Yes":"No")<<'\n';
}
int main(){
   
    int t;
    cin>>t;
    while(t--){
   
        solve();
    }
    return 0;
}

题目描述

有一些石子堆,第i堆有 ai个石子。你和算卦先生轮流从任一堆中任取若干颗石子(每次只能取自一堆,并且不能不取),取到最后一颗石子的人获胜。
算卦先生来问你,如果你先手,你是否有必胜策略?若是改动其中几个石子堆中石子的数量呢?

输入描述

第一行两个正整数 n,q,表示有 n 个石堆,q 次操作。
第二行 n 个整数,第i个数 ai表示第i个石堆初始有 ai个石子。
接下去 q 行,每行两个正整数 x,y,表示把第x堆石子的个数修改成 y。操作是累计的,也就是说,每次操作是在上一次操作结束后的状态上操作的。

输出描述

共q行,输出每次操作之后先手是否有必胜策略如果有,输出"Kan",否则输出"Li"

输入样例

5 4
6 7 3 4 5
1 6
2 1
2 4
5 5

输出样例

Kan
Kan
Li
Li

code:

#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N];
int n,q;
int main(){
   
    cin>>n>>q;
    for(int i=1;i<=n;i++){
   
        cin>>a[i];
    }
    int ans=0;
    for(int i=1;i<=n;i++){
   
        ans^=a[i];
    }
    for(int i=1;i<=q;i++){
   
        int x,y;
        cin>>x>>y;
        ans^=a[x];//将上次的第x位去掉,当一个数被异或运算两次,这个数会变成原来的数
        //一个数异或0不会改变这个数
        a[x]=y;
        ans^=a[x];
        cout<<(ans ? "Kan":"Li")<<'\n';
    }
    return 0;
}

题目描述

现在有两堆石子,两个人轮流从中取石子,且每个人每一次只能取1、3或9个石子,取到最后一个石子的人win.
假设先手后手都会选择最好的方式来取石子,请您判断先后手的输赢情况。

输入描述:

多组输入
每组一行,一行包括两个正整数n1和n2(1<=n1<=100,1<=n2<=100),代表了两堆石子的数目

输出描述:

如果先手能赢,输出"win";否则就输出"1ose"。

输入

1 1
1 2

输出

lose
win

code1:

#include<iostream>
#include<map>
using namespace std;
map<pair<int ,int>,bool>mp;
int f(int x,int y){
   
    if(mp.count({
   x,y})) return mp[
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值