算法-dp斜率优化 + 题解-[APIO2014]序列分割

算法-dp斜率优化

题解-[APIO2014]序列分割


个人认为斜率优化还是很玄学的,例题:

[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 2n100000,1kmin{n1,200},1ai10000


首先证明,切的顺序不影响结果。设序列为连着的 a , b , c a,b,c a,b,c 三段。三段的和分别为 A , B , C A,B,C A,B,C

如果先切开 a ∣ b , c a|b,c ab,c 再切开 a ∣ b ∣ c a|b|c abc,获益为 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,bc 再切开 a ∣ b ∣ c a|b|c abc,获益为 ( 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=1iaj,则有状态转移方程:

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,j1+st(sist)}(0t<i)

因为 F i , j F_{i,j} Fi,j 只由 F t , j − 1 F_{t,j-1} Ft,j1 推得,所以可以滚动数组 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,j1,那么上式就变成:

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(sist)}(0t<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(sist) 这个式子,如果有一个 p ( 0 ≤ p < i ) p(0\le p<i) p(0p<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(sisp)gt+st(sist)

则推式可得:

( 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 (gpsp2)(gtst2)stsispsi

∴ ( 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 stsp(gpsp2)(gtst2)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=stsp(gpsp2)(gtst2)

如果把 ( − s p , g p − s p 2 ) (-s_p,g_p-s_p^2) (sp,gpsp2) ( − s t , g t − s t 2 ) (-s_t,g_t-s_t^2) (st,gtst2) 看作平面直角坐标系中的两个点,那么 s l o p e slope slope 就是这两个点连边的斜率。

因为 dp \texttt{dp} dp 循环 i = 1 → n i=1\to n i=1n s i s_i si 是递增的,而两个点的 s l o p e slope slope 又不是随 s i s_i si 变化的,所以可以维护一个双头单调队列,每次把 i i i 放到队尾,队列满足:

  1. 从左到右数递增。
  2. 从左到右相邻两个数所对应的点连边的斜率递减。

然后维护队列并 dp \texttt{dp} dp

循环 j = 1 → k j=1\to k j=1k

赋值滚动数组 g = f g=f g=f,清零 f f f
清空队列并在队列中加入 0 0 0(相当于原点)。
循环 i = 1 → n i=1\to n i=1n

把队列头相邻两个数 s l o p e ≤ s i slope\le s_i slopesi 的踢掉。
取队列头 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(sisp)
因为最终要输出方案,所以记录索引 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,gisi2),如果队尾相邻元素的 s l o p e ≥ i slope\ge i slopei 和队尾元素的 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;
}

祝大家学习愉快!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值