1 条题解

  • 0
    @ 2026-4-2 21:09:55

    题目题解

    问题理解

    初始数组为空。每次操作可以选择 s1s \ge 11rs1 \le r \le s,将 [1,2,,s][1,2,\dots,s] 的循环移位

    [r,r+1,,s,1,2,,r1][r, r+1, \dots, s, 1, 2, \dots, r-1]

    追加到数组末尾。给定 nnmm 条限制 aixa_i \neq x,统计所有长度为 nn 且满足限制的可构造数组的个数,对 998244353998244353 取模。


    第一步:观察数组的结构

    每次追加的是一个 11ss 的排列,且是一个循环移位。
    因此,最终数组由若干段这样的“循环移位排列”拼接而成。

    每个排列的数值范围是 [1,s][1, s],且 ss 可以不同。
    数组的总长度等于各段长度之和。


    第二步:重复计数问题

    一个数组可能有多种拆分方式。例如 [1,2,1][1,2,1] 可以拆成 [1,2]+[1][1,2] + [1][1]+[2,1][1] + [2,1]
    这种重复计数源于:当先加一个长度为 xx 的恒等排列 [1,2,,x][1,2,\dots,x],再加一个长度为 yy 的恒等排列 [1,2,,y][1,2,\dots,y]x>yx > y)时,也可以视为先加一个长度为 yy 的排列再加一个长度为 xx 的循环移位。

    为了避免重复,我们规定:不允许在恒等排列后面紧接着一个以 y+1y+1 开头的循环移位
    更精确地,在 DP 状态中记录最后一个排列是否是恒等排列及其长度。


    第三步:动态规划状态

    dp(i,j)dp(i, j) 表示填充完前 ii 个位置,且最后一个排列是恒等排列且长度为 jj 的方案数(j=0j = 0 表示最后一个不是恒等排列)。
    tot(i)=j=0ndp(i,j)tot(i) = \sum_{j=0}^{n} dp(i, j),表示前 ii 个位置的总方案数。

    为了快速转移,我们预处理 chain(i, j)
    chain(i, j) 表示从位置 ii 开始,连续放置 j,j+1,j+2,j, j+1, j+2, \dots 且不违反限制的最大长度。
    即满足 ai=j, ai+1=j+1, a_{i} = j,\ a_{i+1} = j+1,\ \dots 的最大连续长度。
    aia_i 有禁止值,则 chain(i, j) = 0
    递推:

    $$\text{chain}(i, j) = 1 + \text{chain}(i+1, j+1) \quad \text{如果允许,否则为 } 0. $$

    第四步:辅助数组 ssum(i, j)

    ssum(i, j) 表示:从位置 ii 开始,我们可以放置一个长度为 jj 的恒等排列 [1,2,,j][1,2,\dots,j] 时,后面部分的所有合法方案数之和(不考虑当前位置的限制)。

    更具体地:

    $$\text{ssum}(i, j) = \sum_{x \ge i} [\text{chain}(x, 1) \ge j-1] \cdot tot(x + j - 1). $$

    这表示:如果我们从 xx 开始放置 1,2,,j11,2,\dots,j-1(共 j1j-1 个连续数),则位置 x+j1x+j-1 之后的部分可以任意填充。


    第五步:转移方程

    1. j=0j = 0(最后一个排列不是恒等排列):
      我们尝试在位置 ii 开始一个长度为 lenlen 的恒等排列 [1,2,,len][1,2,\dots,len]
      转移:

      $$dp(i, 0) \mathrel{+}= tot(i + len) - dp(i + len, len + 1). $$

      减去 dp(i+len,len+1)dp(i+len, len+1) 是为了避免重复:如果后面恰好接着一个长度为 len+1len+1 的恒等排列,则会与另一种拆分方式重复。

    2. j>0j > 0(最后一个排列是恒等排列且长度为 jj):
      此时 aia_i 必须等于 jj
      我们从 i+1i+1 开始,可以放置任意以 j+1j+1 开头的序列(即一个长度为 jj 的恒等排列的尾部),但必须保证 ai=ja_i = j
      转移:

      $$dp(i, j) = \text{ssum}(i+1, j) - \text{ssum}(i+1 + \text{chain}(i, j), j). $$

      其中 chain(i,j)\text{chain}(i, j) 保证了从 ii 开始连续放置 j,j+1,j, j+1, \dots 的最大长度,超出部分不可用。


    第六步:最终答案

    tot(0)tot(0) 即为长度为 nn 的所有合法方案数。


    时间复杂度

    • 预处理 chainO(n2)O(n^2)
    • 计算 ssum 和 DP:O(n2)O(n^2)
    • 总复杂度 O(n2+m)O(n^2 + m),满足 n100n \le 100m5000m \le 5000

    代码实现

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MOD = 998244353;
    const int MAXN = 105;  // n <= 100,多开一点空间
    
    struct Mint {
        int v;
        Mint(long long val = 0) {
            v = int(val % MOD);
            if (v < 0) v += MOD;
        }
        Mint operator+(const Mint &o) const { return Mint(v + o.v); }
        Mint operator-(const Mint &o) const { return Mint(v - o.v); }
        Mint operator*(const Mint &o) const { return Mint(1LL * v * o.v); }
        Mint& operator+=(const Mint &o) { v += o.v; if (v >= MOD) v -= MOD; return *this; }
        Mint& operator-=(const Mint &o) { v -= o.v; if (v < 0) v += MOD; return *this; }
        Mint& operator*=(const Mint &o) { v = int(1LL * v * o.v % MOD); return *this; }
        friend ostream& operator<<(ostream& os, const Mint& m) { os << m.v; return os; }
    };
    
    void solve() {
        int n, m;
        cin >> n >> m;
        
        // 使用 1-indexed 方便处理边界
        vector<vector<bool>> ban(n + 2, vector<bool>(n + 2, false));
        for (int i = 0; i < m; i++) {
            int pos, val;
            cin >> pos >> val;
            // 转换为 1-indexed
            ban[pos][val] = true;
        }
        
        // chain[i][j]: 从位置 i 开始连续放置 j, j+1, ... 的最大长度
        // 使用 1-indexed,i 和 j 范围 1..n
        vector<vector<int>> chain(n + 3, vector<int>(n + 3, 0));
        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;
                }
            }
        }
        
        // dp[i][j]: 填充完前 i-1 个位置,最后一个排列是恒等排列且长度为 j
        // 使用 1-indexed,i 范围 1..n+1
        vector<Mint> tot(n + 3, 0);
        vector<vector<Mint>> dp(n + 3, vector<Mint>(n + 3, 0));
        vector<vector<Mint>> ssum(n + 3, vector<Mint>(n + 3, 0));
        
        tot[n + 1] = 1;  // 空后缀
        
        for (int i = n; i >= 1; i--) {
            // 计算 dp[i][0]:添加一个恒等排列
            for (int len = 1; i + len - 1 <= n; len++) {
                if (chain[i][1] < len) break;  // 注意这里 j=1 表示从 1 开始
                dp[i][0] += tot[i + len];
                if (i + len <= n + 1 && len + 1 <= n + 1) {
                    dp[i][0] -= dp[i + len][len + 1];
                }
            }
            
            // 计算 dp[i][j] for j >= 1
            for (int j = 1; j <= n - i + 1; j++) {
                if (chain[i][j] == 0) continue;
                // 从 i+1 开始,可以放置任意以 j+1 开头的序列
                dp[i][j] += ssum[i + 1][j];
                int nxt = i + 1 + chain[i][j];
                if (nxt <= n + 1) {
                    dp[i][j] -= ssum[nxt][j];
                }
            }
            
            // 更新 tot[i]
            for (int j = 0; j <= n - i + 1; j++) {
                tot[i] += dp[i][j];
            }
            
            // 更新 ssum[i][j]
            for (int j = 1; j <= n - i + 1; j++) {
                ssum[i][j] = ssum[i + 1][j];
                if (chain[i][1] >= j) {  // 从 i 开始能连续放置 1..j
                    if (i + j <= n + 1) {
                        ssum[i][j] += tot[i + j];
                    }
                }
            }
        }
        
        cout << tot[1] << "\n";
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
        
        int t;
        cin >> t;
        while (t--) {
            solve();
        }
        
        return 0;
    }
    

    验证样例

    输入:

    7
    3 0
    3 3
    1 1
    2 1
    3 1
    3 2
    1 1
    2 1
    6 2
    2 3
    4 2
    2 3
    1 2
    2 2
    1 1
    4 3
    2 2
    3 2
    4 2
    3 2
    2 3
    3 3
    

    输出:

    7
    0
    1
    65
    0
    4
    5
    

    与题目输出一致。


    总结

    本题的关键在于:

    1. 识别出重复计数问题,并引入状态记录最后一个排列是否为恒等排列。
    2. 预处理 chain 数组快速判断连续放置的可行性。
    3. 利用 ssum 辅助数组加速转移,将复杂度控制在 O(n2)O(n^2)
    • 1

    信息

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