[APIO2014] 序列分割(斜率优化dp)

problem

luogu-P3648

你正在玩一个关于长度为 n n n 的非负整数序列的游戏。这个游戏中你需要把序列分成 k + 1 k+1 k+1 个非空的块。

为了得到 k + 1 k+1 k+1 块,你需要重复下面的操作 k k k 次:

  • 选择一个有超过一个元素的块(初始时你只有一块,即整个序列)
  • 选择两个相邻元素把这个块从中间分开,得到两个非空的块。
  • 每次操作后你将获得那两个新产生的块的元素和的乘积的分数。

你想要最大化最后的总得分。

输出最后得分以及划分的任一方案。

solution

前提结论:得分与切的先后顺序无关

假设要把 a b c abc abc 切成 a ∣ b ∣ c a\mid b\mid c abc

  • 先切 a ∣ b c a\mid bc abc,再切 a ∣ b ∣ c a\mid b\mid c abc a n s = a ∗ ( b + c ) + b ∗ c = a b + a c + b c ans=a*(b+c)+b*c=ab+ac+bc ans=a(b+c)+bc=ab+ac+bc
  • 先切 a b ∣ c ab\mid c abc,再切 a ∣ b ∣ c a\mid b\mid c abc a n s = ( a + b ) ∗ c + a ∗ b = a c + b c + a b ans=(a+b)*c+a*b=ac+bc+ab ans=(a+b)c+ab=ac+bc+ab

这样就可以从左到右 d p dp dp 了。记 s u m i = ∑ j = 1 i a i sum_i=\sum_{j=1}^ia_i sumi=j=1iai,即元素分数前缀和。

f ( i , j ) : f(i,j): f(i,j): i i i 个数切成 j j j 段的最大得分。

f ( i , j ) = max ⁡ k < i { f ( k , j − 1 ) + s u m ( k ) ∗ ( s u m ( i ) − s u m ( k ) ) } f(i,j)=\max_{k<i}\Big\{f(k,j-1)+sum(k)*\big(sum(i)-sum(k)\big)\Big\} f(i,j)=maxk<i{f(k,j1)+sum(k)(sum(i)sum(k))}

j j j 只跟 j − 1 j-1 j1 有关,考虑提到最外面循环,内层简写 f ( i , j − 1 ) = f ( i ) f(i,j-1)=f(i) f(i,j1)=f(i)

假设 k 1 < k 2 k_1<k_2 k1<k2,且 k 1 k_1 k1 决策优于 k 2 k_2 k2

则必定满足:
f ( k 1 ) + s u m k 1 ∗ s u m i − s u m k 1 2 > f ( k 2 ) + s u m k 2 ∗ s u m i − s u m k 2 2 f(k_1)+sum_{k_1}*sum_i-sum_{k_1}^2>f(k_2)+sum_{k_2}*sum_i-sum_{k_2}^2 f(k1)+sumk1sumisumk12>f(k2)+sumk2sumisumk22

f ( k 1 ) − s u m k 1 2 − f ( k 2 ) + s u m k 2 2 > ( s u m k 2 − s u m k 1 ) s u m i f(k_1)-sum_{k_1}^2-f(k_2)+sum_{k_2}^2>(sum_{k_2}-sum_{k_1})sum_i f(k1)sumk12f(k2)+sumk22>(sumk2sumk1)sumi

s u m sum sum 是前缀和数组,所以 s u m k 2 ≥ s u m k 1 sum_{k_2}\ge sum_{k_1} sumk2sumk1,具有单调性。
( f ( k 1 ) − s u m k 1 2 ) − ( f ( k 2 ) − s u m k 2 2 ) ( − s u m k 1 ) − ( − s u m k 2 ) > s u m ( i ) \frac{\big(f(k_1)-sum_{k_1}^2\big)-\big(f(k_2)-sum_{k_2}^2\big)}{(-sum_{k_1})-(-sum_{k_2})}>sum(i) (sumk1)(sumk2)(f(k1)sumk12)(f(k2)sumk22)>sum(i)
标准斜率优化的式子,因为 O ( n k ) O(nk) O(nk) 无法再接受一个 log ⁡ \log log ,所以我最热爱的李超树就被 pass \text{pass} pass 掉了。

斜率优化想必最头疼的就是用队列维护首尾弹出时的大小于符号的定向问题。

网上一堆凸包图形,对于我这种平面几何感几乎为零的蒟蒻而言简直就是天方夜谭。

