1 条题解
-
0
题目重述
初始数组为 。每一步操作:设当前数组的逆序对数为 ,将 插入到数组的任意位置(包括开头和结尾)。给定 ,问经过若干次操作后,能得到的所有不同的、长度为 的数组的数量,结果对 取模。
关键观察
. 初始数组: 的逆序对数为 。 . 当逆序对数 时, 大于数组中的所有元素,插入 后,新数组的逆序对数会如何变化?
- 若插入到末尾:因为 比所有元素大,不会产生新的逆序对,逆序对数仍为 。
- 若插入到其他位置: 比它前面的所有元素大,但比它后面的所有元素小?等一下——如果 大于所有元素,那么它比后面所有元素都大,所以会与后面每个元素形成一个逆序对。因此逆序对数会增加,并且新的逆序对数 > ,从而继续保持 的性质。
因此,一旦逆序对数超过数组中的最大值,这个性质就会一直保持下去。
状态定义
设 表示:当前数组长度为 ,且当前逆序对数 严格大于数组所有元素时,最终能得到的长度为 的不同数组的数量。
转移推导
当前数组长度为 ,逆序对数为 ()。
- 如果我们在末尾插入 ,逆序对数不变,长度变为 ,仍然满足 。这样我们可以连续在末尾插入 多次,最后再在某个非末尾位置插入一次,从而跳到更大的长度。
- 更系统地说:
设当前是 的状态。我们考虑第一次在非末尾位置插入 的时刻。在此之前,我们可以在末尾插入 任意次(包括 次)。设我们在末尾插入了 次,那么长度变为 ,然后我们选择一个非末尾位置(有 种选择)插入 ,长度变为 ,此时新的逆序对数 ,状态变为 。
所以:
这里的 是:我们可以一直只在末尾插入 ,直到长度达到 ,这样得到一种最终数组(不再插入非末尾位置)。也就是说,我们可以在状态 时就停止并直接得到最终数组。
令 ,则:
$$dp[i] = i \cdot s + \sum_{j \ge 1} j \cdot dp[i+j+1] + 1 $$但更简单的推导来自标程:
标程中实际上是:
$$dp[i] = \left( i \cdot \sum_{j>i} dp[j] + 1 \right) \bmod M $$为什么?因为从 出发,选择一次非末尾插入,位置有 种(插入到前 个位置,因为末尾单独处理)。插入后长度变成 ,但这里要注意:插入一次后,逆序对数变为 ,状态变为 ,但 已经包含了之后的所有可能性。所以 直接依赖于 及之后的所有 值。
更严格的推导是:
$$dp[i] = i \cdot dp[i+1] + i \cdot dp[i+2] + \dots + 1 $$即:第一次非末尾插入的位置有 种,然后长度变成 并进入 状态;或者插入后长度变成 (如果我们在末尾先插入一次再在非末尾插入)等等。实际上就是:
这正是标程中的形式。
初始状态和最终答案
我们只关心 当 时,因为 对应初始 但此时 并不大于 ,所以 不能用这个递推。
但我们知道,初始时只有两个元素 ,逆序对数 。我们要从 出发,通过操作得到长度为 的数组。
有两种情况:
. 整个过程逆序对数一直
这意味着我们只插入 和 。可以证明,这样得到的数组只能是形如:前面一串 ,中间一个 模式,后面一串 。更具体地说,这类数组的数量是 (因为我们可以选择第一次出现 的位置,但最后要满足逆序对数 的限制)。标程中直接给出 种。. 过程中第一次出现逆序对数
$$\sum_{m=3}^n \left( \frac{(m-2)(m-1)}{2} - 1 \right) \cdot dp[m] $$
设此时数组长度为 (),并且这次插入的 。在此之前,数组处于“逆序对数 ≤1”的状态。前面已知,长度为 的“逆序对数 ≤1”的数组有 种(推导略,可参考提示)。然后我们在这个数组上插入一个 ,使得新的逆序对数 ,进入 状态。因此这部分贡献为:所以最终答案:
$$\text{ans} = (n-1) + \sum_{m=3}^n \left( \frac{(m-2)(m-1)}{2} - 1 \right) \cdot dp[m] \quad (\text{mod } 998244353) $$
代码实现
#include<bits/stdc++.h> using namespace std; const int N = 1e6 + 9, mod = 998244353; using ll = long long; int dp[N]; // dp[i] 表示长度 i 且当前逆序对数 > max 时,最终长度为 n 的数组数 void solve() { int n; cin >> n; int sum = 0; // 倒序计算 dp for (int i = n; i >= 1; i--) { dp[i] = (1LL * i * sum % mod + 1) % mod; sum = (sum + dp[i]) % mod; } int ans = n - 1; // 逆序对数 ≤1 的情况 for (int k = 3; k <= n; k++) { int ways = (1LL * (k - 1) * (k - 2) / 2 - 1 + mod) % mod; ans = (ans + 1LL * ways * dp[k]) % mod; } cout << ans << '\n'; } int32_t main() { ios_base::sync_with_stdio(0); cin.tie(0); int t = 1; cin >> t; while (t--) { solve(); } return 0; }
时间复杂度
预处理 数组 ,每个测试用例 ,总 ,可以通过。
总结
- 关键洞察:当逆序对数大于数组最大值时,插入操作有规律,可以 。
- 分两类讨论:始终 逆序对,和第一次出现 逆序对。
- 通过后缀和优化 转移。
- 1
信息
- ID
- 6604
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 1
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者