博弈论

博弈论入门

  • 巴什博弈
  • 尼姆博弈(Nim)
    • 台阶-Nim游戏
    • 集合-Nim游戏
    • 拆分-Nim游戏
  • 威佐夫博弈
  • 斐波那契博弈

1318. 取石子游戏(巴什博弈)

题目描述

有一种有趣的游戏,玩法如下:

玩家: 2 人;

道具: N 颗石子;

规则:

  1. 游戏双方轮流取石子;
  2. 每人每次取走若干颗石子(最少取 1 颗,最多取 K 颗);
  3. 石子取光,则游戏结束;
  4. 最后取石子的一方为胜。
    假如参与游戏的玩家都非常聪明,问最后谁会获胜?

输入格式

输入仅一行,两个整数 \(N\)\(K\)

输出格式

输出仅一行,一个整数,若先手获胜输出 \(1\),后手获胜输出 \(2\)

数据范围

\(1≤N≤10^5\),
\(1≤K≤N\)

输入样例:

23 3

输出样例:

1

解题思路

结论:\(n\) 整除 \(k+1\) 时,先手必败

分类讨论:

  1. \(n\leq k\),此时先手可以直接拿走所有石子,先手必胜
  2. \(n=m\times (k+1)\),对于每 \(k+1\) 堆石子(共 \(m\) 堆石子),不管先手怎么拿,后手都可以拿完这堆石子,先手必败
  3. \(n=m\times (k+1)+x,0<x<k+1\),先手可以将拿走 \(x\) 个石子,此时为必败态,转移到了后手,先手必胜
    故:\(n\) 整除 \(k+1\) 时,先手必败

代码

#include<bits/stdc++.h>
using namespace std;
int n,k;
int main()
{
    scanf("%d%d",&n,&k);
    if(n%(k+1))puts("1");
    else
        puts("2");
    return 0;
}

P2197 【模板】nim 游戏(尼姆博弈(Nim))


题目描述

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

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

输入格式

本题有多组测试数据。

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

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

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

输出格式

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

输入

2
2
1 1
2
1 0

输出

No
Yes

解题思路

结论:\(a_1\oplus a_2\oplus \dots\oplus a_n=0\) 时,先手必败

证明:

  1. \(0\oplus0\dots\oplus0=0\)
  2. 设有 \(a_1\oplus a_2\oplus\dots\oplus a_n=x,x>0\),设 \(x\) 最高位为第 \(k\) 位,则显然,在 \(1\sim n\) 堆石子中,必然存在一个 \(a_i\),其第 \(k\) 位为 \(1\),且有 \(x \oplus a_i<a_i\),则从该堆石子中取 \(a_i-x\oplus a_i\) 堆石子,此时该堆石子有 \(x\oplus a_i\) 个,故:\(a_1\oplus a_2\oplus a_i\oplus x \oplus\dots\oplus a_n=0\)
  3. 设有 \(a_1\oplus a_2\oplus\dots\oplus a_n=0\),设从 \(i\) 堆中取走石子,该堆剩下 \(a_k\) 个石子,设 \(a_1\oplus a_2\oplus a_k\oplus \dots\oplus a_n=0\),两个式子左右异或,有 \(a_i\oplus a_k=0\),即 \(a_i=a_k\),显然不成立,故有:\(a_1\oplus a_2\oplus\dots\oplus a_n=0\) 时,取石子后其异或值不为 \(0\)

最终状态一定是情况1,故有:\(a_1\oplus a_2\oplus \dots\oplus a_n=x,x>0\) 时,先手必胜

代码

  • 时间复杂度:\(O(n)\)
#include<bits/stdc++.h>
using namespace std;
int main()
{
    int t,n,res,other;
    for(scanf("%d",&t);t;t--)
    {
        scanf("%d%d",&n,&res);
        while(--n)
        {
            scanf("%d",&other);
            res^=other;
        }
        puts(res?"Yes":"No");
    }
    return 0;
}

