【APIO 2014】序列分割

本文深入探讨了一款关于非负整数序列的游戏算法,目标是通过特定操作将序列分割成多个块,以最大化最终得分。文章详细介绍了问题背景、解决方案及代码实现,包括如何使用动态规划和斜率优化技术来解决此问题。

传送门


Problem

你正在玩一个关于长度为 n n n 的非负整数序列的游戏。这个游戏中你需要把序列分成 ( k + 1 ) (k + 1) (k+1) 个非空的块。为了得到 ( k + 1 ) (k + 1) (k+1) 块,你需要重复下面的操作 k k k 次:

  1. 选择一个有超过一个元素的块(初始时你只有一块,即整个序列)。
  2. 选择两个相邻元素把这个块从中间分开,得到两个非空的块。

每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分。

数据范围: 0 ≤ a i ≤ 1 0 4 0≤a_i≤10^4 0ai104 2 ≤ n ≤ 100000 2≤n≤100000 2n100000 1 ≤ k ≤ m i n { n − 1 , 200 } 1≤k≤min\{n−1,200\} 1kmin{n1,200}


Solution

这篇博客写得很好,接下来有部分内容取自这里。

首先有一个结论,即当分割的位置确定时,分割的先后顺序对答案无影响。

下面是简单的说明:
如果我们有长度为 3 3 3 的序列 x , y , z x,y,z x,y,z 将其分为 3 3 3 部分,有如下两种分割方法:

  1. 先在 x x x 后面分割,答案为 x ( y + z ) + y z x(y+z)+yz x(y+z)+yz,即 x y + y z + z x xy+yz+zx xy+yz+zx
  2. 先在 y y y 后面分割,答案为 ( x + y ) z + x y (x+y)z+xy (x+y)z+xy,即 x y + y z + z x xy+yz+zx xy+yz+zx

这个结论可以扩展到任意长度的序列(分析一下贡献),证毕。

定义 F i , j F_{i,j} Fi,j 表示前 i i i 个数进行 j j j 次切割的最大得分。记 S i S_i Si 表示 a i a_i ai 的前缀和,那么转移方程为:

F i , k = max ⁡ j = 1 i − 1 { F j , k − 1 + S j ( S i − S j ) } F_{i,k}=\max_{j=1}^{i-1}\{F_{j,k−1}+S_j(S_i−S_j)\} Fi,k=j=1maxi1{Fj,k1+Sj(SiSj)}

发现 k k k 只会从 k − 1 k-1 k1 转移过来,因此可以用滚动数组来优化掉 k k k 这一维。

现在记 F i , k F_{i,k} Fi,k​ 为 f i f_i fi​, F j , k − 1 F_{j,k−1} Fj,k1​ 为 g j g_j gj​,那么方程为:

f i = max ⁡ j = 1 i − 1 g j + S j ( S i − S j ) f_i=\max_{j=1}^{i-1}{g_j+S_j(S_i−S_j)} fi=j=1maxi1gj+Sj(SiSj)

感觉是一个可以斜率优化的式子!

按照套路,我们取 j , k j,k j,k 满足 1 ≤ k &lt; j &lt; i 1≤k&lt;j&lt;i 1k<j<i j j j k k k 更优,那么有如下不等式:

g j + S j ( S i − S j ) &gt; g k + S k ( S i − S k ) g_j+S_j(S_i−S_j)&gt;g_k+S_k(S_i−S_k) gj+Sj(SiSj)>gk+Sk(SiSk)

化个简,得到:

( g j − S j 2 ) − ( g k − S k 2 ) S k − S j &lt; S i \frac{(g_j-S_j^2)-(g_k-S_k^2)}{S_k-S_j}&lt;S_i SkSj(gjSj2)(gkSk2)<Si

维护一个下凸壳然后斜率优化即可。

注:本题中 a i a_i ai 是非负整数,所以 S k − S j S_k−S_j SkSj 可能等于 0 0 0。这种情况需要特判, s l o p e slope slope 需要返回 − i n f −inf inf

然后输出方案的话直接记一个 p r e pre pre 表示从哪转移过来即可。

时间复杂度: O ( n k ) O(nk) O(nk)


Code

#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 100005
#define ll long long
using namespace std;
ll f[N],g[N];
int S[N],Q[N],pre[N][205];
ll ordi(int x)  {return g[x]-1ll*S[x]*S[x];}
double slope(int x,int y){
	return (S[x]==S[y])?-1e20:1.0*(ordi(x)-ordi(y))/(S[y]-S[x]);
}
int main(){
	int n,k,i,j,x;
	scanf("%d%d",&n,&k);
	for(i=1;i<=n;++i)
		scanf("%d",&x),S[i]=S[i-1]+x;
	for(j=1;j<=k;++j){
		int l=0,r=0;
		for(i=1;i<=n;++i){
			while(l<r&&slope(Q[l],Q[l+1])<=S[i])  l++;
			f[i]=g[Q[l]]+1ll*S[Q[l]]*(S[i]-S[Q[l]]),pre[i][j]=Q[l];
			while(l<r&&slope(Q[r-1],Q[r])>=slope(Q[r],i))  r--;
			Q[++r]=i;
		}
		memcpy(g,f,sizeof(g));
	}
	printf("%lld\n",f[n]);
	for(x=n,i=k;i>=1;--i)  x=pre[x][i],printf("%d ",x);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值