1 条题解

  • 0
    @ 2025-12-11 23:43:00

    题目分析

    题目要求我们根据给定的信息——对于每个位置 ii,以 ii 为右端点的最长连续区间长度 LiL_i——计算符合条件的排列个数。

    1. 连续区间的性质

    • 定义:一个区间是连续的,当且仅当其最大值与最小值的差等于区间长度减 1。即该区间内的数排序后是连续的整数序列。
    • 嵌套性质:如果两个连续区间有交集且不包含,则它们的交集也是连续区间,它们的并集也是连续区间。但在本题中,我们只关心“最长”的连续区间。题目给出的 LiL_i 实际上描述了一个树形结构(析合树/连续段树的思想)。

    2. 树形结构的转化

    对于一个排列,所有的连续区间构成了一个层级结构。对于每个右端点 ii,题目给出了最长的连续区间长度 lenilen_i,即区间 [ileni+1,i][i - len_i + 1, i] 是连续的。 这实际上定义了一个树形包含关系

    • 对于每个位置 ii,区间 [ileni+1,i][i - len_i + 1, i] 要么被包含在另一个更大的连续区间内,要么与其它区间不相交(除了包含它的区间)。
    • 我们可以用单调栈来通过 LiL_i 还原出这个树形结构。
      • 遍历 ii 从 1 到 nn
      • 对于当前的区间 [li,i][l_i, i](其中 li=iLi+1l_i = i - L_i + 1),它必须包含栈顶的若干个区间。
      • 如果 lil_i 穿插在栈顶区间的中间(即 lstk[top]<li<stk[top]l_{stk[top]} < l_i < stk[top]),则无解,因为这破坏了最长连续区间的性质(或者说暗示了存在一个以 stk[top]stk[top] 为右端点的更长的连续区间,与输入矛盾)。
      • 如果 lil_i 恰好包住了栈顶的若干区间,那么这些被包住的区间就成为了 ii 的“子节点”。
    • 最终,整个排列对应于根节点代表的区间 [1,n][1, n]

    3. 问题的分解

    对于树中的每一个节点(代表一个连续区间),它由若干个子节点(子连续区间)拼接而成。 设一个节点代表的区间长度为 lenlen,它有 kk 个子节点(子连续区间),长度分别为 s1,s2,,sks_1, s_2, \dots, s_k。 我们需要确定这 kk 个子节点在父区间内的相对数值大小关系。 这实际上等价于:构造一个长度为 kk 的排列,使得该排列中除了整个区间 [1,k][1, k] 外,不存在其他的连续区间(因为如果存在,那么对应的子节点就会合并成一个更大的连续区间,作为当前节点的子节点,这与给定的树结构矛盾)。 这种排列被称为单排列(Simple Permutation)或不可约排列

    4. 计数 DP

    fnf_n 表示长度为 nn 的单排列的个数。 那么对于题目给出的树形结构,总方案数就是所有非叶子节点的子节点数量 kk 对应的 fkf_k 的乘积。 问题的核心转化为了求 fnf_n

    5. 求单排列个数 fnf_n

    我们可以通过容斥原理或生成函数来求 fnf_n。 全排列个数为 n!n!。 一个排列如果不是单排列,那么它一定包含一个长度 <n< n 的连续区间。 这可以看作是将排列分解为若干个极大的连续段。 设 gn=n!g_n = n!。 根据析合树理论,任意排列都可以唯一分解。 具体推导涉及到较为复杂的生成函数关系。 根据经典的结论(参考 OEIS A000111 或相关论文),fnf_n 满足以下关系: 令 F(x)=n=1fnxnF(x) = \sum_{n=1}^{\infty} f_n x^n 为单排列的生成函数。 实际上,本题所用的 fnf_n 稍有不同,因为题目定义的连续区间是针对“以 ii 为右端点的最长连续区间”。这其实对应的是析合树中合点的子节点排列。如果是析点,子节点必须按升序或降序排列(只有2种情况,但题目中给出的限制实际上排除了析点的情况,或者说对于本题的输入格式,我们只需要考虑一种通用的不可约计数)。

    仔细观察代码中的 DP 递推式: solve(l, r) 函数实际上是在做分治 FFT/NTT 来求解 fnf_n。 递推式为: $f_n = (n-1) f_{n-1} + \sum_{k=2}^{n-2} f_k (n-1-k)! \times \dots$ ? 不,代码中的逻辑更接近于求: 有多少个长度为 nn 的排列,满足对于任意 i<ni < n,以 ii 为右端点的最长连续区间长度都是 1(即 Li=1L_i=1)? 并且 Ln=nL_n = n。 这正是对应于树形结构中,一个节点由 nn 个子节点组成的情况。

    经过数学推导(此处省略复杂的生成函数方程),得到 fnf_n 的递推关系。代码中使用的是分治 NTT 来加速这个卷积过程。 具体来说,代码中计算的 fif_i 实际上是满足“只有最后一个位置的最长连续区间长度为 ii,其余位置均为 1”的排列个数

    代码详解

    1. 多项式板子 (Poly 命名空间)

    代码包含了一个完整的多项式运算库,支持 NTT、求逆、求导、积分、ln、exp、开根、快速幂等。

    • ntt: 快速数论变换,用于 O(nlogn)O(n \log n) 多项式乘法。
    • get_inv: 多项式求逆。
    • ln, exp: 多项式对数和指数函数。 虽然板子很全,但本题实际上只用到了 分治 FFT (NTT) 来进行多项式乘法加速 DP 转移。

    2. 预处理 fnf_n (solve 函数)

    f[i] 存储的是长度为 i 的“核心排列”个数。 观察 solve 函数,它是一个典型的分治 NTT 结构,用于计算满足特定卷积形式的递推式。 核心递推逻辑(简化理解): 通过分治,左区间 [l,mid][l, mid] 计算出的 ff 值,通过卷积贡献给右区间 [mid+1,r][mid+1, r]。 涉及到两个卷积:

    1. z = x * y: 其中 xx 对应 fi×(i1)f_i \times (i-1)yy 对应 fif_i。这是处理某种形式的转移。
    2. z = x * y (在 l > 2 时): 处理另一种边界情况的转移。 这部分预处理出了所有可能用到的节点大小对应的方案数。

    3. 主函数逻辑

    • 读入与预处理:

      cin >> genshin >> n; // genshin 是 T, n 是排列长度
      f[0] = 1, f[1] = 2; // 初始值
      solve(2, n); // 分治 NTT 预处理 f 数组
      

      注意:这里 f[1]=2f[1]=2 似乎是个特殊的定义,或者对应题目的具体计数意义(比如长度为1的排列有1种,但为了配合公式变成了2?或者代码里的 ff 定义略有偏移)。实际上通常 f1=1f_1=1。代码中 f[1]=2f[1]=2 可能是为了处理 i×f[i]i \times f[i] 之类的项方便,或者 ff 的下标含义有偏移。 修正理解:结合 solve 里的 f[i] * (i-1),以及题目性质,这里的 fnf_n 实际上计算的是合法的单层结构排列数。

    • 处理每组数据: 对于每组输入 L1,,LnL_1, \dots, L_n:

      1. 还原树结构: 使用单调栈 stk

        l[i] = i - l[i] + 1; // 计算左端点
        while (top && l[i] <= stk[top]) {
            if (l[i] > l[stk[top]]) flag = 0; // 出现交叉,不合法
            ++s[i]; // i 节点的子节点数量 +1
            --top;  // 栈顶元素成为 i 的子节点,出栈
        }
        stk[++top] = i; // i 入栈
        

        s[i] 记录了以 ii 为右端点的最长连续区间(作为父节点)直接包含的子连续区间(子节点)的数量。

      2. 判无解:

        • l[n] != 1: 整个排列必须是一个连续区间,即 LnL_n 必须等于 nn,左端点必须是 1。
        • !flag: 出现区间交叉,不满足树形结构。
      3. 统计答案:

        ans = 1;
        L(i, 1, n) ans = ans * f[s[i]] % mod;
        

        答案就是所有非叶子节点的子节点个数 s[i]s[i] 对应的方案数 fs[i]f_{s[i]} 的乘积。 注意:对于叶子节点(即 Li=1L_i=1 的位置),在单调栈处理中,它们不会成为任何节点的父节点(或者说它们没有子节点弹出),s[i]s[i] 为 0(除了初始值)。 但在代码中,s[i] 是在 while 循环中统计的,也就是 ii 弹出了多少个栈顶元素,它就有多少个子节点。 对于叶子节点,Li=1L_i=1,它入栈前不会弹出任何元素(因为 LijL_i \le j 且栈顶元素右端点 <i< i),所以 s[i]=0s[i]=0f[0]=1f[0] = 1,乘积不受影响。 对于非叶子节点,如果有 kk 个子节点,就乘上 fkf_k

    总结

    1. 转化: 将 LiL_i 限制转化为析合树(连续段树)的结构判定。利用单调栈判断是否合法并统计每个节点的子节点数量。
    2. 计数: 总方案数等于树中每个节点内部排列方案数的乘积。节点内部排列方案数 fnf_n 是一个关于不可约排列的计数序列。
    3. 计算 fnf_n: 利用分治 NTT 在 O(nlog2n)O(n \log^2 n) 时间内预处理出 fnf_n
    4. 复杂度: 预处理 O(nlog2n)O(n \log^2 n),每组询问 O(n)O(n)。总复杂度 O(nlog2n+Tn)O(n \log^2 n + T n)

    该代码通过多项式技巧解决了核心的计数递推,配合单调栈解决了树形结构的解析。

    • 1

    信息

    ID
    6094
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者