【杂题】[BZOJ4709]【JSOI2011】柠檬

本文介绍了一种解决最大收益分割序列问题的算法,通过动态规划和单调栈优化,实现O(nlogn)的时间复杂度。关键在于理解最优解的性质,即将序列分割成多个子段,每个子段首尾相同,收益由子段中特定数的出现次数决定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Description

有一个长度为n的序列a
你需要这个序列分成若干段,每个段可以任意指定一个数t,设v为t在这段中出现的次数,这一段的收益就是v2∗tv^2*tv2t
求最大的总收益和
n≤100000,ai≤10000n\leq 100000,ai\leq 10000n100000,ai10000

Solution

显而易见的是,最优情况下任何一段的开头和结尾的数都是相同的,且都是我们对这一段指定的这个数,否则不妨让他自成一段都会更优。

那么考虑DP
F[i]F[i]F[i]表示已经做完了前i个数,最后一段以位置i结尾的最大收益

考虑转移,假设它要从位置x转移而来,那么∀x≤i,a[x]=a[i],F[i]=max⁡((cnt[i]−cnt[x]+1)2∗a[i]+F[x−1])\forall x\leq i,a[x]=a[i],F[i]=\max\left((cnt[i]-cnt[x]+1)^2*a[i]+F[x-1]\right)xi,a[x]=a[i],F[i]=max((cnt[i]cnt[x]+1)2a[i]+F[x1])

其中cnt[i]cnt[i]cnt[i]为1到i中a[i]出现次数

我们发现,对于相同的a,总是越往前的增长越快。

也就是说决策是具有单调性的。

我们只考虑相同的a,建立直角坐标系设横坐标为出现次数,纵坐标为贡献。

那么一个决策(x)对应在坐标系中就是一个开口向上的二次函数
对于相同的a,我们可以用一个单调栈,来维护这个东西(类比单调队列维护下凸壳)
明显越往栈顶它的增长越慢,只要更下面的决策在某一个时间点优于上面的决策,那以后一直都是更优的。

在i入栈前,考虑栈顶决策对应的曲线与栈顶下一个决策对应的曲线的交点(即栈顶下一个决策何时优于栈顶决策)和栈顶决策曲线与决策i曲线的交点

若前者时间比后者时间更靠前,说明栈顶决策没用了(要么就是决策i更优,要么就是栈顶下一个更优了),此时将栈顶弹掉。
求两个决策的交点可以采用二分。
弹到不能弹为止,将i入栈

此时还需要判断栈顶下一个决策在当前x=cnt[i]横坐标是否已经超过了栈顶,是的话将栈顶弹掉,一直弹到不能弹为止。

这时直接取栈顶转移即可
总的复杂度是O(nlog⁡n)O(n\log n)O(nlogn)

Code

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
#include <vector>
#define fo(i,a,b) for(int i=a;i<=b;++i)
#define fod(i,a,b) for(int i=a;i>=b;--i)
#define N 100005
#define LL long long
using namespace std;
LL f[N],a[N],cnt[N];
int le[10005],n,c1[10005];
vector<int>d[10005];
inline LL sqr(LL x)
{
	return x*x;
}
LL get(int x,int i)
{
	return f[x-1]+sqr(i-cnt[x]+1)*a[x];
}
LL fd(int x,int i)
{
	int l=1,r=n+1;
	while(l+1<r)
	{
		int mid=(l+r)>>1;
		if(get(x,mid)>=get(i,mid)) r=mid;
		else l=mid;
	}
	if(get(x,l)>=get(i,l)) return l;
	else return r;
}
int main()
{
	cin>>n;
	fo(i,1,n)
	{
		scanf("%lld",&a[i]);
		cnt[i]=++c1[a[i]];
		while(le[a[i]]>1&&fd(d[a[i]][le[a[i]]-2],d[a[i]][le[a[i]]-1])<=fd(d[a[i]][le[a[i]]-1],i))
		{
			le[a[i]]--,d[a[i]].pop_back();
		}
		++le[a[i]];
		d[a[i]].push_back(i);
		int x=d[a[i]][le[a[i]]-1],y;
		while(le[a[i]]>1)
		{
			y=d[a[i]][le[a[i]]-2];
			if(get(x,cnt[i])<=get(y,cnt[i])) le[a[i]]--,d[a[i]].pop_back();
			else break;
			x=y;
		}
		f[i]=get(x,cnt[i]);
	}
	printf("%lld\n",f[n]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值