「APIO2014」序列分割 斜率优化

「APIO2014」序列分割

Part 0(P10的写法,巨佬请忽略)

可以发现其实只用知道分成那几个块即可,不用知道中途分裂的顺序。

记R[i]为第i个元素后面最近的裂痕在哪个元素的后面(若这个元素后面无裂痕就为n+1),sum[i]为1~i的元素的和,那么第i个元素对答案的贡献就是 v a l [ i ] ∗ ( s u m [ n ] − s u m [ R [ i ] ] ) val[i]*(sum[n]-sum[R[i]]) val[i](sum[n]sum[R[i]])。答案就是每个元素对答案的贡献之和。

故第一档的写法就是二进制枚举裂痕+算答案。

Part 1(P4的写法)

这一档和上一档的方法又一样。不过这档是正解的基础。

我们从左到右枚举裂痕。此时我们只用知道上一个裂痕在哪就可算出本次分裂所增加的答案。这个可以用动态规划。我们定义 d p [ i ] [ j ] dp[i][j] dp[i][j]的含义为第j条裂痕(从左向右数第j条)在i这个位置,分裂成j+1个区间时产生的最大答案。

状态转移方程为:
d p [ n o w ] [ c n t ] = max ⁡ ( d p [ L s t ] [ c n t − 1 ] + ( s u m [ n o w ] − s u m [ L s t ] ) ∗ ( s u m [ n ] − s u m [ n o w ] ) ) dp[now][cnt]=\max(dp[Lst][cnt-1]+(sum[now]-sum[Lst])*(sum[n]-sum[now])) dp[now][cnt]=max(dp[Lst][cnt1]+(sum[now]sum[Lst])(sum[n]sum[now]))
记录方案就是记录这个状态从哪个状态转移而来,输出时就一直往回跳即可。

代码:

long long dp[1005][205];
int Pre[1005][205];
long long sum[1005];
long long Get_sum(int L,int R){
	return sum[L]-sum[R+1];
}
void Solve(){
	memset(Pre,0,sizeof(Pre));
	long long ans=0;
	int ans_step;
	sum[n+1]=0;
	for(int i=n;i>=1;i--)sum[i]=sum[i+1]+val[i];//这里是处理后缀和 
	memset(dp,0,sizeof(dp));
	for(int now=1;now<n;now++){
		for(int cnt=0;cnt<K;cnt++){
			bool flag=false;//这个主要是怕有一个答案==0 
			for(int Lst=cnt==0?0:now-1;Lst>=0;Lst--){//枚举上一个裂痕在哪 
				long long nxt_ans=dp[Lst][cnt]+1ll*Get_sum(Lst+1,now)*sum[now+1];//计算答案 
				if(nxt_ans>dp[now][cnt+1]||!flag){
					dp[now][cnt+1]=nxt_ans;
					Pre[now][cnt+1]=Lst;
				}
				flag=true;
				if(cnt==K-1&&dp[now][cnt+1]>ans){
					ans=dp[now][cnt+1];
					ans_step=now;
				}
			}
		}
	}
	printf("%lld\n",ans);
	for(int i=K;i>=1;i--){
		printf("%d ",ans_step);
		ans_step=Pre[ans_step][i];
	}
	puts("");
}

Part2 正解

