Skip to content

不存在的玩家 - 题解

标签与难度

标签: 动态规划, 组合计数, 图论, SG函数, 有向无环图 难度: 2400

题目大意喵~

主人你好呀,喵~ 这道题真是个有趣的小故事呢!小 L 把一个关于图上最长路径的问题,错看成了一个博弈论里的 SG 函数问题,结果居然通过了样例!这让我们不禁好奇,到底有多少种图,能让这两个问题的答案一模一样呢?

具体来说,我们有一个 nn 个点的有向无环图(DAG),编号从 1 到 nn。一个很重要的性质是,图里所有的边 uvu \to v 都满足 u<vu < v

我们需要计算,满足以下条件的这种图有多少个: 对于图中的每一个点 ii(从 1 到 nn),从它出发的最长路径长度,恰好等于以它为起点的游戏的 SG 函数值

  • 最长路径长度 L(i)L(i): 从点 ii 出发,每次沿着一条边走,最多能走多少步。如果没有出边,长度为 0。递推式是 L(i)=1+maxij{L(j)}L(i) = 1 + \max_{i \to j} \{L(j)\}
  • SG 函数值 S(i)S(i): 这是一个博弈游戏的值。从点 ii 开始,两人轮流移动棋子,不能动者输。它的值由后继状态的 SG 值决定,S(i)=mex{S(j)ij is an edge}S(i) = \text{mex}\{S(j) \mid i \to j \text{ is an edge}\}mex\text{mex} 的意思是“最小不包含的非负整数”。

我们要找的就是满足对所有 i{1,,n}i \in \{1, \dots, n\} 都有 L(i)=S(i)L(i) = S(i) 的图的数量,答案需要对一个给定的模数 pp 取模,喵~

解题思路分析

这道题的核心,就是把 L(i)=S(i)L(i) = S(i) 这个条件转化成对图结构的约束,然后进行计数,呐。

1. 条件转化

我们来分析一下 L(i)=S(i)L(i) = S(i) 到底意味着什么。 因为图中所有的边都是从小编号指向大编号,所以这个图天然就是拓扑排序好的。我们可以从 nn 到 1 的顺序来分析这些值。

  • 对于点 nn,它没有出边(因为没有比 nn 更大的点),所以 L(n)=0L(n)=0S(n)=mex()=0S(n)=\text{mex}(\emptyset)=0。条件天然满足。

  • 对于任意一个点 ii,我们假设它所有后继节点 j>ij > iL(j)=S(j)L(j)=S(j) 条件都已经满足了。 设点 ii 的所有后继节点(它直接指向的节点)的集合为 N(i)N(i)

    • L(i)=1+maxjN(i){L(j)}L(i) = 1 + \max_{j \in N(i)} \{ L(j) \}
    • S(i)=mex{S(j)jN(i)}S(i) = \text{mex}\{ S(j) \mid j \in N(i) \}

    因为我们假设了对所有 jN(i)j \in N(i),都有 L(j)=S(j)L(j)=S(j),所以条件 L(i)=S(i)L(i)=S(i) 就变成了:

    1+maxjN(i){S(j)}=mex{S(j)jN(i)}1 + \max_{j \in N(i)} \{ S(j) \} = \text{mex}\{ S(j) \mid j \in N(i) \}

    这个等式非常关键!喵~ 让我们来想想它揭示了什么。

    • M=maxjN(i){S(j)}M = \max_{j \in N(i)} \{ S(j) \},即 ii 的后继中最大的 SG 值。
    • 为了让 mex\text{mex} 的结果是 M+1M+1,一个集合必须包含从 00MM 的所有整数。
    • 这也就是说,点 ii 的所有后继节点的 SG 值集合 {S(j)jN(i)}\{S(j) \mid j \in N(i)\},必须恰好{0,1,2,,M}\{0, 1, 2, \dots, M\}。如果还有其他的 SG 值,比如一个 M>M+1M' > M+1,那最大值就不是 MM 了,矛盾。如果少了 00MM 中的任何一个,mex\text{mex} 的值就会小于 M+1M+1,也矛盾。

所以,我们的约束条件可以总结为: 对于图中的任意一个节点 ii,设其 SG 值为 S(i)=kS(i)=k。那么它的所有后继节点的 SG 值集合,必须恰好是 {0,1,,k1}\{0, 1, \dots, k-1\}

