1 条题解
-
0
1. 题目理解
我们有一个数组,每次操作可以选定一个不包含颜色 的子段,然后将整个子段染成颜色 。目标是让整个数组变成单一颜色,求最小操作次数。
数据规模:,,。允许 的 DP。
2. 关键观察
一次操作的效果是将一个子段统一染成某颜色,且该颜色之前不在子段中出现。这暗示我们可以将原问题划分为子区间上的子问题。
定义两种状态:
- :将区间 全部染成颜色 所需的最少操作次数(注意:这里的 是经过“压缩”后的下标,即去掉了区间两端已经是颜色 的部分,因为两端已是 的话无需操作)。
- :将区间 变成不包含颜色 的任意状态(即最终颜色不是 )所需的最少操作次数。
标程中通过预处理
nxtx、prvx等数组来快速跳过已经是某颜色的元素,从而将区间压缩到真正需要处理的范围。3. 预处理数组
nxtx[i][c]:从下标 开始(包括 )向右第一个颜色等于 的位置。prvx[i][c]:从下标 开始向左第一个颜色等于 的位置。nxtnx[i][c]:从 开始向右第一个颜色不等于 的位置。prvnx[i][c]:从 开始向左第一个颜色不等于 的位置。
这些预处理允许我们在 DP 时直接跳过区间两端已经是目标颜色(或不是目标颜色)的元素,缩小状态空间。
4. DP 转移
:将区间染成颜色
首先用预处理数组将 跳到第一个不为 的位置, 跳到最后一个不为 的位置。若 ,说明区间已经全是 ,返回 。
转移有两种选择:
- 直接染色:先用若干操作将区间变成不包含 的状态(即 ),然后一次操作将整个区间染成 ,总次数 。
- 分治染色:将区间分成两段 和 ,分别染成 ,取最小次数之和。
:将区间变成不含 的状态
同样先压缩 到第一个等于 的位置, 到最后一个等于 的位置。若 ,说明区间内没有 ,返回 。
转移有两种:
- 分治:将区间分成两部分分别处理,求和。
- 先染成其他颜色 :选择一种其他颜色 ,将整个区间染成 (即 ),这样区间自然不再含 。
最终答案是对所有颜色 ,取 的最小值。
5. 复杂度分析
- 状态数:,每个状态转移需要 或 时间,总复杂度 或 。
- 由于 ,最大计算量约为 级别,在 秒内完全可行。
- 使用记忆化搜索(标程中的递归写法)实现 DP。
6. 代码(附注释)
#include <bits/stdc++.h> using namespace std; const int N = 111; int n, k; int a[N]; int nxtx[N][N], prvx[N][N]; // 下一个/上一个颜色等于 x 的位置 int nxtnx[N][N], prvnx[N][N]; // 下一个/上一个颜色不等于 x 的位置 int dp1[N][N][N], dp2[N][N][N]; int calc2(int l, int r, int x); int calc1(int l, int r, int x) { // 压缩区间:跳过两端已经是颜色 x 的元素 l = nxtnx[l][x], r = prvnx[r][x]; if (l > r) return 0; if (dp1[l][r][x] != -1) return dp1[l][r][x]; // 选项1:先变成不含 x 的状态,再一次染色 dp1[l][r][x] = calc2(l, r, x) + 1; // 选项2:分两段分别染色 for (int i = l; i < r; ++i) dp1[l][r][x] = min(dp1[l][r][x], calc1(l, i, x) + calc1(i + 1, r, x)); return dp1[l][r][x]; } int calc2(int l, int r, int x) { // 压缩区间:跳过两端不等于颜色 x 的元素(即定位到含有 x 的部分) l = nxtx[l][x], r = prvx[r][x]; if (l > r) return 0; if (dp2[l][r][x] != -1) return dp2[l][r][x]; dp2[l][r][x] = n; // 初始化为较大值 // 选项1:分两段分别处理 for (int i = l; i < r; ++i) dp2[l][r][x] = min(dp2[l][r][x], calc2(l, i, x) + calc2(i + 1, r, x)); // 选项2:直接染成另一种颜色 y for (int y = 0; y < k; ++y) if (x != y) dp2[l][r][x] = min(dp2[l][r][x], calc1(l, r, y)); return dp2[l][r][x]; } void solve() { cin >> n >> k; for (int i = 0; i < n; ++i) cin >> a[i], --a[i]; // 转为0-based颜色 // 初始化前驱数组 for (int x = 0; x < k; ++x) prvx[0][x] = prvnx[0][x] = -1; for (int i = 0; i < n; ++i) { prvx[i][a[i]] = i; for (int x = 0; x < k; ++x) prvx[i + 1][x] = prvx[i][x]; for (int x = 0; x < k; ++x) if (x != a[i]) prvnx[i][x] = i; for (int x = 0; x < k; ++x) prvnx[i + 1][x] = prvnx[i][x]; } // 初始化后继数组 for (int x = 0; x < k; ++x) nxtx[n][x] = nxtnx[n][x] = n; for (int i = n - 1; i >= 0; --i) { for (int x = 0; x < k; ++x) nxtx[i][x] = nxtx[i + 1][x]; nxtx[i][a[i]] = i; for (int x = 0; x < k; ++x) nxtnx[i][x] = nxtnx[i + 1][x]; for (int x = 0; x < k; ++x) if (x != a[i]) nxtnx[i][x] = i; } memset(dp1, -1, sizeof(dp1)); memset(dp2, -1, sizeof(dp2)); int ans = n; for (int x = 0; x < k; ++x) ans = min(ans, calc1(0, n - 1, x)); cout << ans << '\n'; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) solve(); return 0; }7. 总结
本题是一道典型的区间 DP 题目,难点在于设计合适的状态和转移。通过引入 (染成特定颜色)和 (避免特定颜色)两个互补状态,并利用预处理数组压缩区间边界,使得 DP 能够高效进行。这种方法同样适用于其他具有“染色”或“覆盖”性质的区间操作问题。
- 1
信息
- ID
- 6523
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者