abc275_f Erase Subarrays 题解
Erase Subarrays
题意
有一个长度为 \(n\) 的整数序列 \(a\),你可以执行以下操作若干次(可以不执行):
- 选择序列的一个子段,将子段中的每个数变为 \(0\)。
对于 \(1 \leqslant i \leqslant m\),请求出是否存在一种操作方法使得 \((\sum\limits_{1 \leqslant j \leqslant n} a_j) = i\),若存在,输出最小操作次数;否则输出 -1
。
数据范围
- \(1 \leqslant n, m \leqslant 3000\)。
- \(1 \leqslant a_i \leqslant 3000\)。
思路
一眼 dp,我用的是子序列 dp。
说是将序列子段的数变为 \(0\),其实可以看作保留一些数。
- 状态:\(dp_{i, j}\) 表示最后选取数了第 \(i\) 个数且数字和为 \(j\)。
- 数字和最大有 \(9 \times 10^6\),总空间 \(2.7\times 10^9\),MLE?
- NO~,由于 \(m\) 最大只有 \(3000\),所以第二维也只要开到 \(3000\) 即可。
- 转移:两种情况分类讨论。
- 选择上一个数,即 \(dp_{i - 1, j - a_i}\)。
- 不选上一个数,即 \(\min\limits_{1 \leqslant k < i - 1}(dp_{k, j - a_i} + 1)\),因为上一个数不选,所以中间需要一次操作,需 \(+1\)。
- 取两种情况最小值即可。
- 一个小问题,第二中情况需要 \(O(n)\) 查找,TLE,所以要用前缀最小值优化。
- 初始状态:\(dp_{0, 0} = 0\),而对于 \(1 \leqslant i \leqslant m\),\(dp_{0, i}\) 需赋值极大值(推荐使用 \(n\))。
- 目标状态(回答询问):\(\min(\min\limits_{1 \leqslant i \leqslant n}(dp_{i, k}+1), dp_{n, k})\)。
- 假设当前回答的是第 \(k\) 组询问。
- 解释:如果没有选择最后一个数,那么序列的后缀必然需要一次操作,所以分开考虑。
复杂度
- 时间:\(O(n \times m)\)。
- 空间:\(O(n \times m)\)。
Code
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 10;
int n, x, m, dp[N][N], sum[N][N], ans; // sum 维护前缀最小值
int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= m; i++) { // 赋值极大值
sum[0][i] = dp[0][i] = n;
}
for (int i = 1; i <= n; i++) {
cin >> x;
for (int j = 0; j < x; j++) {
dp[i][j] = n; // 数字和小于 x 的没有意义,赋值极大值
}
for (int j = x; j <= m; j++) { // 转移
dp[i][j] = dp[i - 1][j - x];
if (i >= 2) {
dp[i][j] = min(dp[i][j], sum[i - 2][j - x] + 1);
}
}
for (int j = 0; j <= m; j++) {
sum[i][j] = min(sum[i - 1][j], dp[i][j]); // 随时维护前缀最小值
}
}
for (int i = 1; i <= m; i++) { // 处理询问
ans = n;
for (int j = n; j; j--) {
ans = min(ans, dp[j][i] + (j < n));
}
cout << (ans < n ? ans : -1) << '\n';
}
return 0;
}