博弈论(取石子游戏)dp+分析

题目描述
给定一个有 NN 个节点的有向无环图,图中某些节点上有棋子,两名玩家交替移动棋子。

玩家每一步可将任意一颗棋子沿一条有向边移动到另一个点,无法移动者输掉游戏。

对于给定的图和棋子初始位置,双方都会采取最优的行动,询问先手必胜还是先手必败。

输入格式
第一行,三个整数 N,M,KN,M,K,NN 表示图中节点总数,MM 表示图中边的条数,KK 表示棋子的个数。

接下来 M 行,每行两个整数 X,YX,Y 表示有一条边从点 XX 出发指向点 YY。

接下来一行,KK 个空格间隔的整数,表示初始时,棋子所在的节点编号。

节点编号从 11 到 NN。

输出格式
若先手胜,输出 win,否则输出 lose。

数据范围
1≤N≤2000,1≤N≤2000,
1≤M≤6000,1≤M≤6000,
1≤K≤N1≤K≤N
输入样例:
6 8 4
2 1
2 4
1 4
1 5
4 5
1 3
3 5
3 6
1 2 4 6
输出样例:
win
这题咋没人写题解嘞 qwqqwq
SG 函数
首先定义 mexmex 函数,这是施加于一个集合的函数,返回最小的不属于这个集合的非负整数
例:mex({1,2})=0,mex({0,1})=2,mex({0,1,2,4})=3mex({1,2})=0,mex({0,1})=2,mex({0,1,2,4})=3
在一张有向无环图中,对于每个点 uu,设其所有能到的点的 SGSG 函数值集合为集合 AA,那么 uu 的 SGSG 函数值为 mex(A)mex(A),记做 SG(u)=mex(A)SG(u)=mex(A)
例图:

例图解释:

