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  阅读(35)  评论(0)    收藏  举报