AcWing 299 裁剪序列
这道题算是我做过所有的单调队列优化 d p dp dp 题目中最难想的一道题,所以写篇题解再捋捋思路。
暴力
首先很容易想到设
d
p
i
dp_i
dpi 表示将前
i
i
i 个数划分成若干序列,【每个序列的最大值之和】的最小值。
那么就会有:
d
p
i
=
m
i
n
{
d
p
j
+
m
a
x
k
=
j
+
1
i
{
a
k
}
}
,
其中
0
≤
j
<
i
且
∑
k
=
j
+
1
i
a
k
≤
m
.
dp_i = min \begin{Bmatrix} dp_j + max_{k = j + 1}^{i} \begin{Bmatrix} a_k \end{Bmatrix} \end{Bmatrix}, \\ 其中 \ 0 \leq j < i \ 且 \ \sum_{k = j + 1}^{i} a_k \leq m.
dpi=min{dpj+maxk=j+1i{ak}},其中 0≤j<i 且 k=j+1∑iak≤m.
这样子的复杂度是
O
(
n
3
)
O(n^3)
O(n3),考虑优化。
优化
先证明一个东西,那就是
d
p
i
dp_i
dpi 是单调不减的(也就是非严格单调递增),即:对于任意的
i
i
i,都有
d
p
i
≤
d
p
i
+
1
dp_i \leq dp_{i + 1}
dpi≤dpi+1。
这是显然的,因为多加一个数,它如果单开一个序列,那么就会造成贡献;如果它归为最后一个已有的序列,那么若它比最后一序列中的最大值小,那么它就不会产生贡献,否则就会产生贡献,使最大值之和变大。
然后观察转移方程:
d
p
i
=
m
i
n
{
d
p
j
+
m
a
x
k
=
j
+
1
i
{
a
k
}
}
dp_i = min \begin{Bmatrix} dp_j + max_{k = j + 1}^{i} \begin{Bmatrix} a_k \end{Bmatrix} \end{Bmatrix}
dpi=min{dpj+maxk=j+1i{ak}}
可以发现,
m
a
x
k
=
j
+
1
i
{
a
k
}
max_{k = j + 1}^{i} \begin{Bmatrix} a_k \end{Bmatrix}
maxk=j+1i{ak} 随着
j
j
j 的增加,是非严格单调递减的,因为右端点
i
i
i 不动,所以
[
j
+
1
,
i
]
[j + 1, i]
[j+1,i] 中的的最大值是越来越小或者不变的。
因此我们就会有一个这样的发现:在
m
a
x
k
=
j
+
1
i
{
a
k
}
max_{k = j + 1}^{i} \left \{ a_k \right\}
maxk=j+1i{ak} 的值相等的情况下,我的决策点
j
j
j 是越靠前越好的(因为
d
p
i
dp_i
dpi 是单调不减的嘛)。
如此一来,在合法区间内有若干个的可能的最优决策点(分别对应使
m
a
x
k
=
j
+
1
i
{
a
k
}
max_{k = j + 1}^{i} \left \{ a_k \right\}
maxk=j+1i{ak} 值相等的最小的位置
j
j
j)。因此我们就用单调队列来维护
a
k
a_k
ak 单调递减,队列所记录的位置就是一个可能的决策点。
至于查询所有可能决策点的最小贡献,用
m
u
l
t
i
s
e
t
multiset
multiset 来实现(这句可能不好理解,可以先看代码)。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 7;
int n, a[maxn];
ll m, s, dp[maxn];
int l[maxn];
int q[maxn], h, t;
multiset<ll> S;
int main() {
scanf("%d%lld", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
if (a[i] > m) {
printf("-1\n");
return 0;
}
}
// 预处理使 a[j+1...i] 之和小于等于 m 的最小 j
for (int i = 1, j = 0; i <= n; ++i) {
s += a[i];
while (s > m) s -= a[++j];
l[i] = j;
}
h = 1, t = 0;
for (int i = 1; i <= n; ++i) {
// 如果之前的某些决策点已经不在合法区间
// 那么就要删去, 其所对应的可能答案也要删去
while (h <= t && q[h] < l[i]) {
S.erase(dp[q[h]] + a[q[h + 1]]);
++h;
}
// 维 a 护单调递减
while (h <= t && a[q[t]] <= a[i]) {
S.erase(dp[q[t - 1]] + a[q[t]]);
--t;
}
if (h <= t) S.insert(dp[q[t]] + a[i]);
q[++t] = i;
// 这句是为了包括【队头的决策点】【从合法区间的最左端转移过来】的情况
// 学校机房上传不了图片来解释,先鸽着
dp[i] = dp[l[i]] + a[q[h]];
if (S.size()) dp[i] = min(dp[i], *(S.begin()));
}
printf("%lld\n", dp[n]);
return 0;
}