[lnsyoj538/luoguP3628/APIO2010]特别行动队
题意
原题链接
给定序列 \(a\) 和自定义二次函数 \(f(x) = ax^2 + bx + c(a<0)\),要求将 \(a\) 分为几段(不妨设为 \(k\) 段),使得 \(\sum_{i=1}^{k} f(\sum_{j=l_i}^{r_i}a_j)\)的值最大,求最大的值
sol
设计状态转移方程。显然,\(dp_i\) 可以由 \(dp_j\) 转移当且仅当 \(j < i\),这表示从第 \(j + 1\) 到第 \(i\) 个数都被分为一段。容易得到(设 \(S_i = \sum_{j=1}^i a_j\)
本题复杂度较高,无法使用 \(O(n^2)\) 的朴素方法 AC。
对于此类题目,我们可以采用斜率优化 DP 的方式优化。
由于在计算时,\(i\) 是确定的,因此 \(dp_i\) 及 \(S_i\) 均可视为常数。唯二的变量为 \(dp_j\) 和 \(S_j\),这启示我们将其变形成函数的形式,最终可以得到:
参考一次函数的 \(y=kx+b\),我们可以得到
由于我们最终要求的是最大的 \(dp_i\),即求出 \(b\) 最大,因此我们要求的就是使截距最大的 \(j\) 值。
同时,我们也可以据此求出有序实数对 \((S_j, dp_j + S_j^2)\),从而在平面直角坐标系中表示出这些点
参考单调队列思想,我们可以找出永远不会作为最大值的所有点,此时,剩余的所有点一定会在这些点的上凸包(最小值为下凸包)上
因此,我们只需要维护这个凸包就可以了,计算的时候,结果即为第一个斜率小于 \(k\) 的线段的左侧点
对于此题,由于 \(x,y\) 的单调性,我们可以维护一个双向队列,每次当队头线段的斜率 \(>k\) 时,就弹出队头;加入该点后不满足凸包性质,就弹出队尾;
代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1000005;
LL f[N], s[N], g[N], q[N];
int n, a, b, c;
LL x(int u){
return s[u];
}
LL y(int u){
return f[u] + a * s[u] * s[u];
}
int main(){
scanf("%d%d%d%d", &n, &a, &b, &c);
for (int i = 1; i <= n; i ++ ) scanf("%lld", &g[i]), s[i] = s[i - 1] + g[i];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ ) {
while (hh < tt && y(q[hh + 1]) - y(q[hh]) >= (2 * a * s[i] + b) * (x(q[hh + 1]) - x(q[hh]))) hh ++ ;
int j = q[hh];
f[i] = f[j] + a * (s[i] - s[j]) * (s[i] - s[j]) + b * (s[i] - s[j]) + c;
while (hh < tt && (y(q[tt]) - y(q[tt - 1])) * (x(i) - x(q[tt])) <= (y(i) - y(q[tt])) * (x(q[tt]) - x(q[tt - 1]))) tt -- ;
q[ ++ tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}
蒟蒻犯的若至错误
- 推式子把 \(aS_j^2\) 的系数推没了(