在编程竞赛中,计数类动态规划常结合“状态转移+区间扩展”的思路,解决序列构造的方案数问题。本文以“事件序列的跌宕起伏排列”为例,拆解这类问题的分析逻辑、实现步骤与代码细节。
给定 n 个事件(每个事件有类型 a_i),要求构造序列 B,满足:序列的每个前缀的最后一个元素,都是该前缀的最大值或最小值。统计本质不同的序列 B 的数量(对 998244353 取模)。
要满足“每个前缀的最后一个元素是当前前缀的最值”,序列的构造过程必然是逐步扩展最值区间:
基于此,我们可以用区间动态规划来建模:
s[0..m-1],m 是去重后的元素个数);cnt[i](即原数组中 s[i] 出现了多少次);dp[i][j]:表示当前序列的最小值是 s[i]、最大值是 s[j] 时,合法序列的数量。首先对原数组排序,再去重得到有序的唯一元素列表,同时统计每个元素的出现次数:
// 排序原数组
qsort(a, n, sizeof(int), cmp);
// 去重并统计每个元素的出现次数
int m = unique(a, n); // 去重后的元素个数
int *cnt = (int *)malloc(m * sizeof(int));
cnt[0] = 1;
int idx = 0;
for (int i = 1; i < n; i++) {
if (a[i] == a[idx]) cnt[idx]++;
else {
idx++;
cnt[idx] = 1;
}
}当序列只包含一个元素时(即区间 [i,i]),方案数等于该元素的出现次数(每个相同元素都是一种选择):
long long **dp = (long long **)malloc(m * sizeof(long long *));
for (int i = 0; i < m; i++) {
dp[i] = (long long *)calloc(m, sizeof(long long));
dp[i][i] = cnt[i]; // 单个元素的方案数是其出现次数
}我们按区间长度从小到大处理,确保转移时子问题已求解:
[i,j],它只能由两种方式扩展而来: [i,j-1](最小值 s[i],最大值 s[j-1])扩展到 [i,j],新增的 s[j] 是新最大值,方案数为 dp[i][j-1] * cnt[j](cnt[j] 是 s[j] 的可选数量);[i+1,j](最小值 s[i+1],最大值 s[j])扩展到 [i,j],新增的 s[i] 是新最小值,方案数为 dp[i+1][j] * cnt[i](cnt[i] 是 s[i] 的可选数量)。转移代码如下:
for (int len = 2; len <= m; len++) { // 区间长度从2到m
for (int i = 0; i + len - 1 < m; i++) {
int j = i + len - 1;
// 扩展最大值到j
dp[i][j] = (dp[i][j] + dp[i][j-1] * cnt[j]) % MOD;
// 扩展最小值到i
dp[i][j] = (dp[i][j] + dp[i+1][j] * cnt[i]) % MOD;
}
}最终答案是覆盖所有唯一元素的区间 [0, m-1] 对应的方案数:
printf("%lld\n", dp[0][m-1] % MOD);#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MOD 998244353
// 排序比较函数
int cmp(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
// 去重函数,返回去重后的元素个数
int unique(int *arr, int n) {
if (n == 0) return 0;
int idx = 1;
for (int i = 1; i < n; i++) {
if (arr[i] != arr[idx-1]) {
arr[idx++] = arr[i];
}
}
return idx;
}
int main() {
int n;
scanf("%d", &n);
int *a = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
// 步骤1:排序、去重、统计出现次数
qsort(a, n, sizeof(int), cmp);
int m = unique(a, n);
int *cnt = (int *)malloc(m * sizeof(int));
cnt[0] = 1;
int idx = 0;
for (int i = 1; i < n; i++) {
if (a[i] == a[idx]) cnt[idx]++;
else {
idx++;
cnt[idx] = 1;
}
}
// 步骤2:初始化DP数组
long long **dp = (long long **)malloc(m * sizeof(long long *));
for (int i = 0; i < m; i++) {
dp[i] = (long long *)calloc(m, sizeof(long long));
dp[i][i] = cnt[i];
}
// 步骤3:区间DP转移
for (int len = 2; len <= m; len++) {
for (int i = 0; i + len - 1 < m; i++) {
int j = i + len - 1;
// 扩展最大值
dp[i][j] = (dp[i][j] + dp[i][j-1] * cnt[j]) % MOD;
// 扩展最小值
dp[i][j] = (dp[i][j] + dp[i+1][j] * cnt[i]) % MOD;
}
}
// 步骤4:输出结果
printf("%lld\n", dp[0][m-1] % MOD);
// 释放内存
free(a);
free(cnt);
for (int i = 0; i < m; i++) {
free(dp[i]);
}
free(dp);
return 0;
}这类“前缀最值约束的序列计数”问题,核心是将序列构造转化为区间扩展,通过区间DP记录当前最值区间的方案数,再结合元素的出现次数进行转移。该思路不仅能解决本题,还可推广到类似“逐步扩展最值”的计数问题中。
要不要我帮你整理一份区间DP常见题型的模板总结,覆盖这类问题的核心转移逻辑?