算法-dp斜率优化
题解-[APIO2014]序列分割
个人认为斜率优化还是很玄学的,例题:
给你一个长度为 n n n 的序列 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an。你可以切 k k k 刀,每一刀可以把某一段序列切成两段,然后获得两段和成绩的收益。最后求最大收益和得到最大收益的切割方案。
数据范围: 2 ≤ n ≤ 100000 , 1 ≤ k ≤ min { n − 1 , 200 } , 1 ≤ a i ≤ 10000 2\le n\le 100000,1\le k\le\min\{n-1,200\},1\le a_i\le 10000 2≤n≤100000,1≤k≤min{n−1,200},1≤ai≤10000。
首先证明,切的顺序不影响结果。设序列为连着的 a , b , c a,b,c a,b,c 三段。三段的和分别为 A , B , C A,B,C A,B,C。
如果先切开
a
∣
b
,
c
a|b,c
a∣b,c 再切开
a
∣
b
∣
c
a|b|c
a∣b∣c,获益为
A
(
B
+
C
)
+
B
C
=
A
B
+
A
C
+
B
C
A(B+C)+BC=AB+AC+BC
A(B+C)+BC=AB+AC+BC。
如果先切开
a
,
b
∣
c
a,b|c
a,b∣c 再切开
a
∣
b
∣
c
a|b|c
a∣b∣c,获益为
(
A
+
B
)
C
+
A
B
=
A
C
+
B
C
+
A
B
(A+B)C+AB=AC+BC+AB
(A+B)C+AB=AC+BC+AB。
所以以此类推,切割的顺序不影响最终收益大小。
然后开始 dp \texttt{dp} dp, F i , j F_{i,j} Fi,j 表示前 i i i 个数切 j j j 刀的最大收益, s i = ∑ j = 1 i a j s_i=\sum\limits^i_{j=1}a_j si=j=1∑iaj,则有状态转移方程:
F i , j = max { F t , j − 1 + s t ( s i − s t ) } ( 0 ≤ t < i ) F_{i,j}=\max\{F_{t,j-1}+s_t(s_i-s_t)\}(0\le t<i) Fi,j=max{Ft,j−1+st(si−st)}(0≤t<i)
因为 F i , j F_{i,j} Fi,j 只由 F t , j − 1 F_{t,j-1} Ft,j−1 推得,所以可以滚动数组 F F F,令 f j = F i , j f_j=F_{i,j} fj=Fi,j, g j = F i , j − 1 g_j=F_{i,j-1} gj=Fi,j−1,那么上式就变成:
f i = max { g t + s t ( s i − s t ) } ( 0 ≤ t < i ) f_i=\max\{g_t+s_t(s_i-s_t)\}(0\le t<i) fi=max{gt+st(si−st)}(0≤t<i)
如果直接暴力跑一次 2 2 2 重循环的 dp \texttt{dp} dp, Θ ( n 2 k ) \Theta(n^2k) Θ(n2k) 会 TLE \color{#357}\texttt{TLE} TLE,但你仔细观察 g t + s t ( s i − s t ) g_t+s_t(s_i-s_t) gt+st(si−st) 这个式子,如果有一个 p ( 0 ≤ p < i ) p(0\le p<i) p(0≤p<i) 满足
g p + s p ( s i − s p ) ≥ g t + s t ( s i − s t ) g_p+s_p(s_i-s_p)\ge g_t+s_t(s_i-s_t) gp+sp(si−sp)≥gt+st(si−st)
则推式可得:
( g p − s p 2 ) − ( g t − s t 2 ) ≥ s t ⋅ s i − s p ⋅ s i (g_p-s_p^2)-(g_t-s_t^2)\ge s_t\cdot s_i-s_p\cdot s_i (gp−sp2)−(gt−st2)≥st⋅si−sp⋅si
∴ ( g p − s p 2 ) − ( g t − s t 2 ) s t − s p ≥ s i \therefore\frac{(g_p-s_p^2)-(g_t-s_t^2)}{s_t-s_p}\ge s_i ∴st−sp(gp−sp2)−(gt−st2)≥si
令 s l o p e = ( g p − s p 2 ) − ( g t − s t 2 ) s t − s p slope=\frac{(g_p-s_p^2)-(g_t-s_t^2)}{s_t-s_p} slope=st−sp(gp−sp2)−(gt−st2)
如果把 ( − s p , g p − s p 2 ) (-s_p,g_p-s_p^2) (−sp,gp−sp2) 和 ( − s t , g t − s t 2 ) (-s_t,g_t-s_t^2) (−st,gt−st2) 看作平面直角坐标系中的两个点,那么 s l o p e slope slope 就是这两个点连边的斜率。
因为 dp \texttt{dp} dp 循环 i = 1 → n i=1\to n i=1→n 时 s i s_i si 是递增的,而两个点的 s l o p e slope slope 又不是随 s i s_i si 变化的,所以可以维护一个双头单调队列,每次把 i i i 放到队尾,队列满足:
- 从左到右数递增。
- 从左到右相邻两个数所对应的点连边的斜率递减。
然后维护队列并 dp \texttt{dp} dp:
循环 j = 1 → k j=1\to k j=1→k:
赋值滚动数组 g = f g=f g=f,清零 f f f。
清空队列并在队列中加入 0 0 0(相当于原点)。
循环 i = 1 → n i=1\to n i=1→n:把队列头相邻两个数 s l o p e ≤ s i slope\le s_i slope≤si 的踢掉。
取队列头 p p p, f i = g p + s p ( s i − s p ) f_i=g_p+s_p(s_i-s_p) fi=gp+sp(si−sp)。
因为最终要输出方案,所以记录索引 p r o i , j = p pro_{i,j}=p proi,j=p。
把 i i i 看作点 ( − s i , g i − s i 2 ) (-s_i,g_i-s_i^2) (−si,gi−si2),如果队尾相邻元素的 s l o p e ≥ i slope\ge i slope≥i 和队尾元素的 s l o p e slope slope,就把队尾元素踢掉。
队尾加入 i i i。
然后在单调队列和斜率优化的加持下,因为维护队列和循环 dp \texttt{dp} dp 的总时间复杂度为 Θ ( n ) \Theta(n) Θ(n), 所以总的时间复杂度缩减为 Θ ( n k ) \Theta(nk) Θ(nk)。于是蒟蒻逃脱了 TLE \color{#357}\texttt{TLE} TLE 的风险。
AC \color{#7c2}\texttt{AC} AC 代码:
#include <bits/stdc++.h>
using namespace std;
/*
{a},{b},{c}
a(b+c)+bc=ab+ac+bc\-\Greatitude
(a+b)c+ab=ac+bc+ab/-/
*/
#define lng long long
const int N=1e5+10,K=210;
int n,k,a[N],q[N],p[N][K]; //n,k,ai,queue,方案路径
lng f[N],g[N],sum[N]; //fi,gt,si
double funct(int x,int y){ //两个点的slope
if(sum[x]==sum[y]) return -1e16;
return 1.0*((g[x]-sum[x]*sum[x])-(g[y]-sum[y]*sum[y]))/(sum[y]-sum[x]);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",a+i),sum[i]=sum[i-1]+a[i];
for(int j=1;j<=k;j++){ //维护单调队列+dp
for(int i=1;i<=n;i++) g[i]=f[i],f[i]=0;
int l=1,r=0;
q[++r]=0;
for(int i=1;i<=n;i++){
while(l<r&&funct(q[l],q[l+1])<=sum[i]) l++;
f[i]=g[q[l]]+sum[q[l]]*(sum[i]-sum[q[l]]);
p[i][j]=q[l];
while(l<r&&funct(q[r-1],q[r])>=funct(q[r],i)) r--;
q[++r]=i;
}
}
printf("%lld\n",f[n]); //输出最终最大收益
for(int i=k,j=n;i>=1;i--)
printf("%d%c",j=p[j][i],"\n "[i>1]); //输出切割方案。
return 0;
}
祝大家学习愉快!