这是一个很菜的 Oier 的博客|

Hanx16Msgr

园龄:2年8个月粉丝:12关注:3

2022-10-10 15:14阅读: 24评论: 0推荐: 0

P3648 [APIO2014] 序列分割

[APIO2014] 序列分割

Luogu P3648

题目描述

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

选择一个有超过一个元素的块(初始时你只有一块,即整个序列)

选择两个相邻元素把这个块从中间分开,得到两个非空的块。

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

输入格式

第一行包含两个整数 nk。保证 k+1n

第二行包含 n 个非负整数 a1,a2,,an (0ai104),表示前文所述的序列。

输出格式

第一行输出你能获得的最大总得分。

第二行输出 k 个介于 1n1 之间的整数,表示为了使得总得分最大,你每次操作中分开两个块的位置。第 i 个整数 si 表示第 i 次操作将在 sisi+1 之间把块分开。

如果有多种方案使得总得分最大,输出任意一种方案即可。

样例 #1

样例输入 #1

7 3
4 1 3 4 0 2 3

样例输出 #1

108
1 3 5

提示

你可以通过下面这些操作获得 108 分:

初始时你有一块 (4,1,3,4,0,2,3)。在第 1 个元素后面分开,获得 4×(1+3+4+0+2+3)=52 分。

你现在有两块 (4),(1,3,4,0,2,3)。在第 3 个元素后面分开,获得 (1+3)×(4+0+2+3)=36 分。

你现在有三块 (4),(1,3),(4,0,2,3)。在第 5 个元素后面分开,获得 (4+0)×(2+3)=20 分。

所以,经过这些操作后你可以获得四块 (4),(1,3),(4,0),(2,3) 并获得 52+36+20=108 分。

限制与约定

第一个子任务共 11 分,满足 1k<n10

第二个子任务共 11 分,满足 1k<n50

第三个子任务共 11 分,满足 1k<n200

第四个子任务共 17 分,满足 2n1000,1kmin{n1,200}

第五个子任务共 21 分,满足 2n10000,1kmin{n1,200}

第六个子任务共 29 分,满足 2n100000,1kmin{n1,200}

感谢@larryzhong 提供的加强数据

Solution

首先需要知道的是,分割的先后顺序并不会影响答案,比如题目中的分割方式是 1 3 5,如果按照 5 3 1 分割,得到的答案是相同的,感兴趣的话可以尝试证明一下。

因此设 fi,k 表示前 i 个分割 k 次得到的最大分数,预处理一下前缀和 si=i=1iai,那么可以推出 fi,k 的转移方程:

fi,k=maxj[1,i1]{fj,k1+sj×(sisj)}

注意到转移的时候 fi,k 只与 fj,k1 相关,因此可以把 k 那一维用滚动数组滚掉,减小空间开销。

但是这样虽然空间变成了 O(n) 的,时间仍然是 O(n2k) 的,没法通过这道题,因此考虑如何优化。

观察 max 内的式子,将其进行变形(k 那一维被滚掉了,因此直接省略掉):

fj+sj×(sisj)=fj+sj×sisj2

因为我们要取一个最优的 j 来进行转移,因此设一个 t<j 使得:

ft+stsist2>fj+sjsisj2

对此不等式变形:

ft+stsist2>fj+sjsisj2(ftst2)+(fjsj2)>(sjst)si(ftst2)+(fjsj2)(sjst)>si(ftst2)+(fjsj2)st(sj)>si

注意到不等号左边可以变成类似于斜率公式 k=ΔyΔx 的形式,因此建出平面直角坐标系,坐标系中的每一个点 P(si,fisi2) 表示,上述式子就可以变成两点之间连线的斜率,用单调队列维护下凸壳即可。

需要注意的是,因为每一个数是非负的,因此 st(sj) 的值可能为 0,因此需要特判一下,令此时的斜率为 inf

对于方案的输出,只需要在转移的时候记录下每次转移的来源即可,输出的时候一路循着输出即可。

时间复杂度是 O(nk)

此题有点卡常,对于滚动数组最好不要在每次滚动的时候模拟滚动的过程 O(n) 地赋值,直接将当前数组和上一个数组的指针交换就可以了。

Code

#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
using namespace std;
template<typename T> void read(T &k)
{
	k=0;T flag=1;char b=getchar();
	while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
	while (isdigit(b)) {k=k*10+b-48;b=getchar();}
	k*=flag;
}
template<typename T> void write(T k) {if (k<0) {putchar('-'),write(-k);return;}if (k>9) write(k/10);putchar(k%10+48);}
template<typename T> void writewith(T k,char c) {write(k);putchar(c);}
const int _SIZE=1e5,INF=1e18;
int k,n;
int a[_SIZE+5],s[_SIZE+5],fa[205][_SIZE+5];
int f[2][_SIZE+5],cur=0,last=1;//f用来滚动,cur指向当前dp数组,last指向上一个dp数组
int q[_SIZE+5],head=1,tail=1;//单调队列(双端队列),head,tail前闭后开
double slope(int x,int y)//求两点斜率
{
	int x1=-s[x],y1=f[last][x]-s[x]*s[x];
	int x2=-s[y],y2=f[last][y]-s[y]*s[y];
	if (x1==x2) return -INF;
	return (y2-y1)*1.0/(x2-x1);
}
signed main()
{
	read(n),read(k);
	for (int i=1;i<=n;i++) read(a[i]),s[i]=a[i]+s[i-1];
	for (int t=1;t<=k;t++)
	{
		cur^=1,last^=1;
		head=tail=0;
		for (int i=1;i<=n;i++)
		{
			while (tail-head>=2 && slope(q[head],q[head+1])<=s[i]) head++;//踢队头
			f[cur][i]=0;
			if (head<tail)//可以转移
			{
				int j=q[head];fa[t][i]=j;//记录来源
				f[cur][i]=f[last][j]+s[j]*(s[i]-s[j]);//按照方程转移
			}
			while (tail-head>=2 && slope(q[tail-1],q[tail-2])>=slope(i,q[tail-1])) tail--;//踢队尾
			q[tail++]=i;//入队
		}
	}
	writewith(f[cur][n],'\n');
	for (int x=fa[k][n];k;x=fa[--k][x]) writewith(x,' ');//输出方案
	return 0;
}
posted @   Hanx16Msgr  阅读(24)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起