1 条题解
-
0
题意
给定 和模数 ( 不一定是素数),求有多少对排列 (长度均为 )满足:
- 的字典序小于 ;
- 的逆序数大于 的逆序数。
答案对 取模。
解题思路
本题是 异常排列对(困难版),。直接枚举所有排列对不可行,需要利用动态规划并优化转移。
1. 状态定义
设 表示长度为 的排列对 中,满足 的字典序小于 且 的数量。
最终答案即为其中 是最大可能逆序数(也是最小可能逆序数的绝对值)。
2. 递推关系
考虑在长度为 的排列对 (已满足 )中分别插入数字 。
设插入 的位置为 ( 到 , 表示最前面, 表示最后面),插入 的位置为 。
插入后新排列 的字典序关系:若 ,则 在第一位不同的位置(即第 位)是 中的 ,而 中对应位置是某个小于 的数,故 ;若 ,则前 位相同,第 位均为 ,后续部分保持 ,所以 仍然成立;若 ,则 在第 位是 , 中是较小数,此时 ,不满足条件。
因此,保持 的插入方式要求 。逆序数变化:插入 到 的第 位会使 增加 (因为 比后面所有数大),同理 增加 。
故逆序数差的变化量为记 ,则 (因为 )。对于固定的 ,满足 且 的有序对 个数为 (推导:令 ,,则 , 可取 到 ,共 种)。
于是得到递推式:
$$f(n, d) = \sum_{i = -(n-1)}^{0} f(n-1,\, d - i) \cdot (n - |i|). \tag{1} $$其中 的取值范围是因为 。
3. 对称形式的递推
注意到系数 关于 对称,且 中的 仅出现在 处。为了方便优化,我们可以将求和扩展到所有 (即 ),并利用对称性将 的项转化为 的项,从而得到与 等价的表达式:
$$f(n, d) = \sum_{|i| < n} f(n-1,\, d - i) \cdot (n - |i|). \tag{2} $$(严格证明需要用到 的某种对称性,此处从略,具体可参考 E1 的官方题解。)
4. 利用前缀和优化转移
直接使用 计算每个 需要 时间,而 的范围是 ,总复杂度 ,不可接受。
我们希望能快速从 的相邻值得到 的另一个值。定义前缀和:
对于固定的 ,考虑 与 的差。由 得:
$$\begin{aligned} f(n, k+1) - f(n, k) &= \sum_{|i|<n} \big(f(n-1, k+1-i) - f(n-1, k-i)\big) \cdot (n-|i|) \\ &= \sum_{i=-(n-1)}^{n-1} \big(f(n-1, k+1-i) - f(n-1, k-i)\big) \cdot (n-|i|). \end{aligned} $$将求和指标重新整理,可以写成:
$$f(n, k+1) = f(n, k) - \big(S(n-1, k) - S(n-1, k-n)\big) + \big(S(n-1, k+n) - S(n-1, k)\big). \tag{3} $$推导思路: 的系数 在 从 到 时,先线性增加( 负时 ),然后线性减少( 正时 )。利用前缀和可以将分段线性权重转化为两个区间和之差。具体推导可参考代码中的实现。
利用 ,我们可以从 推出 ,每次计算只需要常数时间(查前缀和)。因此,对于每个 ,我们可以用 的时间计算出所有 ,其中 是 的可能取值个数,约为 。总时间复杂度 ,空间复杂度可以优化到 (只保留上一层的 和 )。
5. 处理负数下标
的范围是 ,其中 。我们在数组下标上统一加上一个偏移量 ,使得实际存储位置为 。
6. 边界条件
当 时,只有一个排列 ,此时 不可能成立(因为只有一对 但 ,不满足严格小于),所以 。
也可以从 出发:定义 (空排列对),然后通过递推得到 的值。注意插入 时, 只能取 ,,系数 ,因此 。但 在 时实际上没有合法对,这里出现了不一致。实际上,我们的递推 适用于 ,而 时需要特殊处理:。不过也可以从 开始并修正最终结果,因为后续计算中 会被正确更新。为保险起见,我们直接初始化 的数组全为 ,然后从 开始递推。7. 最终答案
计算完 后,答案即为
$$\text{ans} = \sum_{d=1}^{D} f(n, d) \pmod{\textit{mod}}. $$8. 复杂度分析
- 时间:, 时约 次操作,配合优化和较短的循环常数,可在 4 秒内通过。
- 空间:(滚动数组存储当前层和上一层的 和 )。
参考代码结构
// 伪代码,实际实现时注意取模和负数下标偏移 int n, mod; cin >> n >> mod; int D = n * (n-1) / 2; int shift = D; vector<int> f_prev(2*D+5, 0), s_prev(2*D+5, 0); // 初始化 n=1 f_prev[0+shift] = 0; // 实际上没有合法对,但为了递推统一,可以设为0 // 或者从 n=0 开始: f0[0+shift]=1,然后递推到 n=1 后手动修正 for (int len = 2; len <= n; ++len) { int curD = len * (len-1) / 2; vector<int> f_cur(2*curD+5, 0), s_cur(2*curD+5, 0); // 计算 f_cur 的所有可能 d // 利用公式 (3) 从 f_cur 的最小 d 开始递推 int min_d = -curD, max_d = curD; // 先求出 f_cur[min_d] 的值(通过公式 (2) 直接计算,因为此时前缀和边界简单) // 然后 for d = min_d to max_d-1 用 (3) 递推 // 每步更新前缀和 s_cur } // 最后求和 long long ans = 0; for (int d = 1; d <= D; ++d) ans = (ans + f_cur[d+shift]) % mod; cout << ans << endl;注意:模数 可能不是素数,但递推中只涉及加减法,无需逆元,直接取模即可。
总结
本题的核心是设计状态 表示字典序合法且逆序数差为 的排列对数量,通过插入数字 得到递推式,并利用前缀和技巧将转移优化到 ,最终在 的范围内求解。
- 1
信息
- ID
- 6526
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 9
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者