1 条题解
-
0
题目题解
问题理解
初始数组为空。每次操作可以选择 和 ,将
追加到数组末尾。给定 和 条限制 ,统计所有长度为 且满足限制的可构造数组的个数,对 取模。
第一步:数组的结构
每次追加的是一个 到 的循环移位排列。
最终数组由若干段这样的排列拼接而成,每段的数值范围是 。
第二步:重复计数问题
同一个数组可能有多种拆分方式。例如 可以拆成 或 。
这种重复源于:当先加一个长度为 的恒等排列 ,再加一个长度为 的恒等排列 ()时,也可以视为先加长度为 的排列再加一个长度为 的循环移位。为了避免重复,我们规定:不允许在恒等排列后面紧接着一个以 开头的循环移位。
在 DP 状态中,我们记录最后一个排列是否是恒等排列及其长度。
第三步:动态规划状态
设:
- 表示从位置 开始到末尾,且 的方案数。
- 表示从位置 开始的总方案数。
- 表示从位置 开始,准备放置以 开头的序列(不检查限制)的方案数。
第四步:预处理
chain(i, j)
$$\text{chain}(i, j) = \begin{cases} 0 & \text{if } a_i \neq j \text{ 是限制}, \\ 1 + \text{chain}(i+1, j+1) & \text{otherwise}. \end{cases} $$chain(i, j)表示从位置 开始,连续放置 且不违反限制的最大长度。
递推:
第五步:计算
suff(i, j)suff(i, j)表示从位置 开始,放置一个以 开头的循环移位排列后,剩余部分的所有方案数之和。若固定 ,我们考虑所有可能的起始位置 ,使得我们可以从 开始连续放置 (共 个连续数)。这等价于 。
此时,位置 之后的部分可以任意填充,方案数为 。因此:
$$\text{suff}(i, j) = \sum_{x = i}^{n} [\text{chain}(x, 1) \ge j-1] \cdot tot(x + j - 1). $$
第六步:转移方程
-
的情况(添加一个恒等排列):
$$dp(i, 1) = \sum_{len \ge 1} \left( tot(i + len) - dp(i + len, len + 1) \right). $$
我们尝试在位置 开始一个长度为 的恒等排列 。
方案数为 ,但要减去 以避免与另一种拆分方式重复。 -
的情况:
$$dp(i, j) = \text{suff}(i+1, j) - \text{suff}(i+1 + \text{chain}(i, j), j). $$
此时 必须等于 。我们从 开始,可以放置任意以 开头的序列,但必须保证从 开始的连续 个位置可用。
这恰好是suff(i+1, j)减去那些因 限制而不可用的部分,即:
第七步:递推顺序
从 向下递推到 :
- 先计算
chain数组。 - 计算
suff(i, j)需要知道 在更大索引的值,因此我们从大到小计算。 - 每步更新 和
suff(i, j)。
第八步:最终答案
即为所有合法方案数。
时间复杂度
- 预处理
chain:。 - 计算
suff和 DP:。 - 总复杂度 ,满足 。
代码实现
#include <bits/stdc++.h> using namespace std; const int MOD = 998244353; const int MAXN = 5005; short chain[MAXN][MAXN]; int ssum[MAXN][MAXN]; int tot[MAXN]; bool ban[MAXN][MAXN]; int c1[MAXN]; void solve() { int n, k; cin >> n >> k; memset(ban, 0, sizeof(ban)); memset(chain, 0, sizeof(chain)); memset(ssum, 0, sizeof(ssum)); memset(tot, 0, sizeof(tot)); for (int i = 0; i < k; i++) { int x, y; cin >> x >> y; ban[x][y] = 1; } tot[n + 1] = 1; // chain for (int i = n; i >= 1; i--) { for (int j = n; j >= 1; j--) { if (ban[i][j]) { chain[i][j] = 0; } else { chain[i][j] = 1 + chain[i + 1][j + 1]; if (chain[i][j] > n - i + 1) chain[i][j] = n - i + 1; if (chain[i][j] > n - j + 1) chain[i][j] = n - j + 1; } } c1[i] = chain[i][1]; } // DP for (int i = n; i >= 1; i--) { // ssum int* ssum_i = ssum[i]; int* ssum_ip1 = ssum[i + 1]; for (int j = 1; j <= n - i + 1; j++) { ssum_i[j] = ssum_ip1[j]; if (c1[i] >= j) { ssum_i[j] = (ssum_i[j] + tot[i + j]) % MOD; } } // j = 0 (恒等排列) int maxlen = c1[i]; for (int len = 1; len <= maxlen; len++) { tot[i] = (tot[i] + tot[i + len]) % MOD; int nxt = i + len; int jj = len + 1; if (nxt <= n + 1 && jj <= n + 1) { int sub = ssum[nxt + 1][jj]; int nxt2 = nxt + 1 + chain[nxt][jj]; if (nxt2 <= n + 1) { sub = (sub - ssum[nxt2][jj] + MOD) % MOD; } tot[i] = (tot[i] - sub + MOD) % MOD; } } // j >= 1 for (int j = 1; j <= n - i + 1; j++) { if (chain[i][j] == 0) continue; int val = ssum[i + 1][j]; int nxt = i + 1 + chain[i][j]; if (nxt <= n + 1) { val = (val - ssum[nxt][j] + MOD) % MOD; } tot[i] = (tot[i] + val) % MOD; } } cout << tot[1] << '\n'; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
总结
本题的关键在于:
- 识别重复计数问题,引入状态记录最后一个排列的类型。
- 预处理
chain数组快速判断连续放置的可行性。 - 使用
suff辅助数组加速转移,将复杂度优化到 。
- 1
信息
- ID
- 6274
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 1
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者