【ACWing】893. 集合-Nim游戏

题目地址:

https://www.acwing.com/problem/content/895/

给定 n n n堆石子(每堆石子个数是 h i h_i hi)和一个 k k k个不同正整数构成的集合 S = { s 1 , . . . , s k } S=\{s_1,...,s_k\} S={ s1,...,sk},现在有两个玩家轮流操作,每次可以从任意一堆石子中拿取 S S S中存在的某个数字个数的石子,最后无法操作的人视为失败。问先手是否有必胜策略。

数据范围:
1 ≤ n , k ≤ 100 1\le n,k\le 100 1n,k100
1 ≤ s i , h i ≤ 10000 1\le s_i,h_i\le 10000 1si,hi10000

先介绍 S G SG SG函数(Sprague–Grundy函数)。我们可以将上面这样的游戏每个人面对的状态画成一个有向图(或者DFA,有限状态自动机),每个人面对一个局面的时候,他可以进行某种操作,使得对手面对的是下一个局面。显然这个图是无环的(因为 s i ≥ 1 s_i\ge 1 si1,所以这个游戏必然会在有限步内结束)。 S G SG SG函数是从状态集到非负整数的一个函数。对于无法操作的状态 S i S_i Si,定义其函数值 S G ( S i ) = 0 SG(S_i)=0 SG(Si)=0,对于其它状态,定义 S G ( S k ) = m e x { S G ( t ) : S k → t } SG(S_k)=\mathrm{mex}\{SG(t):S_k\to t\} SG(Sk)=mex{ SG(t):Skt}其中 m e x \mathrm{mex} mex函数是指minimum excluded函数,其在非负整数集上面的定义是: m e x ( S ) = min ⁡ { n : n ∈ N − S } \mathrm{mex}(S)=\min\{n:n\in \mathbb{N}-S\} mex(S)=min{ n:nNS}即不在 S S S中的最小非负整数。那么 S G ( S k ) SG(S_k) SG(Sk)表示在 S k S_k Sk能走到的所有状态的 S G SG SG函数值中,不在其内的最小非负整数。那么该有向图的每个状态都对应着一个 S G SG SG函数。有如下结论:如果对于某个状态 S k S_k Sk,如果 S G ( S k ) = 0 SG(S_k)=0 SG(Sk)=0,那么该状态是必败态;反之是必胜态。首先无法操作的状态的 S G SG SG函数值确实是 0 0 0,它们确实是必败态。接着,如果先手面对的是状态 S k S_k Sk并且 S G ( S k ) ≠ 0 SG(S_k)\ne 0 SG(Sk)=0,根据 S G SG SG函数的定义,先手从 S k S_k Sk必然能走到状态 S k + 1 S_{k+1} Sk+1使得 S G ( S k + 1 ) = 0 SG(S_{k+1})=0 SG(Sk+1)=0,而后手从 S k + 1 S_{k+1} Sk+1这个状态出发,必然走到的都是 S G SG SG函数值是正数的状态,接着先手又可以走到 S G SG SG值为 0 0 0的状态,以此类推。这样先手总有办法每次都面对 S G SG SG 0 0 0的状态,从而走到 S G SG SG 0 0 0的状态,而后手总是面对 S G SG SG 0 0 0的状态,而走到 S G SG SG 0 0 0的状态。由于游戏在有限步内必然终止,所以最后的无法操作的状态必然是后手遇到的,所以先手有必胜策略。否则后手有必胜策略。这就导出一个结论,对于起始状态 S 0 S_0 S0,先手有必胜策略当且仅当 S G ( S 0 ) ≠ 0 SG(S_0)\ne 0 SG(S0)=0

接着考虑更加复杂的情形,如果是有若干个不同的状态图,也是两个玩家轮流玩,但是每次任意玩家都可以在其中任意一个图上进行操作。同样是无法操作了就判负。如果这些图的初始状态是 S 1 , S 2 , . . . , S n S_1,S_2,...,S_n S1,S2,...,Sn,Sprague–Grundy定理告诉我们,先手有必胜策略当且仅当 S G ( S 1 ) ∧ S G ( S 2 ) ∧ . . . ∧ S G ( S n ) ≠ 0 SG(S_1)\wedge SG(S_2)\wedge...\wedge SG(S_n)\ne 0 SG(S1)SG(S2)...SG(Sn)=0。证明方法和经典Nim游戏的证明方法完全类似,参考https://blog.csdn.net/qq_46105170/article/details/114006771

对于这道题目,可以将每个石子堆看成一个有向图,状态就是当前石子个数。所以只需要算一下初始状态的 S G SG SG函数值即可。这可以用记忆化搜索来做。代码如下:

#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;

const int N = 110, M = 10010;

int k, n;
// s存能取哪些个数的石子,f[i]存初始石子数是i的状态的SG函数值
int s[N], f[M];

// 计算初始石子数是x的情况下的SG函数值
int sg(int x) {
    
    
	// 如果之前已经算出,则直接返回
    if (f[x] != -1) return f[x];
	
	// 记录一下从当前状态可以走到的那些状态的SG函数值
    unordered_set<int> set;
    for (int i = 0; i < k; i++) {
    
    
        if (x >= s[i]) set.insert(sg(x - s[i]));
    }

	// 找到第一个不在set里的非负整数,这就是SG函数值
    for (int i = 0; ; i++)
        if (!set.count(i)) return f[x] = i;
}

int main() {
    
    
    cin >> k;
    for (int i = 0; i < k; i++) cin >> s[i];
    cin >> n;

    memset(f, -1, sizeof f);

    int res = 0;
    // 计算一下以x为石子数的初始状态对应的SG函数值,并做异或
    for (int i = 0; i < n; i++) {
    
    
        int x;
        cin >> x;
        res ^= sg(x);
    }

    cout << (res != 0 ? "Yes" : "No") << endl;

    return 0;
}

时间复杂度 O ( k max ⁡ h i ) O(k\max h_i) O(kmaxhi),空间 O ( max ⁡ h i ) O(\max h_i) O(maxhi)

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/114006814