SG(5)=mex({∅})=0SG(5)=mex({∅})=0
SG(3)=mex({SG(5)})=mex({0})=1SG(3)=mex({SG(5)})=mex({0})=1
SG(4)=mex({SG(5),SG(3)})=mex({0,1})=2SG(4)=mex({SG(5),SG(3)})=mex({0,1})=2
SG(2)=mex({SG(3)}=mex({1})=0SG(2)=mex({SG(3)}=mex({1})=0
SG(1)=mex({SG(2),SG(4)})=mex({0,2})=1SG(1)=mex({SG(2),SG(4)})=mex({0,2})=1
那么 SGSG 函数的定义说完了,这题和 SGSG 函数又有什么关系呢?
下面先说本题做法,再证明该方法正确性。

做法:求出每个棋子所在的点的 SGSG 函数值,将所有值异或起来。若异或值不为 00,则输出win,否则输出lose

证明:
首先,由于这是一张有向无环图,所以游戏最后一定会结束,也就是说每个棋子最后都会移动到一个点上,且该点没有任何能到达的点。
那么根据定义,结束状态的所有点的 SGSG 函数值异或起来为 00,做法对于结束状态可行。
所以接下来,只要证明出

任何一种每个棋子所在点的 SGSG 函数值异或起来非 00 的情况,一定能通过一次移动棋子,到达一个 每个棋子所在点的 SGSG 函数值异或起来为 00 的情况
任何一种每个棋子所在点的 SGSG 函数值异或起来为 00 的情况,一定不能通过一次移动棋子,到达一个每个棋子所在点的 SGSG 函数值异或起来为 00 的情况
那么做法就是对的

证明 1:
设每个棋子所在点的 SGSG 函数值分别为 a1,a2,⋯,ana1,a2,⋯,an
设 x=a1 XOR a2 XOR ⋯ XOR anx=a1 XOR a2 XOR ⋯ XOR an,设 xx 的最高位为第 kk 位,那么在 a1,a2,⋯,ana1,a2,⋯,an 中,一定有一个值的第 kk 位为 11
设该值为 aiai,那么由于 xx 的第 kk 位和 aiai 的第 kk 位都是 11,且第 kk 位是 xx 的最高位,所以 ai XOR xai XOR x 一定小于 aiai
又因为 aiai 是其中一个棋子所在点的 SGSG 函数值,那么根据 SGSG 函数值的定义,该点能到达的所有点中,一定存在一个点的 SGSG 函数值为 ai XOR xai XOR x
那么我们就可以将该点上的棋子,移到一个 SGSG 函数值为 ai XOR xai XOR x 的点上去
移完之后,原来每个棋子所在点的 SGSG 函数异或值就变为了 a1 XOR a2 XOR ⋯ XOR ai−1 XOR (ai XOR x) XOR ai+1 ⋯ XOR ana1 XOR a2 XOR ⋯ XOR ai−1 XOR (ai XOR x) XOR ai+1 ⋯ XOR an
=(a1 XOR a2 XOR ⋯ XOR an) XOR x=x XOR x=0=(a1 XOR a2 XOR ⋯ XOR an) XOR x=x XOR x=0
1 证毕

证明 2:
反证法,设将点 uu 上的棋子移动到点 vv 上后,每个棋子所在点的 SGSG 函数值仍然为 00
那就说明 SG(u)=SG(v)SG(u)=SG(v),不符合 SGSG 函数的定义,不成立
2 证毕

所以做法是正确的。

那么如何求出每个点的 SGSG 函数值呢?
记忆化搜索就好啦~
每层记忆化搜索中,如果该点的 SGSG 函数值已经被计算出,那就直接返回该值。否则用一个 setset 记录每个点能到的所有点的 SGSG 函数值集合,然后从 00 开始遍历,找到第一个 setset 里面没有的数,将该值记录在该点上并返回。

时间复杂度
最坏情况下,每个点都会被遍历一次,时间复杂度为 O(n)O(n)。

对于每个点,我们会将其所能到达的所有点扔到一个 set 中。

而每个点能到达的点的数量,取决于从该点出发的边的数量。

所以总共我们会往 set 中插入 mm 次。

但是对于每个 set,我们至多只会往其中插入 n - 1 个数。

所以对于 set 的总复杂度为 O(mlogn)O(mlog⁡n)。

那么本题的总时间复杂度即为 O(n+mlogn)O(n+mlog⁡n)。

C++C++ 代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <set>

using namespace std;

const int N = 2005;
const int M = 6005;

int n, m, k;
int h[N], e[M], ne[M], idx;          // 邻接表存图
int sg[N];                           // 存所有被计算过的点的 SG 函数值
int res;

inline void add(int u, int v)        // 加边函数。从点 u 向点 v 连一条有向边
{
    e[ ++ idx] = v;
    ne[idx] = h[u];
    h[u] = idx;
}

int SG(int u)
{
    if (~sg[u]) return sg[u];        // 如果当前 sg[u] 不是 -1,那么说明该点的 SG 函数值已经被计算过了,直接返回
    set<int> S;                      // 否则要建一个集合 S,存该点能到的所有点的 SG 函数值
    for (int i = h[u]; i; i = ne[i]) // 遍历点 u 能到达的所有点
        S.insert(SG(e[i]));          // 计算该点的 SG 函数值,并放入集合 S
    for (int i = 0; ; i ++ )         // 从 0 开始枚举所有非负整数
        if (!S.count(i))             // 如果该值没有在 S 中出现过
        {
            sg[u] = i;               // 那么将该值记录在 sg[u] 中并返回
            return i;
        }
}

int main()
{
    scanf("%d %d %d", &n, &m, &k);   // 读入题目中 N, M, K
    for (int i = 0; i < m; i ++ )    // 读入 M 条边并建图
    {
        int u, v;
        scanf("%d %d", &u, &v);
        add(u, v);
    }
    memset(sg, -1, sizeof sg);       // 先将 sg 数组中的所有值初始化成 -1,表示没有记录过
    while (k -- )                    // 读入 K 个棋子所在的点
    {
        int u;
        scanf("%d", &u);
        res ^= SG(u);
    }
    if (res) puts("win");            // 如果 res 不为 0,那么输出 win
    else    puts("lose");            // 否则输出 lose
    return 0;
}

作者:垫底抽风
链接:https://www.acwing.com/solution/content/15279/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


简单情况:所有堆的石子个数>1>1
设b=堆数+石子总数−1b=堆数+石子总数−1
先手必胜 <=> bb是奇数
对于任何一个奇数,一定存在一个偶数后继
对于任何一个偶数,所有后继必然是奇数
又当b=1b=1时,有b=1(堆数)+1(石子总数)−1=1b=1(堆数)+1(石子总数)−1=1则最终状态一定是奇数

证明思路:
奇数出发(自己)->能到偶数(对手)->必回奇数(自己)->…->剩1(自己)->自己赢

奇数:
堆数>1>1 => 合并两堆(堆数-1) b->偶数
堆数=1,≥3=1,≥3 => 取1石子(石子数-1)b->偶数
即任何一个奇数状态都可以转移到某一个偶数状态

偶数:
堆数>1>1 => 合并两堆(堆数-1) b->奇数

取一子
1 该堆>2>2 b->奇数
2 该堆=2=2 b->奇数
该堆剩1
2.1 总共堆数=1=1 则奇对手赢
2.2 总共堆数>1>1 则奇对手一定在之后把这剩1的堆给合并
假设剩两堆 22数的堆+奇数个数的堆(b=2+b=2+奇-1+2=1+2=偶) 拿完后 1+1+奇 奇对手合并两堆
->最后只剩11堆偶数给偶对手(b=1+b=1+偶−1−1)

一般情况:有堆的石子个数=1=1
石子个数=1=1的堆个数=aa
bb 其他个数(>1)(>1)的堆个数+其他堆石子总数−1−1
f(a,b)=⎧⎩⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪f(a−1,b),从a中取1个f(a,b−1),从b中取1个f(a,b−1),合并b中2个f(a−2,b+3),合并a中2个(b堆石子数+2,堆数+1)f(a−1,b+1),合并a中1个b中1个(b堆石子数+1,a个数−1)
f(a,b)={f(a−1,b),从a中取1个f(a,b−1),从b中取1个f(a,b−1),合并b中2个f(a−2,b+3),合并a中2个(b堆石子数+2,堆数+1)f(a−1,b+1),合并a中1个b中1个(b堆石子数+1,a个数−1)

#include <cstdio>
#include <cstring>
#include<iostream>

using namespace std;

const int N = 55, M = 50050;

int f[N][M];

int dp(int a, int b)
{
    int &v = f[a][b];
    if (v != -1) return v;
    // 简单情况
    if (!a) return v = b % 2;
    // 一般情况
    // 上一次取完后 b中只有1堆 且只有1个石子 b=1+1-1=1 这堆并入a中
    if (b == 1) return dp(a + 1, 0);
    // 有a 从a中取1个
    if (a && !dp(a - 1, b)) return v = 1;
    // 有b 从b中取1个 or 合并b中2个
    if (b && !dp(a, b - 1)) return v = 1;
    // 合并a中2个
    if (a >= 2 && !dp(a - 2, b + (b ? 3 : 2))) return v = 1;
    // 合并a中1个b中1个
    if (a && b && !dp(a - 1, b + 1)) return v = 1;

    return v = 0;
}

int main()
{
    memset(f, -1, sizeof f);

    int T;
    cin >> T;
    while (T -- )
    {
        int n;
        cin >> n;
        int a = 0, b = 0;
        for (int i = 0; i < n; i ++ )
        {
            int x;
            cin >> x;
            if (x == 1) a ++ ;
            // b==0时 加1堆+加x石子=0 + 1+x-1=x
            // b!=0时 加1堆+加x石子=原来的+x+1
            else b += b ? x + 1 : x;
        }

        if (dp(a, b)) puts("YES");
        else puts("NO");
    }

    return 0;
}

作者:仅存老实人
链接:https://www.acwing.com/solution/content/26214/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


在研究过 Nim 游戏及各种变种之后,Orez 又发现了一种全新的取石子游戏,这个游戏是这样的:

有 n 堆石子,将这 n 堆石子摆成一排。

游戏由两个人进行,两人轮流操作,每次操作者都可以从最左或最右的一堆中取出若干颗石子,可以将那一堆全部取掉,但不能不取,不能操作的人就输了。

Orez 问:对于任意给出的一个初始局面,是否存在先手必胜策略。

输入格式
第一行为一个整数 T,表示有 T 组测试数据。

对于每组测试数据,第一行为一个整数 n,表示有 n 堆石子,第二行为 n 个整数 ai ,依次表示每堆石子的数目。

输出格式
对于每组测试数据仅输出一个整数 0 或 1,占一行。

其中 1 表示有先手必胜策略,0 表示没有。

数据范围
1≤T≤10,
1≤n≤1000,
1≤ai≤109
输入样例:
1
4
3 1 9 4
输出样例:
0
假设当前i∼ji∼j堆固定 o[i j]o
left[i][j]left[i][j]表示我在左边放多少个石子先手必败
left[i][j]left[i][j]必然存在且唯一
唯一:
反证:
假设有两个取值L1<L2L1<L2使得先手必败
当左边先放L2L2并先手拿完后剩L1L1个给对手的局面时自己就不是必败了

同理可以求一个right[i][j]right[i][j]表示我在右边放多少个石子先手必败

最后答案:
left[2][n]==a[1]left[2][n]==a[1]
left[i][j]left[i][j]递推
?[i j-1] 第j堆(石子个数x)
则我们希望求的是假设i~j已经固定了,我们在左边放多少个可以使得?[i j-1] 第j堆是必败的
定义:
左边放LL时,L[i j-1]必败
右边放RR时,[i j-1]R必败

情况1:
R=xR=x [i j-1]x
left[i][j]=0left[i][j]=0
情况2:
x<L且x<Rx<L且x<R x[i j-1]x
left[i][j]=xleft[i][j]=x
不管先手怎么取 必然在取完后某一此后某一边剩0个石子,另一边剩yy个石子0<y≤x0<y≤x
0[i j-1]y
此时 由于x<L,x<Rx<L,x<R 0<y≤x0<y≤x
有y<L,y<Ry<L,y<R
此时留给对手只有在左边是LL或者右边是RR的时候才是必败,则此时对手必然不是必败,则自己是必败

情况3:
3.1
L>RL>R 则有R<x≤LR<x≤L
x-1 [i j-1] x
此时右边取xx 左边取x−1x−1
left[i][j]=x−1left[i][j]=x−1时必败

先手取右边
首先 先手一定不能把右边取RR
假设先手把右边取RR 后手把左边取完 则先手必败

如果先手把右边取到x<Rx<R时, 后手立即把左边取到和右边相同–转化为情况2–先手必败

如果先手把右边取到x>Rx>R时,后手立即把左边取到比右边少11的x−1x−1
由于右边>R>R 则右边xx最小取到11 左边xx最小取到00
则必然会将右边取到RR(情况1)或者RR以下(情况2) – 必败

先手取左边
如果先手把左边取到x≥Rx≥R时,后手就把右边边取到x+1x+1 (情况3.1)

如果先手把左边取到x<Rx<R时,后手就把右边边取到xx (情况2)

则只要左边≥R≥R右边≥R+1≥R+1 后手保证左边比右边少一个(情况3),一旦左边或者右边有一个<R<R,后手就保证左右两边一样多(情况2)

3.2
R>LR>L 则有L≤x<RL≤x<R
left[i][j]=x+1left[i][j]=x+1 先手必败
x+1 [i j-1] x

如果先手把左边取到≥L+1≥L+1时,后手就把右边边取到≥L≥L 后手永远保证左边比右边多11
如果先手把左边取到LL时,后手就把右边取完 ,先手必败

如果先手把左边或右边取到<L<L,后手就保证左右两边一样多 ,先手必败

后手保证左边比右边多一个(情况3.2),一旦左边或者右边有一个<L<L,后手就保证左右两边一样多(情况2)

情况4
x>L且x>Rx>L且x>R
left[i][j]=xleft[i][j]=x
意味着只要x>L且x>Rx>L且x>R 左边和右边取一样多就行

4.1
L>RL>R时
先手取完后>L>L 后手保证左右两边相同

一旦先手把某一边个数取到(R,L](R,L] 后手保证左边比右边少一个(情况3.1)
一旦先手把某一边个数取到[R,L)[R,L) 后手保证右边比左边多一个
一旦先手把某一边个数取到(,R)(,R) 后手保证右边和左边一样多

4.2
R>LR>L时
对称
先手取完后>R>R 后手保证左右两边相同
一旦先手把某一边个数取到(L,R](L,R] 后手保证右边比左边少一个(情况3.2)
一旦先手把某一边个数取到[L,R)[L,R) 后手保证左边比右边多一个
一旦先手把某一边个数取到(,L)(,L) 后手保证右边和左边一样多

#include<iostream>
#include<cstring>

using namespace std;

const int N = 1010;

int n;
int a[N];
int l[N][N], r[N][N];

int main()
{
    int T;
    cin >> T;
    while(T--)
    {
        cin >> n;
        for (int i = 1; i <= n; i ++ ) cin >> a[i];
        // 长度
        for (int len = 1; len <= n; len ++ )
            //  左右端点i 右端点j
            for (int i = 1; i + len - 1 <= n; i ++ )
            {
                int j = i + len - 1;
                // 区间只有一个堆x 则左右取为x -> x [x] x
                // 只要先手取哪一堆 后手在另一堆取一样多的石子
                // 则只要先手取得这堆有,后手取得另一堆也一定有,
                // 直到先手取得那堆取完,后手把另一堆取完 先手必败
                if (len == 1) l[i][j] = r[i][j] = a[i];
                else
                {
                    int L = l[i][j - 1], R = r[i][j - 1], X = a[j];
                    // 情况1
                    if (R == X) l[i][j] = 0;
                    // 情况2 情况4
                    else if (X < L && X < R || X > L && X > R) l[i][j] = X;
                    // 情况3.1 情况4.1
                    else if (L > R) l[i][j] = X - 1;
                    // 情况3.2 情况4.2
                    else l[i][j] = X + 1;

                    // 与上述情况对称的四种情况
                    L = l[i + 1][j], R = r[i + 1][j], X = a[i];
                    if (L == X) r[i][j] = 0;
                    else if (X < L && X < R || X > L && X > R) r[i][j] = X;
                    else if (R > L) r[i][j] = X - 1;
                    else r[i][j] = X + 1;
                }
            }

        if (n == 1) puts("1");
        else printf("%d\n", l[2][n] != a[1]);
    }
    return 0;
}

作者:仅存老实人
链接:https://www.acwing.com/solution/content/26286/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值