P3648 [APIO2014] 序列分割
[APIO2014] 序列分割
题目描述
你正在玩一个关于长度为 \(n\) 的非负整数序列的游戏。这个游戏中你需要把序列分成 \(k + 1\) 个非空的块。为了得到 \(k + 1\) 块,你需要重复下面的操作 \(k\) 次:
选择一个有超过一个元素的块(初始时你只有一块,即整个序列)
选择两个相邻元素把这个块从中间分开,得到两个非空的块。
每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分。
输入格式
第一行包含两个整数 \(n\) 和 \(k\)。保证 \(k + 1 \leq n\)。
第二行包含 \(n\) 个非负整数 \(a_1, a_2, \cdots, a_n\) \((0 \leq a_i \leq 10^4)\),表示前文所述的序列。
输出格式
第一行输出你能获得的最大总得分。
第二行输出 \(k\) 个介于 \(1\) 到 \(n - 1\) 之间的整数,表示为了使得总得分最大,你每次操作中分开两个块的位置。第 \(i\) 个整数 \(s_i\) 表示第 \(i\) 次操作将在 \(s_i\) 和 \(s_{i + 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 \times (1 + 3 + 4 + 0 + 2 + 3) = 52\) 分。
你现在有两块 \((4), (1, 3, 4, 0, 2, 3)\)。在第 \(3\) 个元素后面分开,获得 \((1 + 3) \times (4 + 0 + 2 + 3) = 36\) 分。
你现在有三块 \((4), (1, 3), (4, 0, 2, 3)\)。在第 \(5\) 个元素后面分开,获得 \((4 + 0) \times (2 + 3) = 20\) 分。
所以,经过这些操作后你可以获得四块 \((4), (1, 3), (4, 0), (2, 3)\) 并获得 \(52 + 36 + 20 = 108\) 分。
限制与约定
第一个子任务共 11 分,满足 \(1 \leq k < n \leq 10\)。
第二个子任务共 11 分,满足 \(1 \leq k < n \leq 50\)。
第三个子任务共 11 分,满足 \(1 \leq k < n \leq 200\)。
第四个子任务共 17 分,满足 \(2 \leq n \leq 1000, 1 \leq k \leq \min\{n - 1, 200\}\)。
第五个子任务共 21 分,满足 \(2 \leq n \leq 10000, 1 \leq k \leq \min\{n - 1, 200\}\)。
第六个子任务共 29 分,满足 \(2 \leq n \leq 100000, 1 \leq k \leq \min\{n - 1, 200\}\)。
感谢@larryzhong 提供的加强数据
Solution
首先需要知道的是,分割的先后顺序并不会影响答案,比如题目中的分割方式是 1 3 5
,如果按照 5 3 1
分割,得到的答案是相同的,感兴趣的话可以尝试证明一下。
因此设 \(f_{i,k}\) 表示前 \(i\) 个分割 \(k\) 次得到的最大分数,预处理一下前缀和 \(s_i=\displaystyle \sum \limits_{i=1}^{i} a_i\),那么可以推出 \(f_{i,k}\) 的转移方程:
注意到转移的时候 \(f_{i,k}\) 只与 \(f_{j,k-1}\) 相关,因此可以把 \(k\) 那一维用滚动数组滚掉,减小空间开销。
但是这样虽然空间变成了 \(\mathcal O(n)\) 的,时间仍然是 \(\mathcal O(n^2k)\) 的,没法通过这道题,因此考虑如何优化。
观察 \(\max\) 内的式子,将其进行变形(\(k\) 那一维被滚掉了,因此直接省略掉):
因为我们要取一个最优的 \(j\) 来进行转移,因此设一个 \(t<j\) 使得:
对此不等式变形:
注意到不等号左边可以变成类似于斜率公式 \(k=\displaystyle\frac{\Delta y}{\Delta x}\) 的形式,因此建出平面直角坐标系,坐标系中的每一个点 \(P\) 用 \((-s_i,f_i-s_i^2)\) 表示,上述式子就可以变成两点之间连线的斜率,用单调队列维护下凸壳即可。
需要注意的是,因为每一个数是非负的,因此 \(-s_t-(-s_j)\) 的值可能为 \(0\),因此需要特判一下,令此时的斜率为 \(-\inf\)。
对于方案的输出,只需要在转移的时候记录下每次转移的来源即可,输出的时候一路循着输出即可。
时间复杂度是 \(\mathcal O(nk)\)。
此题有点卡常,对于滚动数组最好不要在每次滚动的时候模拟滚动的过程 \(\mathcal 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;
}