1 条题解
-
0
题目重述
构造长度为 的序列 ,满足:
- 对于 ,设:
- = 前 个数中 的最小值(不存在则为 )
- = 前 个数中 的最大值(不存在则为 ) 要求
求不同的序列个数,对 取模。
深入理解限制条件
这个条件实际上是在维护一个 动态的有序集合:
- 当我们已经选择了 时,这些数字形成了一个集合
- 对于 ,在这个集合中它有 前驱(比它小的最大数)和 后继(比它大的最小数)
- 必须落在 这个区间内
特殊情况:
- 如果 是当前最小的,则前驱 =
- 如果 是当前最大的,则后继 =
关键性质分析
1. 区间单调性
随着序列的增长, 对应的区间 会 越来越窄,因为集合中的数字越来越多,前驱和后继会越来越接近 。
2. 状态可压缩性
我们不需要记住所有之前选择的数字,只需要知道:
- 当前 的值
- 它的前驱
- 它的后继
因为这三个值已经包含了确定 取值范围的全部信息。
详细状态设计
令:
dp[i][last][L][R] = 方案数其中:
- : 当前要填第 个位置(从 1 开始)
- : 的值
- : 前驱( 或某个具体值)
- : 后继( 或某个具体值)
状态含义:在已经确定了前 个数,且满足:
- 在 中,小于等于 的最大值是
- 在 中,大于等于 的最小值是
的情况下,填写剩余位置的方案数。
状态转移分析
从状态 出发:
枚举 ,需要满足:
- (值域限制)
- (区间限制)
然后根据 与 的关系,更新前驱后继:
情况 1:
A_{i-1} = last A_i = x (< last)在集合 中:
- 的前驱仍然是 (不变)
- 的后继变成 (因为 且是最接近的)
新状态:
情况 2:
A_{i-1} = last A_i = x (> last)在集合 中:
- 的前驱变成 (因为 且是最接近的)
- 的后继仍然是 (不变)
新状态:
情况 3:
A_{i-1} = last A_i = last前驱后继不变(因为集合中已经有 )
新状态:
边界情况处理
初始状态 ()
- 只有 被选择
- (枚举 )
- (前面没有比 小的数)
- (前面没有比 大的数)
终止状态 ()
所有位置填完,返回 1。
离散化实现
由于 可能是 ,需要映射到数组下标:
const int INF = 200; // 表示 +∞ const int NINF = 0; // 表示 -∞ const int MAXV = 201; // 值域上限 int get_idx(int x) { if (x == NINF) return 0; if (x == INF) return MAXV - 1; return x; // 实际值从 1 到 150 }
完整代码(带详细注释)
#include <bits/stdc++.h> using namespace std; const int MOD = 998244353; const int INF = 200; // 表示 +∞ const int NINF = 0; // 表示 -∞ const int MAXV = 201; // 值域大小 (0: -∞, 1..150: 实际值, 200: +∞) const int MAXN = 155; int n; vector<int> r; int dp[MAXN][MAXV][MAXV][MAXV]; // dp[pos][last][L][R] // 将值映射到数组下标 int get_idx(int x) { if (x == NINF) return 0; if (x == INF) return MAXV - 1; return x; } int dfs(int pos, int last, int L, int R) { if (pos == n) return 1; // 所有位置填完 int &res = dp[pos][last][get_idx(L)][get_idx(R)]; if (res != -1) return res; res = 0; int max_val = r[pos]; // 当前位的上限 for (int x = 1; x <= max_val; x++) { // 检查是否在 [L, R] 范围内 if (L != NINF && x < L) continue; if (R != INF && x > R) continue; int new_L = L, new_R = R; if (x < last) { // x 比 last 小,last 成为 x 的后继 new_R = last; } else if (x > last) { // x 比 last 大,last 成为 x 的前驱 new_L = last; } // x == last 时前驱后继不变 res = (res + dfs(pos + 1, x, new_L, new_R)) % MOD; } return res; } int main() { cin >> n; r.resize(n); for (int i = 0; i < n; i++) { cin >> r[i]; } memset(dp, -1, sizeof(dp)); int ans = 0; // 枚举第一个数字 A1 for (int a1 = 1; a1 <= r[0]; a1++) { // 初始状态:pos=1, last=a1, L=-∞, R=+∞ // 注意:这里 pos=1 表示接下来要填第2个位置 ans = (ans + dfs(1, a1, NINF, INF)) % MOD; } cout << ans << endl; return 0; }
复杂度优化讨论
当前复杂度
- 状态数:,
- 转移:
- 总复杂度:$O(n \cdot M^4) \approx 150 \cdot 150^4 \approx 7.6 \times 10^9$(理论上不可行)
实际优化方法
-
值域离散化: 和 只能是之前出现过的值或 ,所以实际状态数远小于
-
状态剪枝:很多 组合是无效的( 或区间为空)
-
迭代DP:改用递推式,按位置顺序计算,避免递归开销
-
滚动数组:只需要保存上一层的状态
算法总结
这道题的核心在于 识别出影响后续决策的关键信息(前驱后继),从而将指数级的状态空间压缩到多项式级别。这是动态规划中 状态设计优化 的典型例子。
通过将复杂的过程限制抽象为简单的区间约束,我们能够用 的状态数来解决原本看似需要 的问题。
- 1
信息
- ID
- 3677
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者