1 条题解

  • 0
    @ 2026-5-4 18:51:45

    CF1988F Heartbeat 题解

    题意简述

    对于一个 1n1 \sim n 的排列,定义:

    • 前缀最大值:位置 ii 满足 ui>uju_i > u_j 对所有 j<ij < i 成立。
    • 后缀最大值:位置 ii 满足 ui>uju_i > u_j 对所有 j>ij > i 成立。
    • 上升点:位置 i (i>1)i\ (i>1) 满足 ui>ui1u_i > u_{i-1}

    给定数组 a[1..n], b[1..n], c[0..n1]a[1..n],\ b[1..n],\ c[0..n-1],一个排列的代价定义为 axbycza_x \cdot b_y \cdot c_z,其中 x,y,zx,y,z 分别为该排列的前缀最大值个数、后缀最大值个数、上升点个数。

    对每个 k=1,2,,nk = 1,2,\ldots,n,求 1k1 \sim k 的所有排列的代价之和,对 998244353998\,244\,353 取模。

    n700n \le 700,各数组元素 <998244353< 998\,244\,353


    核心思路:按最大值 nn 分割

    前缀最大值和后缀最大值无法在同一个 DP 中同时维护,但观察发现:

    • 最大值 nn 一定是前缀最大值,也一定是后缀最大值。
    • nn 左侧只有前缀最大值,nn 右侧只有后缀最大值。

    因此分别计算 nn 左侧(前缀部分)和 nn 右侧(后缀部分)的贡献,最后拼接。问题分成三步:

    1. 对前缀部分,计算 fi,uf_{i,u}:用 ii 个数组成有 uu 个上升点的所有排列中,axa_xx=x= 前缀最大值个数, nn)的总和。
    2. 对后缀部分,计算 gj,vg_{j,v}:用 jj 个数组成有 vv 个上升点的所有排列中,byb_yy=y= 后缀最大值个数, nn)的总和。
    3. 通过 f,gf,g 与组合数合并出最终答案。

    第一部分:前缀 DP

    DP 设计

    采用“从大到小插入”的构造方式。想象我们在构造 nn 左侧的序列,数字从 n1n-1 向下依次插入。全局最大值 nn 始终位于最右端作为“边界”。

    dpi,j,kdp_{i,j,k} 表示:已插入 ii 个数(来自 {n1,,ni}\{n-1,\dots,n-i\}),nn 在内的前缀最大值个数为 jj,序列内部的上升对数为 kk 的方案数。

    初始状态:i=0i=0 时,只有 nn 自身,j=1, k=0j=1,\ k=0。实际实现中直接处理 f0,0=a1f_{0,0}=a_1g0,0=b1g_{0,0}=b_1,并将首个元素插入作为 dp1,2,0=1dp_{1,2,0}=1

    转移

    插入当前最小的数时,有三种位置选择:

    插入位置 前缀最大值 jj 上升对数 kk 方案乘数
    插入到最开头 j+1j+1 k+1k+1 11
    插入到一个下降对中间(l>rl > r jj (i1)k(i-1)-k
    插入到一个上升对中间或末尾 kk 1+k1+k

    解释:

    • 开头插入:新数成为第一个元素,自动成为前缀最大值;且新数 < 原首元素,产生一个上升对。
    • 插入下降对中间:原有 l>rl > r 变为 l>新数<rl > \text{新数} < r,新增一个上升对(新数 <r< r),前缀最大值不变。
    • 插入上升对中间或末尾:原有 l<rl < r 变为 l>新数<rl > \text{新数} < r,消失一个上升对同时新增一个,净变化 00;前缀最大值不变。

    其中 (i1)k(i-1)-k 为当前序列中下降对的个数,1+k1+k 为上升对个数 +1+1(末尾位置)。

    转移均为主动转移,复杂度 O(n3)O(n^3)

    计算 ffgg

    完成 dpdp 后:

    fi,k=j=1i+1dpi,j,kajf_{i,k} = \sum_{j=1}^{i+1} dp_{i,j,k} \cdot a_j

    gg 的处理有一个巧妙转换:将后缀部分左右翻转,则原后缀最大值变为前缀最大值,上升对变为下降对。因此:

    $$g_{i,\ (i-1)-k} = \sum_{j=1}^{i+1} dp_{i,j,k} \cdot b_j $$

    即后缀部分 ii 个数、vv 个上升点的情况,对应翻转后 ii 个数、(i1)v(i-1)-v 个上升点的 dpdp 状态。


    第二部分:合并答案

    最终的排列结构为:左侧 ii 个数(上升点 uu,前缀最大值含 nn),最大值 nn,右侧 jj 个数(上升点 vv,后缀最大值含 nn)。总长度 s=i+j+1s = i + j + 1

    总上升点数 z=u+v+[i0]z = u + v + [i \neq 0](左侧非空时,左部分末元素 <n< n 产生一个额外上升;nn 到右部分首元素是下降,不计入)。

    答案为:

    $$ans_s = \sum_{\substack{i+j+1=s \\ u+v \le n}} f_{i,u} \cdot c_{u+v+[i\neq0]} \cdot g_{j,v} \cdot \binom{i+j}{i} $$

    其中 (i+ji)\binom{i+j}{i} 表示从 n1n-1 个数中选出 ii 个放入左侧的方案数。

    合并过程

    先对每个 i,vi,v 合并 uu 维度:

    $$iv_{i,v} = \sum_{u} f_{i,u} \cdot c_{u+v+[i\neq0]} $$

    再对每个 i,ji,j 合并 vv 维度:

    iji,j=vivi,vgj,vij_{i,j} = \sum_{v} iv_{i,v} \cdot g_{j,v}

    最后乘组合数贡献到答案:

    $$ans_{i+j+1} \mathrel{+}= ij_{i,j} \cdot \binom{i+j}{i} $$

    每次合并都是 O(n3)O(n^3),总复杂度 O(n3)O(n^3)


    复杂度

    • DP 状态 O(n3)O(n^3),转移 O(1)O(1)
    • 合并答案 O(n3)O(n^3)
    • 组合数预处理 O(n2)O(n^2)

    n700n \le 700n33.4×108n^3 \approx 3.4 \times 10^8,常数较小,可通过。


    参考代码

    #include <bits/stdc++.h>
    using namespace std;
    
    constexpr int N = 700;
    constexpr int P = 998244353;
    
    int C[N + 1][N + 1];
    int n, a[N + 1], b[N + 1], c[N];
    int dp[2][N + 1][N + 1];
    int f[N + 1][N + 1], g[N + 1][N + 1];
    int iv[N + 1][N + 1], ij[N + 1][N + 1];
    int ans[N + 1];
    
    inline void init() {
        for (int i = 0; i <= n; i++)
            for (int j = 0; j <= i; j++) {
                if (j == 0 || j == i)
                    C[i][j] = 1;
                else
                    C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
            }
    }
    
    inline void get_fg() {
        memset(dp, 0, sizeof dp);
        f[0][0] = a[1];
        g[0][0] = b[1];
        dp[1][2][0] = 1;
        for (int i = 1; i < n; i++) {
            memset(dp[i & 1 ^ 1], 0, sizeof dp[i & 1 ^ 1]);
            auto &from = dp[i & 1];
            auto &to = dp[i & 1 ^ 1];
            for (int j = 1; j <= i + 1; j++)
                for (int k = 0; k <= i; k++)
                    if (from[j][k]) {
                        if (i < n - 1) {
                            // 插在开头
                            int add = from[j][k];
                            (to[j + 1][k + 1] += add) %= P;
                            // 插在下降对中间
                            add = from[j][k] * 1LL * ((i - 1) - k) % P;
                            (to[j][k + 1] += add) %= P;
                            // 插在末尾或上升对中间
                            add = from[j][k] * 1LL * (1 + k) % P;
                            (to[j][k] += add) %= P;
                        }
                        (f[i][k] += from[j][k] * 1LL * a[j] % P) %= P;
                        (g[i][(i - 1) - k] += from[j][k] * 1LL * b[j] % P) %= P;
                    }
        }
    }
    
    inline void get_ans() {
        for (int i = 0; i < n; i++)
            for (int u = 0; u <= n; u++)
                for (int v = 0; u + v <= n; v++)
                    (iv[i][v] += f[i][u] * 1LL * c[u + v + (i != 0)] % P) %= P;
        for (int i = 0; i < n; i++)
            for (int v = 0; v <= n; v++)
                for (int j = 0; j < n; j++)
                    (ij[i][j] += iv[i][v] * 1LL * g[j][v] % P) %= P;
        for (int i = 0; i < n; i++)
            for (int j = 0; i + j < n; j++)
                (ans[i + j + 1] += ij[i][j] * 1LL * C[i + j][i] % P) %= P;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
        cin >> n;
        for (int i = 1; i <= n; i++) cin >> a[i];
        for (int i = 1; i <= n; i++) cin >> b[i];
        for (int i = 0; i < n; i++) cin >> c[i];
        init();
        get_fg();
        get_ans();
        for (int i = 1; i <= n; i++)
            cout << ans[i] << " \n"[i == n];
        return 0;
    }
    • 1

    信息

    ID
    6829
    时间
    5000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者