裁剪序列
裁剪序列
给定一个长度为 的序列 ,要求把该序列分成若干段,在满足“每段中所有数的和”不超过 的前提下,让“每段中所有数的最大值”之和最小。
试计算这个最小值。
输入格式
第一行包含两个整数 和 。
第二行包含 个整数,表示完整的序列 。
输出格式
输出一个整数,表示结果。
如果结果不存在,则输出 。
数据范围
,
,
序列 中的数非负,且不超过 。
输入样例:
8 17 2 2 2 8 1 8 2 1
输出样例:
12
解题思路
这题就是纯数据结构优化dp,难的地方全在去优化上。
定义状态表示所有前个数的合法划分方案(每一段的和不超过)的集合,属性是代价的最小值。根据第个元素所在的最后一段的长度来划分集合,状态转移方程就是
其中式子中的表示前缀和,选取的左端点要满足。
这种做法的时间复杂度为,因此需要优化。这题麻烦的地方在于取最小值的量是一个整体,会随着的变化而变化(即取最大值的地方),因此是一个动态变化的量,不好维护。
当枚举到,假设最远可以取到左端点满足(可以通过双指针来维护求得),我们先直接选择作为最后一段,那么有,其中(如果有多个值等于,那么取最右边的那个)。
我们观察一下有什么性质,可以发现对于,不变。如果我们的左端点选择,那么在中,会随着的减小而单调递减,下面来证明这个性质。
假设有两个序列和,其中比多出一部分。
对于的任何一种划分方案都可以对应到中,如果在中不存在则忽略(如上图中后面多出的最后一段)。那么和中前面共同部分的每一段都是一样的,因此每一段的最大值之和也是一样的,区别在于最后一段,由于的最后一段比要少,因此最后一段的最大值一定不超过最后一段的最大值(后面可能还有其他段)。因此必然有,即长度越短,对应的也越小。
因此得到会随着减小而递减这个性质。因此如果选择最为最后一段,为了使得最小,在中左端点应该取。
如果左端点取到后面的位置,那么要重新求一下中的最大值(同理如果有多个最大值应该取最靠右的那个)。
假设最大值在位置,一定小于。
如果选取的左端点,为了使得取到最小值,与上面的分析一样,应该选择,那么就会有。
以此类推,继续在找到最大值,在找到最大值....
若选取的左端点在区间内,的最小值就是 。
因此我们可以维护这个单调递减的序列,那么左端点从这些下标中选,有(当然还应包括整个的)。问题是如何维护这个序列呢?其实这个过程与用单调队列维护区间最大值相同。每当往后移动,也只会往后移动(双指针),序列中的如果不在范围内删除即可。当枚举到,那么把序列中所对应的删掉。
当然如果直接枚举这个维护的序列来求最小值还是的,由于每次都是在一个集合中求最小值,且每次操作都是对序列的头部尾部删除和插入元素,因此可以开个平衡树来实现这些操作,这里用。平衡树维护的值就是维护的序列中对应的 。
这里考虑一些边界情况。如果维护的序列中有个元素,那么就有个 ,即平衡树维护的元素数量为。由于在删除序列中的元素时也要在平衡树中删除对应的元素 ,因此只有序列元素至少有两个时,才能从平衡树中进行删除。插入的情况也是,只有在插入元素后序列至少有两个元素,才能有对应的插入到平衡树中。
AC代码如下,时间复杂度为:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 typedef long long LL; 5 6 const int N = 1e5 + 10; 7 8 int a[N]; 9 LL f[N]; 10 int q[N], hh, tt = -1; 11 12 int main() { 13 LL n, m; 14 scanf("%lld %lld", &n, &m); 15 for (int i = 1; i <= n; i++) { 16 scanf("%d", a + i); 17 if (a[i] > m) { // 如果某个a[i]>m那么一定无解 18 printf("-1"); 19 return 0; 20 } 21 } 22 multiset<LL> st; 23 LL s = 0; 24 for (int i = 1, j = 1; i <= n; i++) { 25 s += a[i]; 26 while (s > m) { 27 s -= a[j++]; 28 } 29 while (hh <= tt && q[hh] < j) { // 在维护的序列中把下标小于j的删掉 30 if (hh < tt) st.erase(st.find(f[q[hh]] + a[q[hh + 1]])); // 如果维护的序列中只有一个元素,此时平衡树中没有元素 31 hh++; 32 } 33 while (hh <= tt && a[q[tt]] <= a[i]) { // 把小于等于a[i]的元素删掉,保持序列单调递减 34 if (hh < tt) st.erase(st.find(f[q[tt - 1]] + a[q[tt]])); // 同理至少要有两个元素才能从平衡树中删除 35 tt--; 36 } 37 q[++tt] = i; // 插入a[i] 38 if (hh < tt) st.insert(f[q[tt - 1]] + a[q[tt]]); // 序列中至少要有两个元素才能往平衡树中插入 39 f[i] = f[j - 1] + a[q[hh]]; // 选择最后一段选择整个[j, i] 40 if (!st.empty()) f[i] = min(f[i], *st.begin()); // 维护序列中的最小值 41 } 42 printf("%lld", f[n]); 43 44 return 0; 45 }
参考资料
AcWing 299. 裁剪序列(蓝桥杯集训·每日一题):https://www.acwing.com/video/4634/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17152708.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2022-02-24 三体攻击