所以这里写一种判断方法吧,不保证正确性

  • 队首的弹出很简单,因为上面的斜率优化式子已经化出来了,直接 s l o p e ( q [ h e a d ] , q [ h e a d + 1 ] ) slope(q[head],q[head+1]) slope(q[head],q[head+1]) s u m ( i ) sum(i) sum(i) 比较。

    如果 ≤ s u m ( i ) \le sum(i) sum(i) 说明 k 2 k_2 k2 即后者 q [ h e a d + 1 ] q[head+1] q[head+1] 更优。

    就需要弹队首。

  • 队尾的弹出较为复杂,考虑的是 s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) slope(q[tail-1],q[tail]) slope(q[tail1],q[tail]) s l o p e ( q [ t a i l ] , i ) slope(q[tail],i) slope(q[tail],i)

    准确而言是考虑 q [ t a i l ] q[tail] q[tail] 可不可能做队首。

    那么当现在的队尾在后面某个枚举位置时变成了队首,

    就必须要满足 s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) ≤ s u m p slope(q[tail-1],q[tail])\le sum_p slope(q[tail1],q[tail])sump,这样才能弹出 q [ t a i l − 1 ] q[tail-1] q[tail1]

    • 假设弹队尾的条件是 s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) ≤ s l o p e ( q [ t a i l ] , i ) slope(q[tail-1],q[tail])\le slope(q[tail],i) slope(q[tail1],q[tail])slope(q[tail],i)

      显然存在 s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) ≤ s u m p < s l o p e ( q [ t a i l ] , i ) slope(q[tail-1],q[tail])\le sum_p<slope(q[tail],i) slope(q[tail1],q[tail])sump<slope(q[tail],i) 的可能性。

      所以这么弹可能把答案取值点给弹出去。

    • 假设弹队尾的条件是 s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) ≥ s l o p e ( q [ t a i l ] , i ) slope(q[tail-1],q[tail])\ge slope(q[tail],i) slope(q[tail1],q[tail])slope(q[tail],i)

      因为 s l o p e ( q [ t a i l ] , i ) ≤ s l o p e ( q [ t a i l − 1 ] , q [ t a i l ] ) ≤ s u m p slope(q[tail],i)\le slope(q[tail-1],q[tail])\le sum_p slope(q[tail],i)slope(q[tail1],q[tail])sump

      所以 q [ t a i l − 1 ] q[tail-1] q[tail1] q [ t a i l ] q[tail] q[tail] 弹出后,马上 q [ t a i l ] q[tail] q[tail] 就会被 i i i 弹出去。

      那么 q [ t a i l ] q[tail] q[tail] 永远都不可能当队首。

    综上,我们确定了队尾判断的符号。

注意:前面的表述据说明 s u m i sum_i sumi 不是严格递增的,存在平台,所以遇到 s u m i = s u m j sum_i=sum_j sumi=sumj 返回斜率极小值即可。

code

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 100005
int n, k, d;
int a[maxn], sum[maxn], q[maxn];
int g[maxn][205], f[maxn][205];
double slope( int x, int y ) {
	if( sum[x] == sum[y] ) return -1e18;
	return (f[x][d] - sum[x] * sum[x] - f[y][d] + sum[y] * sum[y]) * 1.0 / (sum[y] - sum[x]);
}
void print( int n, int d ) {
	if( d == 1 ) return;
	print( g[n][d], d - 1 );
	printf( "%d ", g[n][d] );
}
signed main() {
	scanf( "%lld %lld", &n, &k );
	for( int i = 1;i <= n;i ++ ) scanf( "%lld", &a[i] );
	for( int i = 1;i <= n;i ++ ) sum[i] = sum[i - 1] + a[i];
	for( d = 1;d <= k;d ++ ) {
		int head = 1, tail = 0;
		for( int i = 1;i <= n;i ++ ) {
			while( head < tail and slope( q[head], q[head + 1] ) <= sum[i] ) head ++;
			f[i][d + 1] = f[q[head]][d] + sum[q[head]] * ( sum[i] - sum[q[head]] );
			g[i][d + 1] = q[head];
			while( head < tail and slope( q[tail - 1], q[tail] ) >= slope( q[tail], i ) ) tail --;
			q[++ tail] = i;
		}
	}
	printf( "%lld\n", f[n][d] );
	print( n, d );
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值