892. 台阶-Nim游戏

题目描述

现在,有一个 \(n\) 级台阶的楼梯,每级台阶上都有若干个石子,其中第 \(i\) 级台阶上有 \(a_i\) 个石子(\(i≥1\))。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 \(n\)

第二行包含 \(n\) 个整数,其中第 \(i\) 个整数表示第 \(i\) 级台阶上的石子数 \(a_i\)

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

\(1≤n≤10^5\),
\(1≤a_i≤10^9\)

输入样例:

3
2 1 3

输出样例:

Yes

解题思路

\(a_1\oplus a_3\oplus a_5\oplus \dots=0\) 时,先手必败

\(a_1\oplus a_3\oplus a_5\oplus \dots=0\) 时,如果此时先手从偶数阶拿石子,此时后手可以把先手从偶数阶拿到奇数阶的石子拿到下一个偶数阶上,此时先手面临的状态依然是 \(a_1\oplus a_3\oplus a_5\oplus \dots=0\);如果此时先手从奇数阶拿石子,根据nim游戏证明,此时 \(a_1\oplus a_3\oplus a_5\oplus \dots=x,x \neq 0\),而后手可以从奇数阶中拿石子,使得先手依然面临 \(a_1\oplus a_3\oplus a_5\oplus \dots=0\) 的状态,最终的结束状态一定有 \(a_1\oplus a_3\oplus a_5\oplus \dots=0\),故有:\(a_1\oplus a_3\oplus a_5\oplus \dots=0\) 时,先手必败

  • 时间复杂度:\(O(n)\)

代码

#include<bits/stdc++.h>
using namespace std;
int n,x,res;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        if(i&1)
            res^=x;
    }
    puts(res?"Yes":"No");
    return 0;
}

893. 集合-Nim游戏

题目描述

给定 \(n\) 堆石子以及一个由 \(k\) 个不同正整数构成的数字集合 \(S\)

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 \(S\),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 \(k\),表示数字集合 \(S\) 中数字的个数。

第二行包含 \(k\) 个整数,其中第 \(i\) 个整数表示数字集合 \(S\) 中的第 \(i\) 个数 \(s_i\)

第三行包含整数 \(n\)

第四行包含 \(n\) 个整数,其中第 \(i\) 个整数表示第 \(i\) 堆石子的数量 \(h_i\)

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

\(1≤n,k≤100\),
\(1≤s_i,h_i≤10000\)

输入样例:

2
2 5
3
2 4 7

输出样例:

Yes

解题思路

