CF1858D Trees and Segments

发布时间 2023-08-26 15:53:11作者: yxu0528

一道考查预处理技巧的 dp。

观察式子 \(a\times L_0+L_1\),一个显然的想法是“定一求一”,即预处理求出对于每个 \(L_1\) 最大的 \(L_0\),然后对于每个 \(a\),枚举 \(L_1\),统计最大的 \(a\times L_0+L_1\)。这样,我们将问题转化为了:已知 \(L_1=len\),求出 \(dp_{len}=L_{0max}\)

dp 数组的状态是“长度”,这似乎并不好维护,我们不得不把它落实到一个个子段上去:对于每个 \(len\in [1,n]\),枚举长度为 \(len\) 的子段,并用 \(x\) 次操作把它改为一个全 \(1\) 段,这就是 \(L_1\) 对应的子段。然后,在它的左边和右边,去寻找 \(L_0\)(利用好剩下的 \(k-x\) 次操作使其尽可能大)并更新 \(dp_{len}\)

问题是,仅仅枚举子段的复杂度就到了 \(O(n^2)\),意味着我们需要预处理出前后缀 \(1\sim i\) 中修改至少 \(j\) 次能得到的最大 \(L_0\),分别设为 \(pre_{i,j},suf_{i,j}\)。通用的方法是“两步走”:以前缀为例,先求出以 \(i\) 结尾的,修改了恰好 \(j\) 次的最大 \(L_0\);再应用类似前缀 \(\max\) 的方法得到答案。

启示:

  1. 动态规划设计状态应该尽可能简洁,既能清晰的表示当前情形,又不包含冗余信息;
  2. 研究复杂的问题可以层层深入,把大问题分解为一个个小问题,各个击破。

下面是 AC 代码:

void solve() {
    int n, k;
    string s;
    cin >> n >> k >> s;
    // 以 i 结尾,修改 j 次的最大 0 段长度
    vector<vector<int>> pre(n, vector<int>(k + 1, 0));
    pre[0][0] = (s[0] == '0');
    if (k > 0) {
        pre[0][1] = (s[0] == '1');
    }
    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= min(i + 1, k); j++) {
            if (s[i] == '0') {
                pre[i][j] = pre[i - 1][j] + 1; // s[i] = 0, 不修改
            } else if (j > 0) {
                pre[i][j] = pre[i - 1][j - 1] + 1; // s[i] = 1, 修改
            } else {
                pre[i][j] = 0; // s[i] = 1, 不修改
            }
        }
    }
    // 将 pre 改为在 i 及 i 之前结尾,最多修改 j 次的最大 0 段长度
    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= k; j++) {
            pre[i][j] = max(pre[i][j], pre[i - 1][j]);
            if (j > 0) {
                pre[i][j] = max(pre[i][j], pre[i][j - 1]);
            }
        }
    }
    // 以 i 开头,修改 j 次的最大 0 段长度
    vector<vector<int>> suf(n, vector<int>(k + 1, 0));
    suf[n - 1][0] = (s[n - 1] == '0');
    if (k > 0) {
        suf[n - 1][1] = (s[n - 1] == '1');
    }
    for (int i = n - 2; i >= 0; i--) {
        for (int j = 0; j <= min(n - i, k); j++) {
            if (s[i] == '0') {
                suf[i][j] = suf[i + 1][j] + 1; // s[i] = 0, 不修改
            } else if (j > 0) {
                suf[i][j] = suf[i + 1][j - 1] + 1; // s[i] = 1, 修改
            } else {
                suf[i][j] = 0; // s[i] = 1, 不修改
            }
        }
    }
    // 将 suf 改为在 i 及 i 之后开头,最多修改 j 次的最大 0 段长度
    for (int i = n - 2; i >= 0; i--) {
        for (int j = 0; j <= k; j++) {
            suf[i][j] = max(suf[i][j], suf[i + 1][j]);
            if (j > 0) {
                suf[i][j] = max(suf[i][j], suf[i][j - 1]);
            }
        }
    }
    vector<int> sum(n, 0);
    sum[0] = s[0] - '0';
    for (int i = 1; i < n; i++) {
        sum[i] = sum[i - 1] + s[i] - '0';
    }
    // dp[len] 为 L1 = len 时的最大 L0
    vector<int> dp(n + 1, -1);
    dp[0] = max(pre[n - 1][k], suf[0][k]);
    for (int l = 0; l < n; l++) {
        for (int r = l; r < n; r++) {
            int len = r - l + 1;
            // [l, r] 中 0 的个数
            int x = len - (sum[r] - (l > 0 ? sum[l - 1] : 0));
            if (x > k) {
                continue;
            }
            if (l == 0 && r == n - 1) {
                dp[len] = max(dp[len], 0);
                continue;
            }
            if (l > 0) {
                dp[len] = max(dp[len], pre[l - 1][k - x]);
            }
            if (r < n - 1) {
                dp[len] = max(dp[len], suf[r + 1][k - x]);
            }
        }
    }
    for (int a = 1; a <= n; a++) {
        int ans = 0;
        for (int len = 0; len <= n; len++) {
            if (dp[len] == -1) {
                continue;
            }
            ans = max(ans, a * dp[len] + len);
        }
        cout << ans << " ";
    }
    cout << endl;
}

THE END