我们观察那个式子,把它转化一下。
d p [ n o w ] [ c n t ] = max ⁡ ( d p [ L s t ] [ c n t − 1 ] − s u m [ L s t ] ∗ ( s u m [ n ] − s u m [ n o w ] ) ) + s u m [ n o w ] ∗ ( s u m [ n ] − s u m [ n o w ] ) dp[now][cnt]=\max(dp[Lst][cnt-1]-sum[Lst]*(sum[n]-sum[now]))+sum[now]*(sum[n]-sum[now]) dp[now][cnt]=max(dp[Lst][cnt1]sum[Lst](sum[n]sum[now]))+sum[now](sum[n]sum[now])
如果我们进行一下定义
s u m [ L s t ] = x k = s u m [ n ] − s u m [ n o w ] y = d p [ L s t ] [ c n t − 1 ] 那 么 由 于 b = y − k x d p [ n o w ] [ c n t ] = m a x ( b ) + s u m [ n o w ] ∗ ( s u m [ n ] − s u m [ n o w ] ) sum[Lst]=x\\ k=sum[n]-sum[now]\\ y=dp[Lst][cnt-1]\\ 那么由于b=y-kx\\ dp[now][cnt]=max(b)+sum[now]*(sum[n]-sum[now]) sum[Lst]=xk=sum[n]sum[now]y=dp[Lst][cnt1]b=ykxdp[now][cnt]=max(b)+sum[now](sum[n]sum[now])
然后就可以用斜率优化了。(斜率优化的入门)。本题要维护一个上凸包(斜率递减)(可分类讨论发现下凸包中间的点都没有两边的点更优)。对于点A和点B(假设点A的x比B小),只有当A–B的斜率大于k时B才比A更优。

由于本题枚举是k单调递减。故当B比A更优之后,只后的k不会使A比B更优,故可以维护一个单调队列一直弹左端点即可。

代码:(有解释)

#include<cstdio>
#include<algorithm>
#include<cstring>
#define M 100005
#define lowbit(x) x&-x
using namespace std;
int n,K;
int val[M];
struct node{
	int id;
	long long x,y;
};
int Pre[M][205];//计算这个状态是由哪个状态转移而来 
long long sum[M],dp[2][M];//这里用滚动数组 
node stk[M];
int L,R;
long long Calc(node A,long long k){//计算b(截距) 
	return 1ll*A.y-1ll*A.x*k;
}
bool check(node A,node B,node C){//如果A--C的斜率大于A--B的斜率 
	return 1.0*(C.y-A.y)*(B.x-A.x)>=1.0*(B.y-A.y)*(C.x-A.x);
}
void Push(node New){//加入一个新的点 
	while(L<R&&check(stk[R-1],stk[R],New))R--;//维护上凸包 
	stk[++R]=New;
}
bool cmp(node A,node B,long long K){//B比A更优 
	return 1ll*A.y-K*A.x<=1ll*B.y-K*B.x;
}
int Get_mx(long long K){
	while(L<R&&cmp(stk[L],stk[L+1],K))L++;//弹左端点 
	return L;
}
void Solve(){
	long long ans=0;
	int ans_step;
	sum[0]=0;
	for(int i=1;i<=n;i++)sum[i]=sum[i-1]+val[i];//前缀和 
	for(int i=1;i<=n;i++){
		dp[0][i]=1ll*sum[i]*(sum[n]-sum[i]);
		if(K==1)ans=max(ans,dp[0][i]),ans_step=i;//特判K=1的情况 
	}
	for(int cnt=2,cur=0;cnt<=K;cnt++,cur=!cur){
		L=R=0;
		Push((node){cnt-1,sum[cnt-1],dp[cur][cnt-1]});//由于放cnt-1个裂痕时最后一个裂痕只能放在第cnt-1个位置之后的位置 
		for(int now=cnt;now<n;now++){
			int flag=Get_mx(sum[n]-sum[now]);//这里返回的是一个id 
			Pre[now][cnt]=stk[flag].id;
			dp[!cur][now]=Calc(stk[flag],sum[n]-sum[now])+1ll*(sum[n]-sum[now])*sum[now];//计算答案 
			Push((node){now,sum[now],dp[cur][now]});
			if(ans<dp[!cur][now]&&cnt==K){//更新答案 
				ans_step=now;
				ans=dp[!cur][now];
			}
		}
	}
	printf("%lld\n",ans);
	for(int i=K;i>=1;i--){//输出方案 
		printf("%d ",ans_step);
		ans_step=Pre[ans_step][i];
	}
	puts("");
}
int main(){
	scanf("%d%d",&n,&K);
	for(int i=1;i<=n;i++)scanf("%d",&val[i]);
	Solve();
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值