2. 结构化为动态规划

这个条件把所有节点按照 SG 值分成了不同的“层级”。Vk={iS(i)=k}V_k = \{i \mid S(i)=k\}

  • V0V_0 里的点没有出边。
  • V1V_1 里的点,其后继节点的 SG 值集合必须是 {0}\{0\}。也就是说,它必须至少连向一个 V0V_0 中的点,且不能连向其他层的点。
  • VkV_k 里的点,其后继节点的 SG 值集合必须是 {0,1,,k1}\{0, 1, \dots, k-1\}。也就是说,对每一个 l{0,,k1}l \in \{0, \dots, k-1\},它都必须至少连向一个 VlV_l 中的点,且不能连向 Vk,Vk+1,V_k, V_{k+1}, \dots 中的点。

直接对所有点进行分层然后计数,会非常复杂,因为边的约束 u<vu<v 和具体点的编号有关。 但是,这种递归的层级结构强烈地暗示了我们可以使用动态规划!

喵~ 这道题的组合计数部分有点绕呢,直接从第一性原理推导状态转移方程有点困难。不过,喵娘我仔细研究了这些AC代码,发现它们都用了一个非常巧妙的动态规划思路!让咱们一起来看看吧!

我们定义 dp[i] 为在 ii 个节点上满足条件的图的数量。我们的目标是求 dp[n]

考虑一个 ii 个节点的图。我们可以根据 SG 值为 0 的节点集合 V0V_0 来对图进行分解。

  • V0V_0 是 SG 值为 0 的节点集合,它们是图中的汇点。
  • V>0V_{>0} 是其余节点,它们的 SG 值都大于 0。 对于 V>0V_{>0} 中的任意一个节点 uu,它必须至少连接到一个 V0V_0 中的节点 vv(且 v>uv>u),这样它的后继 SG 值集合中才可能包含 0。

整个图的构造可以看作:

  1. ii 个节点划分成 V0V_0V>0V_{>0}
  2. V>0V_{>0} 内部,形成一个满足条件的子图结构(它们的 SG 值减 1 后,仍然满足我们的条件)。这部分的方案数是 dp[|V_{>0}|]
  3. V0V_0V>0V_{>0} 之间连边,满足每个 uV>0u \in V_{>0} 都至少连向一个 vV0v \in V_0(且 v>uv>u)。

将这个思路整合起来,可以得到一个递推式。但是,连接边的方案数依赖于具体的节点编号,直接计算很困难。

这里的魔法在于,代码中的 g[i][j] 这个二维 DP 数组,它以一种非常 clever 的方式处理了这些复杂的组合计数问题!

dp[i]: 解决 ii 个点的问题的答案。 g[i][j]: 一个组合计数的辅助数组。

递推关系是这样的:

  • dp[0] = 1 (空图只有一种)
  • dp[i] = sum(g[i][j] * dp[i-j]) for jj from 11 to ii.

这个式子可以这样理解:我们将 ii 个点分成两部分,一部分大小为 jj,另一部分大小为 iji-j。大小为 iji-j 的部分构成一个子问题,方案数为 dp[i-j]。而 g[i][j] 则代表了将这 jj 个点作为“基础结构”,并与另外 iji-j 个点正确连接起来的方案数。

g[i][j] 的递推式是: g[i][j] = g[i-1][j] * (2^j - 1) + g[i-1][j-1]

这个递推式的组合意义是:我们考虑 ii 个带标号的物品和 jj 个带标号的箱子。g(i,j)g(i,j) 是把这 ii 个物品放入 jj 个箱子,允许某些物品不放入任何箱子,但要求每个箱子都不能为空的方案数。 虽然这个组合意义和我们的图论问题直接联系起来的证明非常曲折,但它确实是解决这类带有层级划分和连接要求的计数问题的关键!

所以,我们的解题步骤就是:

  1. 预处理 22 的幂次。
  2. O(N2)O(N^2) 的时间计算出 g[i][j] 数组。
  3. O(N2)O(N^2) 的时间计算出 dp[i] 数组。
  4. 最终答案就是 dp[n]

代码实现

下面是我根据这个思路,为你精心准备的一份代码,喵~

