【题解】洛谷P5504 [JSOI2011]柠檬 斜率DP

给定长为 n n n(1e5)的数列 s s s(1e4),将其分成若干连续段。
可以从一段中任选一个数 s 0 s_0 s0,这一段的贡献就是 c n t [ s 0 ] 2 ∗ s 0 cnt[s_0]^2*s_0 cnt[s0]2s0,其中 c n t [ s 0 ] cnt[s_0] cnt[s0]表示这一段中 s 0 s_0 s0出现的次数。
求所有段的贡献之和的最大值。


一个基础的dp:

d i d_i di表示把前 i i i个数分好的最大贡献, d 0 = 0 d_0=0 d0=0.
d i = m a x { d j − 1 + c a l ( j , i ) } d_i = max\{d_{j-1}+cal(j,i)\} di=max{dj1+cal(j,i)},其中 0 < j < = i 0<j<=i 0<j<=i c a l ( a , b ) cal(a,b) cal(a,b)表示把 a a a b b b分成一段的最大贡献。

一个明显的问题是 c a l ( j , i ) cal(j,i) cal(j,i)难以计算,强行计算的复杂度可能达到 O ( n 3 ) O(n^3) O(n3)


结论:最优情况下,每一段的首尾数字必定相同,而且作为 s 0 s_0 s0
证明:如果存在一个段的首部数字不是这个段的 s 0 s_0 s0,那么可以将其独立成段,贡献显然增加。尾部同理。

n n n个位置按值分组,记 p i , j p_{i,j} pi,j表示数值 i i i在原数列中第 j j j次(从第0次开始算)出现的位置,那么有: s [ p i , j ] = i s[p_{i,j}]=i s[pi,j]=i

此时对于位置 p i , j p_{i,j} pi,j,只应该从 p i , j − 1 、 p i , j − 1 − 1 、 p i , j − 2 − 1... 、 p i , 0 − 1 p_{i,j}-1、p_{i,j-1}-1、p_{i,j-2}-1...、p_{i,0}-1 pi,j1pi,j11pi,j21...pi,01去转移,且数值 i i i出现的次数很容易算出,即:
d [ p i , j ] = m a x { d [ p i , k − 1 ] + i ∗ ( j − k + 1 ) 2 } d[p_{i,j}] = max\{d[p_{i,k}-1]+i*(j-k+1)^2\} d[pi,j]=max{d[pi,k1]+i(jk+1)2} 0 < = k < = j 0<=k<=j 0<=k<=j

保存每个位置对应的 i , j i,j i,j值,按 p i , j p_{i,j} pi,j递增的顺序转移即可。

此时复杂度并不能完全满足需求,如果所有数值都相同还是会卡到 O ( n 2 ) O(n^2) O(n2),还需要进一步优化。

int xi[M], xj[M]; 
vector<int> pos[10016];
ll dp[M];
int main()
{
	int n = read();
	for(int p=1; p<=n; ++p)
	{
		xi[p] = read();
		xj[p] = pos[xi[p]].size();
		pos[xi[p]].push_back(p);
	}
	for(int p=1; p<=n; ++p)
	{	
		int i = xi[p], j = xj[p]; 
		for(int k=0; k<=j; ++k)
			dp[p] = max(dp[p], dp[pos[i][k]-1] + 1ll*i*(j-k+1)*(j-k+1));
	}
	cout << dp[n] << "\n";
}

开O2直接AC了你敢信


以下内容需要斜率优化作为前置知识,可以自行了解或者察看我的博客qwq.

对于转移式: d [ p i , j ] = m a x { d [ p i , k − 1 ] + i ∗ ( j − k + 1 ) 2 } d[p_{i,j}] = max\{d[p_{i,k}-1]+i*(j-k+1)^2\} d[pi,j]=max{d[pi,k1]+i(jk+1)2}

