1 条题解
-
0
CF1988F Heartbeat 题解
题意简述
对于一个 的排列,定义:
- 前缀最大值:位置 满足 对所有 成立。
- 后缀最大值:位置 满足 对所有 成立。
- 上升点:位置 满足 。
给定数组 ,一个排列的代价定义为 ,其中 分别为该排列的前缀最大值个数、后缀最大值个数、上升点个数。
对每个 ,求 的所有排列的代价之和,对 取模。
,各数组元素 。
核心思路:按最大值 分割
前缀最大值和后缀最大值无法在同一个 DP 中同时维护,但观察发现:
- 最大值 一定是前缀最大值,也一定是后缀最大值。
- 左侧只有前缀最大值, 右侧只有后缀最大值。
因此分别计算 左侧(前缀部分)和 右侧(后缀部分)的贡献,最后拼接。问题分成三步:
- 对前缀部分,计算 :用 个数组成有 个上升点的所有排列中,( 前缀最大值个数,含 )的总和。
- 对后缀部分,计算 :用 个数组成有 个上升点的所有排列中,( 后缀最大值个数,含 )的总和。
- 通过 与组合数合并出最终答案。
第一部分:前缀 DP
DP 设计
采用“从大到小插入”的构造方式。想象我们在构造 左侧的序列,数字从 向下依次插入。全局最大值 始终位于最右端作为“边界”。
设 表示:已插入 个数(来自 ),含 在内的前缀最大值个数为 ,序列内部的上升对数为 的方案数。
初始状态: 时,只有 自身,。实际实现中直接处理 和 ,并将首个元素插入作为 。
转移
插入当前最小的数时,有三种位置选择:
插入位置 前缀最大值 上升对数 方案乘数 插入到最开头 插入到一个下降对中间() 插入到一个上升对中间或末尾 解释:
- 开头插入:新数成为第一个元素,自动成为前缀最大值;且新数 < 原首元素,产生一个上升对。
- 插入下降对中间:原有 变为 ,新增一个上升对(新数 ),前缀最大值不变。
- 插入上升对中间或末尾:原有 变为 ,消失一个上升对同时新增一个,净变化 ;前缀最大值不变。
其中 为当前序列中下降对的个数, 为上升对个数 (末尾位置)。
转移均为主动转移,复杂度 。
计算 与
完成 后:
的处理有一个巧妙转换:将后缀部分左右翻转,则原后缀最大值变为前缀最大值,上升对变为下降对。因此:
$$g_{i,\ (i-1)-k} = \sum_{j=1}^{i+1} dp_{i,j,k} \cdot b_j $$即后缀部分 个数、 个上升点的情况,对应翻转后 个数、 个上升点的 状态。
第二部分:合并答案
最终的排列结构为:左侧 个数(上升点 ,前缀最大值含 ),最大值 ,右侧 个数(上升点 ,后缀最大值含 )。总长度 。
总上升点数 (左侧非空时,左部分末元素 产生一个额外上升; 到右部分首元素是下降,不计入)。
答案为:
$$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} $$其中 表示从 个数中选出 个放入左侧的方案数。
合并过程
先对每个 合并 维度:
$$iv_{i,v} = \sum_{u} f_{i,u} \cdot c_{u+v+[i\neq0]} $$再对每个 合并 维度:
最后乘组合数贡献到答案:
$$ans_{i+j+1} \mathrel{+}= ij_{i,j} \cdot \binom{i+j}{i} $$每次合并都是 ,总复杂度 。
复杂度
- DP 状态 ,转移 。
- 合并答案 。
- 组合数预处理 。
时 ,常数较小,可通过。
参考代码
#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
- 上传者