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)\)
转移:
这个方程直接去转移的话是 \(O(n^3)\) 的,可以倒序枚举 \(j\) ,用一个变量来记录 \(max(a_k)\) ,可以将时间复杂度优化到 \(O(n^2)\) ,但是对于 \(10^5\) 的数据还是不够快
dp转移的指导思想就是及时排除不可能的决策
所以我们要想一想我们的转移中是否存在不可能成为最优解的决策?
可以画图来帮助理解
假设现在要求 \(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\)
我们找到 \(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;
}