洛谷 P3648 APIO2014 序列分割 题解
写了挺多斜率优化的题目了,这道(差点)就速切了,原因还是单调队列维护斜率的写法出锅。
题意
题目描述
你正在玩一个关于长度为 n n n 的非负整数序列的游戏。这个游戏中你需要把序列分成 k + 1 k + 1 k+1 个非空的块。为了得到 k + 1 k + 1 k+1 块,你需要重复下面的操作 k k k 次:
选择一个有超过一个元素的块(初始时你只有一块,即整个序列)
选择两个相邻元素把这个块从中间分开,得到两个非空的块。
每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分,并知道得到最大总分的方案。
如果有多种方案使得总得分最大,输出任意一种方案即可。
输入
7 3
4 1 3 4 0 2 3
输出
108
1 3 5
2 ≤ n ≤ 100000 , 1 ≤ k ≤ min { n − 1 , 200 } 2 \leq n \leq 100000, 1 \leq k \leq \min\{n - 1, 200\} 2≤n≤100000,1≤k≤min{n−1,200}。
思路
对序列分割,分割的顺序会不会对答案造成影响呢?
之前 smsky 选拔的时候,第一题与之是类似的。我们可以猜测:对于这种大块的乘积,如果固定了分割点,无论分割顺序如何,都不会改变结果。
考虑简要地证明这个结论:设序列有三个分割点 1 , 2 , 3 1,2,3 1,2,3,序列被分割成了元素和依次为 a , b , c , d a,b,c,d a,b,c,d 的四块。
- 按 1 , 2 , 3 1,2,3 1,2,3 的顺序分割: a n s 1 = a ( b + c + d ) + a ( b + c ) + c d = a b + a c + a d + b c + b d + c d ans_1=a(b+c+d)+a(b+c)+cd=ab+ac+ad+bc+bd+cd ans1=a(b+c+d)+a(b+c)+cd=ab+ac+ad+bc+bd+cd;
- 按 3 , 1 , 2 3,1,2 3,1,2 的顺序分割: a n s 2 = ( a + b + c ) d + a ( b + c ) + b c = a d + b d + c d + a b + a c + b c ans_2=(a+b+c)d+a(b+c)+bc=ad+bd+cd+ab+ac+bc ans2=(a+b+c)d+a(b+c)+bc=ad+bd+cd+ab+ac+bc;
- 按 1 , 3 , 2 1,3,2 1,3,2 的顺序分割:。。。
多枚举几组,我们发现 a n s ans ans 始终不变。
因此简证成立。
那么我们就可以 1 ∼ n 1\sim n 1∼n 枚举分割点了。考虑朴素的 dp,设 f i , t f_{i,t} fi,t 表示前 i i i 个数,分割了 t t t 次,且在 i i i 之后不分割的最小花费。(这样子就不用特意讨论 n n n 之后还被分割了的情况了)
处理前缀和
p
r
e
i
=
∑
j
=
1
i
a
j
pre_i=\sum_{j=1}^{i}a_j
prei=∑j=1iaj。枚举分割点
j
j
j,在
1
∼
i
1\sim i
1∼i 分成了
1
∼
j
1\sim j
1∼j 和
j
+
1
∼
i
j+1\sim i
j+1∼i 两块,和分别是:
f
i
,
t
=
max
j
=
1
i
−
1
{
f
j
,
t
−
1
+
p
r
e
j
(
p
r
e
i
−
p
r
e
j
)
}
f_{i,t}=\max_{j=1}^{i-1}\left\{f_{j,t-1}+pre_j(pre_i-pre_j)\right\}
fi,t=j=1maxi−1{fj,t−1+prej(prei−prej)}
设两个决策点
j
1
<
j
2
j1<j2
j1<j2 且
j
2
j2
j2 优于
j
1
j1
j1,那么:
f
j
1
,
t
−
1
+
p
r
e
j
1
(
p
r
e
i
−
p
r
e
j
1
)
<
f
j
2
,
t
−
1
+
p
r
e
j
2
(
p
r
e
i
−
p
r
e
j
2
)
f_{j1,t-1}+pre_{j1}(pre_i-pre_{j1})<f_{j2,t-1}+pre_{j2}(pre_i-pre_{j2})
fj1,t−1+prej1(prei−prej1)<fj2,t−1+prej2(prei−prej2)
f j 1 , t − 1 + p r e i p r e j 1 − p r e j 1 2 ) < f j 2 , t − 1 + p r e i p r e j 2 − p r e j 2 2 f_{j1,t-1}+pre_ipre_{j1}-pre_{j1}^2)<f_{j2,t-1}+pre_ipre_{j2}-pre_{j2}^2 fj1,t−1+preiprej1−prej12)<fj2,t−1+preiprej2−prej22
f j 1 , t − 1 − f j 2 , t − 1 + p r e j 2 2 − p r e j 1 2 < p r e i ( p r e j 2 − p r e j 1 ) f_{j1,t-1}-f_{j2,t-1}+pre_{j2}^2-pre_{j1}^2<pre_i(pre_{j2}-pre_{j1}) fj1,t−1−fj2,t−1+prej22−prej12<prei(prej2−prej1)
注意到
p
r
e
j
2
−
p
r
e
j
1
≥
0
pre_{j2}-pre_{j1}\ge 0
prej2−prej1≥0,依旧注意避开
0
0
0;如果非
0
0
0 就可以直接除过去:
f
j
1
,
t
−
1
−
f
j
2
,
t
−
1
+
p
r
e
j
2
2
−
p
r
e
j
1
2
p
r
e
j
2
−
p
r
e
j
1
<
p
r
e
i
\frac{f_{j1,t-1}-f_{j2,t-1}+pre_{j2}^2-pre_{j1}^2}{pre_{j2}-pre_{j1}}<pre_i
prej2−prej1fj1,t−1−fj2,t−1+prej22−prej12<prei
s l o p e < p r e i slope<pre_i slope<prei 且 p r e i pre_i prei 单增,和玩具装箱是同一类,维护单调递增斜率,最优决策扔队首就好。
因为还有一个切割次数的维度,所以可以对每一次切割 t ∈ [ 1 , k ] t\in[1,k] t∈[1,k],每次 t + 1 ← t t+1\leftarrow t t+1←t 转移的时候做一次斜率优化 dp 即可。
题目还有一个要求就是求切割的方案,那也很好做,就是记录前缀数组 f a t i , t fat_{i,t} fati,t 表示枚举到 i i i,切割了 t t t 次的第 t t t 次切割点。由于单调队列直接维护反馈的就是最优决策点,那么用 f a t fat fat 数组记录每次的队首就好。
有一点要注意,因为 f a t fat fat 数组与单调队列维护的最优决策点挂钩,那么单调队列应当严谨维护。在上一篇题解,弹出队尾时时要写 ≤ \le ≤ 之外,还要把队首往后压,防止出现 0 0 0 的情况(其实就是把推式子的时候,把两个决策点 j 1 j1 j1 和 j 2 j2 j2 之间的符号改成 ≤ \le ≤,对答案 1 1 1 正确性没有影响,但是缩减了很多不必要的 0 0 0 决策点)
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define dd double
const ll N=1e5+9,K=202,inf=0x7f7f7f7f;
ll n,k,a[N];
ll f[N][K],q[N],pre[N];
int fat[N][K];
ll _2(ll x)
{
return x*x;
}
dd slope(ll j1,ll j2,ll t)
{
if(pre[j1]==pre[j2])return -inf;
return (dd)(f[j1][t-1]-f[j2][t-1]+_2(pre[j2])-_2(pre[j1]))/(dd)(pre[j2]-pre[j1]);
}
int main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
pre[i]=pre[i-1]+a[i];
}
for(int t=1;t<=k;t++)
{
ll hh=0,tt=0;
for(int i=1;i<=n;i++)
{
while(hh<tt&&slope(q[hh],q[hh+1],t)<=pre[i])hh++;
f[i][t]=f[q[hh]][t-1]+pre[q[hh]]*(pre[i]-pre[q[hh]]);
fat[i][t]=q[hh];
while(hh<tt&&slope(q[tt],i,t)<=slope(q[tt-1],q[tt],t))tt--;
q[++tt]=i;
}
}
printf("%lld\n",f[n][k]);
for(int tem=n,i=k;i>=1;i--)
{
tem=fat[tem][i];
printf("%d ",tem);
}
return 0;
}