「Newcoder练习赛40D」小A与最大子段和

题目

挺好的一道题

我们考虑把\(i\)作为选取的最大子段的结束位置,我们如何往前计算贡献呢

考虑一下这个乘上其在队列中的位置可以表示为这个数被算了多少次,而我们往前扩展一位当前已经被扩展的就会被计算一次

\(s_i\)表示序列的前缀和

扩展一次

\[s_i-s_{i-1} \]

再扩展一次

\[s_i-s_{i-1}+s_i-s_{i-2} \]

发现如果我们往前算到第\(j\)项的话贡献就是

\[(i-j+1)\times s_i-\sum_{k=j-1}^{i-1}s_k \]

如果对前缀和序列在求一个前缀和,得到一个\(S\)序列就变成了这个柿子

\[i\times s_i-s_ij+s_i-S_{i-1}+S_{j-2} \]

发现只有

\[-s_ij+S_{j-2} \]

会影响我们的决策,所以考虑让这一项最大就好了

\[b=-s_ij+S_{j-2} \]

\[s_ij+b=S_{j-2} \]

这不是标准的斜率式吗,把\((j,S_{j-2})\)看成点,\(s_i\)看成斜率找到一个最优决策点就好了

自然凸壳上二分斜率了

第一次写这个东西,细节不少

代码

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define maxn 200005
#define re register
#define LL long long
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
LL s[maxn],pre[maxn];
LL ans=0;
int q[maxn],h=1,t,n;
inline LL X(int i) {return (LL)i;}
inline LL Y(int i) {return pre[((i-2)>0)?(i-2):0];}
inline double K(int a,int b) {
	if(X(a)==X(b)) return 9999999999;
	return (double)(Y(a)-Y(b))/(double)(X(a)-X(b));
}
inline void ins(int x) {
	while(h<t&&K(q[t-1],q[t])<K(q[t-1],x)) t--;
	q[++t]=x;
}
inline int find(LL x) {
	if(h==t) return q[t];
	if(h==t-1) {
		if(K(q[h],q[t])<x) return q[h];
		return q[t];
	}
	int l=h,r=t;
	while(l<=r) {
		if(l==r-1) {
			if(K(q[l],q[r])<x) return q[l];
			return q[r];
		}
		int mid=l+r>>1;
		if(mid+1>r) break;//细节1
		if(K(q[mid],q[mid+1])<x) r=mid;//细节2,不能将mid排除,这样在终止条件的时候就计算不到了
			else l=mid+1;
	}
	return q[t];
}
int main()
{
	scanf("%d",&n);
	for(re int i=1;i<=n;i++) scanf("%lld",&s[i]);
	for(re int i=1;i<=n;i++) s[i]+=s[i-1];
	for(re int i=1;i<=n;i++) pre[i]=pre[i-1]+s[i];
	ins(1);ans=s[1];
	for(re int i=2;i<=n;i++) {
		ins(i);int x=find(s[i]);
		ans=max(ans,(LL)(i+1)*s[i]-s[i]*x-pre[i-1]+pre[((x-2)>0)?(x-2):0]);
	}
	printf("%lld\n",ans);
	return 0;
}
posted @ 2019-02-17 19:55  asuldb  阅读(331)  评论(0编辑  收藏  举报