数学-博弈论. 集合-Nim游戏

c++

AcWing 893. 集合-Nim游戏

/*
 *  题目描述:
 *      Acwing 893. 集合-Nim游戏:
 *      给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。
 *      现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。
 *      问如果两人都采用最优策略,先手是否必胜。
 *      输入格式:
 *      第一行包含整数 k,表示数字集合 S 中数字的个数。
 *      第二行包含 k 个整数,其中第 i 个整数表示数字集合 S 中的第 i 个数 si。
 *      第三行包含整数 n。
 *      第四行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 hi。
 *      输出格式:
 *      如果先手方必胜,则输出 Yes。
 *      否则,输出 No。
 *      数据范围:
 *      1 ≤ n, k ≤ 100,
 *      1 ≤ si, hi ≤ 10000
 *
 *  解题思路:
 *      对于 Nim 问题,我们通常需要考虑初始必负态,然后一步步导入到其他必负态和必胜态。
 *      对于本题,n 堆石子过于复杂,我们首先考虑 1 堆石子的情况,必负态是 0 个石子,
 *      然后根据必胜态的定义: 存在一种方案可以使得对手达到必负态的状态,迅速确定出其他必胜态和必负态(无法到达其他必负态的状态)。
 *      然后对于每堆石子,都可以求出最终的状态是必胜态和必负态,倘若必负态为奇数,说明先手必负,否则先手必胜。
 *
 *      看似上种方法非常合理,但是为 wrong answer,原因在于不同堆石子之间计算必胜态和必负态不难简单的奇数偶数统计。
 *          简单证明一下错误性: 倘若当前存在 5 堆石子,存在两个必胜态,三个必负态,按照上种方法可知先手为必负的。
 *          但是,先手可以有可能存在一种方案,通过移动必胜态的那堆石子,使其还是必胜态,也就是说,后手的状态局面
 *          仍然是 5 堆石子,存在两个必胜态,三个必负态,难道存在先手和后手同时存在必胜吗?显然上面这个做法不合适。
 *
 *      那么,该问题应当如何判断多个堆石子合并后的必胜态、必负态呢?在最初的 Nim 游戏版本中,每堆石子拿取的数量是不受限制的,
 *      我们能否通过某些方法 (DFS) 转移到类似的问题呢?
 *          首先,我们定义一个运算,Mex运算:
 *              设S表示一个非负整数集合.定义mex(S)为求出不属于集合S的最小非负整数运算,即:
 *              mes(S)=min{x};
 *              例如:S={0,1,2,4},那么mes(S)=3;
 *
 *          其次,定义 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).
 *
 *          看起来这些函数怪怪的对不对?我们详细解释一下,说明其来源的合理性,并查看其对于最终答案的作用。
 *              SG(x) = 0,说明当前状态为必负态(仅代表当前堆),因为倘若当前 = 0,那么其指向的状态必定 > 0,或者是当前为最终状态,
 *                  对方可以一直让先手出于 0 的尴尬境地,直到达到最终状态 0,无法选择石子。
 *              SG(x) > 0,说明当前状态为必胜态(仅代表当前堆),可以一直控制对手为 0,直至对手无法选择石子。
 *                  那么 SG(x) 这个具体数值的含义是什么呢?他很像是我们必胜态必负态的扩展,但是存在何作用呢?
 *
 *          我们先给出合并堆状态的最终公式,而后证明其合理性。
 *          res = SG(a1) ^ SG(a2) ^ ··· ^ SG(an),倘若 res = 0,说明先手必负,否则说明先手必胜。
 *          证明:
 *              1. SG(a1) = SG(a2) = ... = SG(an) = 0 时,当前为必负态,满足 SG(a1) ^ SG(a2) ^ ··· ^ SG(an) = 0 条件。
 *              2. 当 SG(a1) ^ SG(a2) ^ ··· ^ SG(an) = x != 0 时,必定存在 i,满足 x 的最大二进制位数 1 在 SG(ai) 中也存在
 *                  因此,SG(ai) -> (SG(ai) ^ x) 可以将对手置于 SG(a1) ^ SG(a2) ^ ··· ^ SG(an) = 0 条件,
 *                  **(SG(ai) ^ x) 严格 小于 SG(ai)**,并且 SG 图中也可定存在改路线,因为 mes 操作满足了该条件。
 *                  注意是严格小于的,刚刚好 mes(S) = t,集合中并不包括 t。
 *              3. 当 SG(a1) ^ SG(a2) ^ ··· ^ SG(an) = 0,无论我和如何操作当前状态,新形成的 ai'
 *                  SG(a1) ^ SG(a2) ^ ··· ^ SG(ai') ^ ... ^ SG(an) != 0。
 *                  反证法可以证明,
 *                      倘若 SG(a1) ^ SG(a2) ^ ··· ^ SG(ai') ^ ... ^ SG(an) = 0。
 *                      又存在 SG(a1) ^ SG(a2) ^ ··· ^ SG(ai) ^ ... ^ SG(an) = 0
 *                      那么等式两边同时与操作
 *                              SG(ai') ^ SG(ai) = 0,说明 SG(ai) = SG(ai')。
 *                  因此,新形成的 SG(a1) ^ SG(a2) ^ ··· ^ SG(ai') ^ ... ^ SG(an) != 0 是肯定的。
 *              4. 因为 SG 图本身是是一个有向无环图,一定是可以在有限步数中走到最终点的,即使其中某些状态转移中 SG 可能会变大。
 *              
 *          聪明的小伙伴可能发现了,在 Nim 游戏中,我们的石子是越拿越少的,不能不拿,我们的 SG() 状态可不一定。
 *              比如说当前某一堆 SG 为 4,转移状态后 SG 可能为 10,但是这并不影响我们上面的正确性。
 *              
 */
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <set>
#include <map>
using namespace std;

const int N = 110, M = 10010;
int s[N], a[N], k, n;
int f[M];   // 请不要把 int 操作成 bool

int get_mex(int x) {
    if (x <= 0) {
        return 0;
    }

    if (f[x] != -1) {
        return f[x];
    }

    set<int> st;
    for (int i = 1; i <= k; i ++) {
        if (x < s[i]) {
            break;
        } else {
            st.insert(get_mex(x - s[i]));
        }
    }

    for (int i = 0; ; i ++ ) {
        if (st.count(i) == 0) {
            f[x] = i;
            break;
        }
    }
    return f[x];
}

bool solution() {
    memset(f, -1, sizeof f);
    sort(s + 1, s + k + 1);

    int res = 0;
    for (int i = 1; i <= n; i ++ ) {
        res = (res ^ get_mex(a[i]));
    }

    if (res != 0) {
        return true;
    } else {
        return false;
    }
}


int main()
{
    scanf("%d", &k);
    for (int i = 1; i <= k; i ++ ) {
        scanf("%d", &s[i]);
    }

    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) {
        scanf("%d", &a[i]);
    }

    if (solution()) {
        puts("Yes");
    } else {
        puts("No");
    }

    return 0;
}

posted @ 2022-07-24 20:47  lucky_light  阅读(243)  评论(0编辑  收藏  举报