裁剪序列
给定一个长度为 $N$ 的序列 $A$,要求把该序列分成若干段,在满足“每段中所有数的和”不超过 $M$ 的前提下,让“每段中所有数的最大值”之和最小。
试计算这个最小值。
输入格式
第一行包含两个整数 $N$ 和 $M$。
第二行包含 $N$ 个整数,表示完整的序列 $A$。
输出格式
输出一个整数,表示结果。
如果结果不存在,则输出 $−1$。
数据范围
$0 \leq N \leq {10}^5$,
$0 \leq M \leq {10}^{11}$,
序列 $A$ 中的数非负,且不超过 ${10}^6$。
输入样例:
8 17 2 2 2 8 1 8 2 1
输出样例:
12
解题思路
这题就是纯数据结构优化dp,难的地方全在去优化上。
定义状态$f(i)$表示所有前$i$个数的合法划分方案(每一段的和不超过$m$)的集合,属性是代价的最小值。根据第$i$个元素所在的最后一段的长度来划分集合,状态转移方程就是$$f(i) = \min\limits_{s_i - s_j \leq m} \left\{ {f(j) + \max\limits_{j+1 \leq k \leq i} \{ {a_k} \}} \right\}$$
其中式子中的$s_k$表示前缀和,选取的左端点$j$要满足$\sum\limits_{k = j}^{i}{a_k} \leq m$。
这种做法的时间复杂度为$O(n^2)$,因此需要优化。这题麻烦的地方在于取最小值的量$f(j) + \max\limits_{j+1 \leq k \leq i} \{ {a_k} \}$是一个整体,会随着$i$的变化而变化(即取最大值的地方),因此是一个动态变化的量,不好维护。
当枚举到$i$,假设最远可以取到左端点$j$满足$\sum\limits_{k = j}^{i}{a_k} \leq m$($j$可以通过双指针来维护求得),我们先直接选择$[j,i]$作为最后一段,那么有$f(i) = f(j-1) + a_{k_{1}}$,其中$a_{k_1} = \max\limits_{j \leq k \leq i} \{ a_k \}$(如果有多个值等于$a_{k_1}$,那么取最右边的那个$a_{k_1}$)。
我们观察一下有什么性质,可以发现对于$u \in [j, k_1]$,$\max\limits_{j \leq u \leq k_1} \{ a_u \} = a_{k_{1}}$不变。如果我们的左端点选择$u$,那么在$f(i) = f(u-1) + a_{k_{1}}$中,$f(u-1)$会随着$u$的减小而单调递减,下面来证明这个性质。
假设有两个序列$i$和$j$,其中$j$比$i$多出一部分。
对于$j$的任何一种划分方案都可以对应到$i$中,如果在$i$中不存在则忽略(如上图中$j$后面多出的最后一段)。那么$i$和$j$中前面共同部分的每一段都是一样的,因此每一段的最大值之和也是一样的,区别在于最后一段,由于$i$的最后一段比$j$要少,因此$i$最后一段的最大值一定不超过$j$最后一段的最大值($j$后面可能还有其他段)。因此必然有$f(i) \leq f(j)$,即长度越短,对应的$f(i)$也越小。
因此得到$f(i)$会随着$i$减小而递减这个性质。因此如果选择$[j,i]$最为最后一段,为了使得$f(i)$最小,在$u \in [j, k_1]$中左端点应该取$u = j$。
如果左端点取到$k_1$后面的位置,那么要重新求一下$[k_1 + 1, i]$中的最大值(同理如果有多个最大值应该取最靠右的那个)。
假设最大值在$k_2$位置,$a_{k_2}$一定小于$a_{k_1}$。
如果选取的左端点$u \in [k_1 + 1, k_2]$,为了使得$f(i)$取到最小值,与上面的分析一样,应该选择$u = k_1 + 1$,那么就会有$f(i) = f(u - 1) + a_{k_2} = f(k_1) + a_{k_2}$。
以此类推,继续在$[k_2+1, i]$找到最大值$a_{k_3}$,在$[k_3 + 1, i]$找到最大值$a_{k_4}$....
若选取的左端点在$[k_u + 1, k_{u+1}]$区间内,$f(i)$的最小值就是$f(k_u) + a_{k_{u+1}}$ 。
因此我们可以维护这个单调递减的序列,那么左端点从这些下标中选,有$f(i) = \min \left\{ f(k_u) + a_{k_{u+1}} \right\}$(当然还应包括整个$[j, i]$的$f(j) + a_{k_1}$)。问题是如何维护这个序列呢?其实这个过程与用单调队列维护区间最大值相同。每当$i$往后移动,$j$也只会往后移动(双指针),序列中的$k$如果不在$[j,i]$范围内删除即可。当枚举到$i+1$,那么把序列中$a_k \leq a_{i+1}$所对应的$k$删掉。
当然如果直接枚举这个维护的序列来求最小值还是$O(n^2)$的,由于每次都是在一个集合中求最小值,且每次操作都是对序列的头部尾部删除和插入元素,因此可以开个平衡树来实现这些操作,这里用$\text{std::multiset}$。平衡树维护的值就是维护的序列中对应的$f(k_u) + a_{k_{u+1}}$ 。
这里考虑一些边界情况。如果维护的序列中有$x$个元素,那么就有$x-1$个$f(k_u) + a_{k_{u+1}}$ ,即平衡树维护的元素数量为$x-1$。由于在删除序列中的元素$k_u$时也要在平衡树中删除对应的元素$f(k_u) + a_{k_{u+1}}$ ,因此只有序列元素至少有两个时,才能从平衡树中进行删除。插入的情况也是,只有在插入元素后序列至少有两个元素,才能有对应的$f(k_u) + a_{k_{u+1}}$插入到平衡树中。
AC代码如下,时间复杂度为$O(n \log n)$:
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/
标签:limits,最大值,裁剪,leq,最小值,端点,序列 From: https://www.cnblogs.com/onlyblues/p/17152708.html