裁剪序列

裁剪序列

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

试计算这个最小值。

输入格式

第一行包含两个整数 NM

第二行包含 N 个整数,表示完整的序列 A

输出格式

输出一个整数,表示结果。

如果结果不存在,则输出 1

数据范围

0N105,
0M1011,
序列 A 中的数非负,且不超过 106

输入样例:

8 17
2 2 2 8 1 8 2 1

输出样例:

12

 

解题思路

  这题就是纯数据结构优化dp,难的地方全在去优化上。

  定义状态f(i)表示所有前i个数的合法划分方案(每一段的和不超过m)的集合,属性是代价的最小值。根据第i个元素所在的最后一段的长度来划分集合,状态转移方程就是f(i)=minsisjm{f(j)+maxj+1ki{ak}}

  其中式子中的sk表示前缀和,选取的左端点j要满足k=jiakm

  这种做法的时间复杂度为O(n2),因此需要优化。这题麻烦的地方在于取最小值的量f(j)+maxj+1ki{ak}是一个整体,会随着i的变化而变化(即取最大值的地方),因此是一个动态变化的量,不好维护。

  当枚举到i,假设最远可以取到左端点j满足k=jiakmj可以通过双指针来维护求得),我们先直接选择[j,i]作为最后一段,那么有f(i)=f(j1)+ak1,其中ak1=maxjki{ak}(如果有多个值等于ak1,那么取最右边的那个ak1)。

  我们观察一下有什么性质,可以发现对于u[j,k1]maxjuk1{au}=ak1不变。如果我们的左端点选择u,那么在f(i)=f(u1)+ak1中,f(u1)会随着u的减小而单调递减,下面来证明这个性质。

  假设有两个序列ij,其中ji多出一部分。

  对于j的任何一种划分方案都可以对应到i中,如果在i中不存在则忽略(如上图中j后面多出的最后一段)。那么ij中前面共同部分的每一段都是一样的,因此每一段的最大值之和也是一样的,区别在于最后一段,由于i的最后一段比j要少,因此i最后一段的最大值一定不超过j最后一段的最大值(j后面可能还有其他段)。因此必然有f(i)f(j),即长度越短,对应的f(i)也越小。

  因此得到f(i)会随着i减小而递减这个性质。因此如果选择[j,i]最为最后一段,为了使得f(i)最小,在u[j,k1]中左端点应该取u=j

  如果左端点取到k1后面的位置,那么要重新求一下[k1+1,i]中的最大值(同理如果有多个最大值应该取最靠右的那个)。

  假设最大值在k2位置,ak2一定小于ak1

  如果选取的左端点u[k1+1,k2],为了使得f(i)取到最小值,与上面的分析一样,应该选择u=k1+1,那么就会有f(i)=f(u1)+ak2=f(k1)+ak2

  以此类推,继续在[k2+1,i]找到最大值ak3,在[k3+1,i]找到最大值ak4....

  若选取的左端点在[ku+1,ku+1]区间内,f(i)的最小值就是f(ku)+aku+1

  因此我们可以维护这个单调递减的序列,那么左端点从这些下标中选,有f(i)=min{f(ku)+aku+1}(当然还应包括整个[j,i]f(j)+ak1)。问题是如何维护这个序列呢?其实这个过程与用单调队列维护区间最大值相同。每当i往后移动,j也只会往后移动(双指针),序列中的k如果不在[j,i]范围内删除即可。当枚举到i+1,那么把序列中akai+1所对应的k删掉。

  当然如果直接枚举这个维护的序列来求最小值还是O(n2)的,由于每次都是在一个集合中求最小值,且每次操作都是对序列的头部尾部删除和插入元素,因此可以开个平衡树来实现这些操作,这里用std::multiset。平衡树维护的值就是维护的序列中对应的f(ku)+aku+1

  这里考虑一些边界情况。如果维护的序列中有x个元素,那么就有x1f(ku)+aku+1 ,即平衡树维护的元素数量为x1。由于在删除序列中的元素ku时也要在平衡树中删除对应的元素f(ku)+aku+1 ,因此只有序列元素至少有两个时,才能从平衡树中进行删除。插入的情况也是,只有在插入元素后序列至少有两个元素,才能有对应的f(ku)+aku+1插入到平衡树中。

  AC代码如下,时间复杂度为O(nlogn)

复制代码
 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/

posted @   onlyblues  阅读(133)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2022-02-24 三体攻击
Web Analytics
点击右上角即可分享
微信分享提示