裁剪序列

裁剪序列

给定一个长度为 $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/

posted @ 2023-02-24 19:49  onlyblues  阅读(68)  评论(0编辑  收藏  举报
Web Analytics