定义:\(sg(x)=mex(sg(y_1,sg(y_2),sg(y_3),\dots)\),其中 \(y_1,y_2,y_3,\dots\) 为 状态 \(x\) 的后继状态,\(mex\) 运算为集合中未出现的最小自然数
结论:\(sg(x_1)\oplus sg(x_2)\oplus sg(x_3)\oplus \dots=0\) 时,其中 \(x_1,x_2,x_3,\dots\) 为初态,先手必败

对于一堆石子来说,假设初态为 \(x\),终态为 \(e\),由定义:\(sg(e)=0\),即 \(sg(e)=0\) 时说明没有后继状态,为必败态。假设先手初态时:\(sg(x)=k,k\neq 0\),由mex定义:其后继状态 \(y\),一定有 \(sg(y)=0,1,2,\dots,k-1\),故先手一定可以使后手面临 \(sg(y)=0\) 的状态,即必败态
对于好多堆石子来说,存在初态 \(x_1,x_2,x_3,\dots\),最终状态一定是 \(0,0,0,\dots\),即 \(sg(0)\oplus sg(0)\oplus sg(0)\oplus \dots=0\)。假设 \(sg(x_1)\oplus sg(x_2)\oplus sg(x_3)\oplus \dots=k,k\neq 0\),同nim游戏的分析方法,即:假设 \(k\) 的最高位为 \(t\),则 \(sg(x_1),sg(x_2),sg(x_3)\dots\) 中存在第 \(t\) 位为 \(1\),假设为 \(sg(x_i)\),由于 \(k\oplus sg(x_i)< sg(x_i)\),由mex定义,状态 \(x_i\) 一定可以转移到状态 \(sg(y)=k\oplus sg(x_i)\) 上,此时后手面对 \(sg(x_1)\oplus sg(x_2)\oplus sg(x_3)\oplus k\oplus sg(x_i)\oplus \dots=0\) 的状态,任意地,设其由 \(sg(x_j)\) 转移到 \(sg(y)\),由 mex定义:\(sg(y)<sg(x_j)\),假设 \(sg(x_1)\oplus sg(x_2)\oplus sg(x_3)\oplus sg(y)\oplus \dots=0\),与上式左右异或,有 \(sg(x_j)\oplus sg(y)=0\),即 \(sg(x_j)=sg(y)\),显然不成立,则有 \(sg(x_1)\oplus sg(x_2)\oplus sg(x_3)\oplus sg(y)\oplus \dots\neq 0\),最后状态一定有:\(sg(0)\oplus sg(0)\oplus sg(0)\oplus \dots=0\),故有:\(sg(x_1)\oplus sg(x_2)\oplus sg(x_3)\oplus \dots=0\) 时,其中 \(x_1,x_2,x_3,\dots\) 为初态,先手必败

另外,本题,采用了记忆化搜索,大大降低了时间复杂度

  • 时间复杂度:\(O(nk)\)

代码

#include<bits/stdc++.h>
using namespace std;
int n,k,x,s[105],f[10005];
int sg(int x)
{
    if(f[x]!=-1)return f[x];
    unordered_set<int> S;
    for(int i=1;i<=k;i++)
        if(s[i]<=x)S.insert(sg(x-s[i]));
    for(int i=0;;i++)
        if(!S.count(i))
            return f[x]=i;
}
int main()
{
    scanf("%d",&k);
    for(int i=1;i<=k;i++)scanf("%d",&s[i]);
    memset(f,-1,sizeof f);
    int res=0;
    scanf("%d",&n);
    while(n--)
    {
        scanf("%d",&x);
        res^=sg(x);
    }
    puts(res?"Yes":"No");
    return 0;
}

894. 拆分-Nim游戏

题目描述

给定 \(n\) 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 \(0\),且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 \(n\)

第二行包含 \(n\) 个整数,其中第 \(i\) 个整数表示第 \(i\) 堆石子的数量 \(a_i\)

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

\(1≤n,a_i≤100\)

输入样例:

2
2 3

输出样例:

Yes

解题思路

题意为每次操作将一堆变为两堆规模更小的石子,采用sg函数解决本题:先求出每一堆石子的sg函数,即对于一堆含有 \(x\) 个石子的石子堆可变为石子数为 \(y_1,y_2\) 的两堆石子,其中 \(y_1,y_2<x\),两堆石子对应的sg函数\(sg(y_1)\oplus sg(y_2)\),这是解决本题的关键,然后将所有石子堆的sg函数异或即可解决本题~

  • 时间复杂度:\(O(n^3)\)

代码

#include<bits/stdc++.h>
using namespace std;
int n,x,f[105];
int sg(int x)
{
    if(f[x]!=-1)return f[x];
    unordered_set<int> S;
    for(int i=0;i<x;i++)
        for(int j=0;j<=i;j++)
        S.insert(sg(i)^sg(j));
    for(int i=0;;i++)
        if(!S.count(i))
            return f[x]=i;
}
int main()
{
    scanf("%d",&n);
    memset(f,-1,sizeof f);
    int res=0;
    while(n--)
    {
        scanf("%d",&x);
        res^=sg(x);
    }
    puts(res?"Yes":"No");
    return 0;
}
posted @ 2021-10-30 21:18  zyy2001  阅读(212)  评论(0编辑  收藏  举报