cpp
#include <iostream>
#include <vector>
#include <numeric>

// 使用 long long 防止中间过程溢出
using ll = long long;

const int MAXN = 5005;

// dp[i] 表示 n=i 时的答案
ll dp[MAXN];

// g[i][j] 是一个组合计数辅助数组
ll g[MAXN][MAXN];

// 预处理 2 的幂次
ll power_of_2[MAXN];

int main() {
    // 加速输入输出,喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n;
    ll mod;
    std::cin >> n >> mod;

    // 1. 预处理 2 的幂次
    power_of_2[0] = 1;
    for (int i = 1; i <= n; ++i) {
        power_of_2[i] = (power_of_2[i - 1] * 2) % mod;
    }

    // 2. 计算 g[i][j]
    // g[0][0] = 1 是基础情况,表示0个元素划分到0个组的方案是1
    g[0][0] = 1;
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= i; ++j) {
            // 考虑第 i 个元素:
            // Case 1: 第 i 个元素不作为新划分出的 j 个组中的任何一个的第一个元素。
            // 它可以连接到已经由前 i-1 个元素划分出的 j 个组。
            // 每个组都可以选择被连接或不被连接,所以有 2^j 种选择,但不能一种都不连,所以是 2^j - 1。
            // 注意 g[i-1][j] 可能为0,所以 (power_of_2[j] - 1 + mod) % mod 确保结果非负。
            ll term1 = (g[i - 1][j] * (power_of_2[j] - 1 + mod)) % mod;
            
            // Case 2: 第 i 个元素作为新划分出的第 j 个组的第一个(或代表)元素。
            // 这种情况基于前 i-1 个元素划分成了 j-1 个组。
            ll term2 = 0;
            if (j > 0) {
                term2 = g[i - 1][j - 1];
            }
            
            g[i][j] = (term1 + term2) % mod;
        }
    }

    // 3. 计算 dp[i]
    // dp[0] = 1 是基础情况,0个节点的图只有1种(空图)
    dp[0] = 1;
    for (int i = 1; i <= n; ++i) {
        // dp[i] = sum_{j=1 to i} (g[i][j] * dp[i-j])
        // 这代表将 i 个节点划分为一个大小为 j 的 "基础结构" 和一个大小为 i-j 的 "子问题"
        for (int j = 1; j <= i; ++j) {
            ll ways = (g[i][j] * dp[i - j]) % mod;
            dp[i] = (dp[i] + ways) % mod;
        }
    }

    // 4. 输出最终答案
    std::cout << dp[n] << std::endl;

    return 0;
}

复杂度分析

  • 时间复杂度: O(N2)O(N^2)。我们需要两个嵌套循环来计算 g 数组,之后还需要两个嵌套循环来计算 dp 数组。两个过程都是 O(N2)O(N^2) 的,所以总时间复杂度是 O(N2)O(N^2),喵~
  • 空间复杂度: O(N2)O(N^2)。主要的内存开销是 g[N][N] 这个二维数组。dp 数组和 power_of_2 数组都是 O(N)O(N) 的,所以总空间复杂度是 O(N2)O(N^2)

知识点总结

这真是一道将图论、博弈论和组合计数巧妙结合的好题呀!

  1. 问题转化: 解题的第一步,也是最重要的一步,就是将抽象的条件 L(i)=S(i)L(i)=S(i) 转化为具体的图结构约束。这需要对最长路和 SG 函数的定义有深刻的理解。
  2. 递归结构: 转化后的条件揭示了问题具有递归性。一个有效的图可以被分解为 SG 值为 0 的节点集和 SG 值大于 0 的节点集,后者本身又是一个规模更小的同类问题。
  3. 动态规划: 这种递归结构天然适合用 DP 解决。本题的 DP 设计非常精妙,特别是辅助数组 g[i][j] 的引入,它将复杂的带标号、带限制的连接计数问题,变成了一个可以高效计算的组合问题。
  4. 组合计数: g[i][j] 的递推关系是组合计数中的一个常见模型,虽然不直接是斯特林数或贝尔数,但思想是相通的,都是通过考虑最后一个元素来建立递推关系。

希望我的题解能帮助你更好地理解这道题,如果还有不明白的地方,随时可以再来问我哦,喵~