abc275_f Erase Subarrays 题解

发布时间 2023-06-02 15:26:57作者: luogu_wsy0704

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;
}