整理得到: d [ p i , j ] = i ( j + 1 ) 2 + m a x { d [ p i , k − 1 ] + i k 2 − 2 i ( j + 1 ) k } d[p_{i,j}] = i(j+1)^2 +max\{d[p_{i,k}-1]+ik^2-2i(j+1)k\} d[pi,j]=i(j+1)2+max{d[pi,k1]+ik22i(j+1)k}

a < b < j a<b<j a<b<j,那么从 d [ p i , b − 1 ] d[p_{i,b}-1] d[pi,b1]转移比从 d [ p i , a − 1 ] d[p_{i,a}-1] d[pi,a1]转移更优等价于

d [ p i , b − 1 ] + i b 2 − 2 i ( j + 1 ) b > d [ p i , a − 1 ] + i a 2 − 2 i ( j + 1 ) a d[p_{i,b}-1]+ib^2-2i(j+1)b>d[p_{i,a}-1]+ia^2-2i(j+1)a d[pi,b1]+ib22i(j+1)b>d[pi,a1]+ia22i(j+1)a

整理得到: ( d [ p i , b − 1 ] + i b 2 ) − ( d [ p i , a − 1 ] + i a 2 ) b − a > 2 i ( j + 1 ) \frac{(d[p_{i,b}-1]+ib^2) - (d[p_{i,a}-1]+ia^2)}{b-a}>2i(j+1) ba(d[pi,b1]+ib2)(d[pi,a1]+ia2)>2i(j+1)

由斜率优化原理, y a = d [ p i , a − 1 ] + i a 2 , x a = a , k = 2 i ( j + 1 ) y_a=d[p_{i,a}-1]+ia^2,x_a=a,k=2i(j+1) ya=d[pi,a1]+ia2xa=ak=2i(j+1)。大于号,维护上凸包。上凸包斜率递减,目标斜率递增,队尾操作。本质上是一个单调栈。

注意,需要给每一个 i i i值都维护一个独立的凸包。


实现细节:

  1. 单调栈中可以存放实际位置, i i i j j j可以由推导得到。
  2. 此题中求 d p [ p ] dp[p] dp[p]时需要把 p p p放入凸包中,所以操作顺序应该是:
    1. 准备放入p,把构成下凸点的栈顶弹出
    2. 放入p
    3. 更新目标斜率,把不满足目标斜率的栈顶弹出
    4. 求dp[p]

AC代码如下:

ll xi[M], xj[M]; 
vector<int> pos[10016];
ll dp[M];

vector<int> ms[10016]; //单调栈
inline int t1(const vector<int> &vc){return vc[vc.size()-1];} //栈顶的第一个元素
inline int t2(const vector<int> &vc){return vc[vc.size()-2];} //栈顶的第二个元素
inline ll subx(int p1, int p2){return xj[p2]-xj[p1];}
inline ll suby(int p1, int p2){
	return (dp[p2-1] + xi[p2]*xj[p2]*xj[p2]) - (dp[p1-1] + xi[p1]*xj[p1]*xj[p1]);
}
inline ll cal(int p, int lp){
	return dp[lp-1] + xi[p]*(xj[p]-xj[lp]+1)*(xj[p]-xj[lp]+1);
}
int main(void)
{
	int n = read();
	for(int p=1; p<=n; ++p)
	{
		xi[p] = read();
		xj[p] = pos[xi[p]].size();
		pos[xi[p]].push_back(p);
	}

	for(int p=1; p<=n; ++p)
	{	
		int i = xi[p], j = xj[p]; 
		while(ms[i].size()>=2 &&
			suby(t2(ms[i]),t1(ms[i]))*subx(t1(ms[i]),p) <= subx(t2(ms[i]),t1(ms[i]))*suby(t1(ms[i]),p)
		) ms[i].pop_back(); 

		ms[i].push_back(p);

		while(ms[i].size()>=2 && suby(t2(ms[i]),t1(ms[i])) <= 2ll*i*(j+1)*subx(t2(ms[i]),t1(ms[i])))
			ms[i].pop_back();

		dp[p] = cal(p, t1(ms[i]));
	}
	cout << dp[n] << "\n";

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值