36 ACwing 299 Cut the Sequnce 题解

Cut the Sequnce

题面

给定一个长度为 N 的序列 A,要求把该序列分成若干段,在满足“每段中所有数的和”不超过 M 的前提下,让“每段中所有数的最大值”之和最小。

试计算这个最小值。

\(0 \le N \le 10^5\)

\(0 \le M \le 10^{11}\)

\(0 \le A_i \le 10^6\)

题解

这道题应该可以算作一个难题了

dp状态不难设计,设 \(f(i)\) 表示将前 \(i\) 个分成若干段的最小代价,初始 \(f(0) = 0\) ,目标 \(f(n)\)

转移:

\[f(i) = \min_{0 \le j \le i - 1,\sum_{j + 1 \le k \le i} a_k \le M} \{ f(j) + \max_{j + 1 \le k \le i} \{ a_k \} \} \]

这个方程直接去转移的话是 \(O(n^3)\) 的,可以倒序枚举 \(j\) ,用一个变量来记录 \(max(a_k)\) ,可以将时间复杂度优化到 \(O(n^2)\) ,但是对于 \(10^5\) 的数据还是不够快

dp转移的指导思想就是及时排除不可能的决策

所以我们要想一想我们的转移中是否存在不可能成为最优解的决策?

可以画图来帮助理解

image-20250910092758833

假设现在要求 \(f(i)\)\(j\) 表示满足 \(\sum_{j \le k \le i} a_k \le M\) 的下标,\(a_{k_1},a_{k_2}...\) 分别代表 \(j \sim i\) 的最大值(相同值选最靠后一个),\(k_1 + 1 \sim i\) 的最大值……

那么我们可以在 \(j \sim i\) 之间任意选一个点作为最后一段的起点

假如我们选 \(j \sim k_1 - 1\) 中的一个点作为最后一段的起点,那么选 \(j\) 一定是最优的

因为此时对于最后一段来说,贡献都是 \(a_{k_1}\) 所以我们选 \(f\) 最小的作为起点即可

有结论 \(f(j) \le f(j + 1)\) ,感性理解很好理解,理性证明一下

\(f(i), f(j)\) 满足 \(i < j\)

image-20250910101150804

我们找到 \(1 \sim j\) 中的任意一种分段方案,然后将其复制到 \(1 \sim i\)

因为 \(1 \sim j\) 的分段方案包含所有 \(1 \sim i\) 的分段方案,并且 \(1 \sim j\) 的段数比 \(1 \sim i\) 只多不少,所以 \(f(i) \le f(j)\)

那么从 \(k_1 \sim k_2 - 1\) 之间选择一个点作为最后一段的起点也是同理,直接选择 \(k_1\) 即可

所以我们现在就要维护一个单调递减的序列 \(k_1,k_2...\)

然后从这个序列中选出最小值 \(f(k_x) + a_{k_{x + 1}}\) 作为答案

因为要维护单点递减的序列,所以我们想到用单调队列来维护

但是还要维护一个有序的集合,支持插入和删除,所以用 multiset 维护集合即可

最终的时间复杂度为 \(O(n \log n)\)

code

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <set>

using namespace std;

typedef long long ll;

const int N = 1e5 + 10;

int n;
int a[N], q[N];
ll f[N], m;
multiset <ll> s;

void erase (ll x) {
    auto it = s.find (x);
    s.erase (it);
}

int main () {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        scanf ("%d", &a[i]);
        if (a[i] > m) {
            cout << -1 << endl;
            return 0;
        }
    }

    int h = 1, t = 0;
    ll sum = 0;
    for (int i = 1, j = 1; i <= n; i ++) {

        sum += a[i];
        while (sum > m) {
            sum -= a[j ++];
        }

        //从 j ~ i - 1 中选择一个点作为最后一个区间的起点
        while (h <= t && q[h] < j) {
            //单调队列中两个元素之间会有一个贡献,所以每次增删都是两个元素之间的操作
            if (h < t) {
                erase (f[q[h]] + a[q[h + 1]]);
            }
            h ++;
        }
        
        while (h <= t && a[q[t]] <= a[i]) {
            if (h < t) {
                erase (f[q[t - 1]] + a[q[t]]);
            }
            t --;
        }
        q[ ++ t] = i;
        if (h < t) s.insert (f[q[t - 1]] + a[q[t]]);
        f[i] = f[j - 1] + a[q[h]];
        if (s.size ()) f[i] = min (f[i], *s.begin ());
    }

    cout << f[n] << endl;


    return 0;
}
posted @ 2025-10-09 21:19  michaele  阅读(8)  评论(0)    收藏  举报