1 条题解
-
0
题目分析
题目要求我们根据给定的信息——对于每个位置 ,以 为右端点的最长连续区间长度 ——计算符合条件的排列个数。
1. 连续区间的性质
- 定义:一个区间是连续的,当且仅当其最大值与最小值的差等于区间长度减 1。即该区间内的数排序后是连续的整数序列。
- 嵌套性质:如果两个连续区间有交集且不包含,则它们的交集也是连续区间,它们的并集也是连续区间。但在本题中,我们只关心“最长”的连续区间。题目给出的 实际上描述了一个树形结构(析合树/连续段树的思想)。
2. 树形结构的转化
对于一个排列,所有的连续区间构成了一个层级结构。对于每个右端点 ,题目给出了最长的连续区间长度 ,即区间 是连续的。 这实际上定义了一个树形包含关系。
- 对于每个位置 ,区间 要么被包含在另一个更大的连续区间内,要么与其它区间不相交(除了包含它的区间)。
- 我们可以用单调栈来通过 还原出这个树形结构。
- 遍历 从 1 到 。
- 对于当前的区间 (其中 ),它必须包含栈顶的若干个区间。
- 如果 穿插在栈顶区间的中间(即 ),则无解,因为这破坏了最长连续区间的性质(或者说暗示了存在一个以 为右端点的更长的连续区间,与输入矛盾)。
- 如果 恰好包住了栈顶的若干区间,那么这些被包住的区间就成为了 的“子节点”。
- 最终,整个排列对应于根节点代表的区间 。
3. 问题的分解
对于树中的每一个节点(代表一个连续区间),它由若干个子节点(子连续区间)拼接而成。 设一个节点代表的区间长度为 ,它有 个子节点(子连续区间),长度分别为 。 我们需要确定这 个子节点在父区间内的相对数值大小关系。 这实际上等价于:构造一个长度为 的排列,使得该排列中除了整个区间 外,不存在其他的连续区间(因为如果存在,那么对应的子节点就会合并成一个更大的连续区间,作为当前节点的子节点,这与给定的树结构矛盾)。 这种排列被称为单排列(Simple Permutation)或不可约排列。
4. 计数 DP
令 表示长度为 的单排列的个数。 那么对于题目给出的树形结构,总方案数就是所有非叶子节点的子节点数量 对应的 的乘积。 问题的核心转化为了求 。
5. 求单排列个数
我们可以通过容斥原理或生成函数来求 。 全排列个数为 。 一个排列如果不是单排列,那么它一定包含一个长度 的连续区间。 这可以看作是将排列分解为若干个极大的连续段。 设 。 根据析合树理论,任意排列都可以唯一分解。 具体推导涉及到较为复杂的生成函数关系。 根据经典的结论(参考 OEIS A000111 或相关论文), 满足以下关系: 令 为单排列的生成函数。 实际上,本题所用的 稍有不同,因为题目定义的连续区间是针对“以 为右端点的最长连续区间”。这其实对应的是析合树中合点的子节点排列。如果是析点,子节点必须按升序或降序排列(只有2种情况,但题目中给出的限制实际上排除了析点的情况,或者说对于本题的输入格式,我们只需要考虑一种通用的不可约计数)。
仔细观察代码中的 DP 递推式:
solve(l, r)函数实际上是在做分治 FFT/NTT 来求解 。 递推式为: $f_n = (n-1) f_{n-1} + \sum_{k=2}^{n-2} f_k (n-1-k)! \times \dots$ ? 不,代码中的逻辑更接近于求: 有多少个长度为 的排列,满足对于任意 ,以 为右端点的最长连续区间长度都是 1(即 )? 并且 。 这正是对应于树形结构中,一个节点由 个子节点组成的情况。经过数学推导(此处省略复杂的生成函数方程),得到 的递推关系。代码中使用的是分治 NTT 来加速这个卷积过程。 具体来说,代码中计算的 实际上是满足“只有最后一个位置的最长连续区间长度为 ,其余位置均为 1”的排列个数。
代码详解
1. 多项式板子 (
Poly命名空间)代码包含了一个完整的多项式运算库,支持 NTT、求逆、求导、积分、ln、exp、开根、快速幂等。
ntt: 快速数论变换,用于 多项式乘法。get_inv: 多项式求逆。ln,exp: 多项式对数和指数函数。 虽然板子很全,但本题实际上只用到了 分治 FFT (NTT) 来进行多项式乘法加速 DP 转移。
2. 预处理 (
solve函数)f[i]存储的是长度为i的“核心排列”个数。 观察solve函数,它是一个典型的分治 NTT 结构,用于计算满足特定卷积形式的递推式。 核心递推逻辑(简化理解): 通过分治,左区间 计算出的 值,通过卷积贡献给右区间 。 涉及到两个卷积:z = x * y: 其中 对应 , 对应 。这是处理某种形式的转移。z = x * y(在l > 2时): 处理另一种边界情况的转移。 这部分预处理出了所有可能用到的节点大小对应的方案数。
3. 主函数逻辑
-
读入与预处理:
cin >> genshin >> n; // genshin 是 T, n 是排列长度 f[0] = 1, f[1] = 2; // 初始值 solve(2, n); // 分治 NTT 预处理 f 数组注意:这里 似乎是个特殊的定义,或者对应题目的具体计数意义(比如长度为1的排列有1种,但为了配合公式变成了2?或者代码里的 定义略有偏移)。实际上通常 。代码中 可能是为了处理 之类的项方便,或者 的下标含义有偏移。 修正理解:结合
solve里的f[i] * (i-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]记录了以 为右端点的最长连续区间(作为父节点)直接包含的子连续区间(子节点)的数量。 -
判无解:
l[n] != 1: 整个排列必须是一个连续区间,即 必须等于 ,左端点必须是 1。!flag: 出现区间交叉,不满足树形结构。
-
统计答案:
ans = 1; L(i, 1, n) ans = ans * f[s[i]] % mod;答案就是所有非叶子节点的子节点个数 对应的方案数 的乘积。 注意:对于叶子节点(即 的位置),在单调栈处理中,它们不会成为任何节点的父节点(或者说它们没有子节点弹出), 为 0(除了初始值)。 但在代码中,
s[i]是在while循环中统计的,也就是 弹出了多少个栈顶元素,它就有多少个子节点。 对于叶子节点,,它入栈前不会弹出任何元素(因为 且栈顶元素右端点 ),所以 。 ,乘积不受影响。 对于非叶子节点,如果有 个子节点,就乘上 。
-
总结
- 转化: 将 限制转化为析合树(连续段树)的结构判定。利用单调栈判断是否合法并统计每个节点的子节点数量。
- 计数: 总方案数等于树中每个节点内部排列方案数的乘积。节点内部排列方案数 是一个关于不可约排列的计数序列。
- 计算 : 利用分治 NTT 在 时间内预处理出 。
- 复杂度: 预处理 ,每组询问 。总复杂度 。
该代码通过多项式技巧解决了核心的计数递推,配合单调栈解决了树形结构的解析。
- 1
信息
- ID
- 6094
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者