SP15648 APIO10A - Commando 题解
题目传送门
题目描述
给定一个长度为 \(n\) 的序列,把它划分成至少一个连续的子段,每个子段的权值为该段的一个二次函数,求最大的权值和。
思路:
这种序列问题的状态通常是一维的,所以我们定义 \(dp(i)\) 为已经划分完前 \(i\) 项且最后在 \(i\) 处划分能获得的最大权值和。
对于每个 \(i\),我们已经确定它为划分的右端点,所以需要枚举划分的左端点 \(j\)。
对于一维线性 dp 问题,最好把它拿到一条直线上来看。
从图中可以看出,\(f(i)\) 的计算与两部分有关,一是前 \(j\) 项能获得的最大权值和,二是 \(j + 1\sim i\) 这一段的权值,而第一部分就是 \(dp(j)\),第二部分是区间 \([j + 1, i]\) 的和。
综上所述,状态转移方程为:
其中 \(f(i)\) 为题中的二次函数,\(sum_i\) 表示前 \(i\) 项的前缀和。
对于单组数据时间复杂度为 \(O(n^2)\),无法通过此题,考虑优化。
去掉 \(\min\) 函数,将 \(f(sum_i - sum_j)\) 函数展开并整理,得:
发现方程中含有 \(i,j\) 的乘积式,可以考虑斜率优化。
将只含 \(i\) 的项和只含 \(j\) 的项丢到等号两边,把 \(i,j\) 的乘积式丢到 \(i\) 那边,得:
这就构成了斜率优化的基本形式,令 \(Y = dp_j + a \cdot sum_j^2, X = sum_j\),就可以快乐地开始斜优啦。
首先发现斜率 \(k = 2 \cdot a \cdot sum_i + b\),由于士兵的初始战斗力为正整数,所以前缀和 \(sum\) 数组是单调递增的,所以我们可以得到两个性质:
- 由于二次函数的 \(a < 0\),所以斜率 \(k\) 单调递减;
- 随着 \(j\) 的增大,\(sum_j\) 也单调递增,这意味着我们只会在右侧加点。
以上两个性质很重要,它们直接决定了维护方式!
这道题要我们求最大值,所以要使 \(dp(i)\) 尽量大,又因为 \(dp(i)\) 是直线截距中系数为正的一项,所以我们要使截距尽量大。
当我们找最优决策即枚举 \(j\) 时,外层循环即 \(i\) 是一定的,所以斜率也一定。想象一下有一条斜率为 \(k\) 的直线从正无穷往下平移,那么当它碰到第一个点时截距最大,如图所示:
这就要求我们维护一个上凸壳。
外层循环每增加 \(1\),就会新加入一个决策点,同时由于斜率会变小,就可能导致前面的一些决策没用了,要及时排除,只保留凸壳上相邻两点连线斜率小于 \(k_i\) 的部分,那么凸壳最左端的点一定就是最优决策点。
这启示我们可以用单调队列来维护凸壳。
如图所示,当直线越来越倾斜时,就应将 \(A,B.C\) 点依次从单调队列中弹出。
根据图像不难看出,当队头与它右边那个点组成的直线斜率大于当前直线斜率时,就应该弹掉,如图所示:
\(k_1 > k_2\) 所以应该弹掉队头。
维护凸壳也是同理,当队尾与它左边那个点组成的直线斜率小于新决策点与队尾组成直线的斜率时,就应该弹掉,如图所示:
\(k_1 < k_2\) 所以应该弹掉队尾。
具体步骤如下:
建立一个单调队列 \(q\),当外层循环 \(i\) 增加 \(1\) 后:
- 排除无用决策,检查队头元素 \(q[hh]\) 和 \(q[hh + 1]\) 构成的直线的斜率是否大于等于当前斜率,具体地,当
时,就应该将队头弹出;
-
取出队头 \(j = q[hh]\) 为最优决策,用它来计算 \(dp(i)\);
-
即将要把 \(i\) 插入队尾,检查队尾 \(q[tt]\)、 \(q[tt - 1]\) 和要加入的 \(i\) 是否构成一个上凸壳,具体地,当
时,就应该将队尾弹出。
其中 \(Y()\) 就是上文提到的 \(Y()\)。
时间复杂度为 \(O(T * n)\)。
\(\texttt{Talk is cheap, show you the code.}\)
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1000010;
typedef long long ll;
int T;
int n;
ll a, b, c;
ll sum[N];
ll dp[N];
int q[N], hh, tt = -1;
inline ll sq(ll x) {return x * x;}
inline ll f(int x) {return a * sq(x) + b * x + c;}
inline ll Y(int x) {return dp[x] + a * sq(sum[x]);}
int main() {
scanf("%d", &T);
while(T--) {
scanf("%d%lld%lld%lld", &n, &a, &b, &c);
for(int i = 1; i <= n; i++) {
scanf("%lld", &sum[i]);
sum[i] += sum[i - 1];
}
hh = tt = 0;
q[0] = 0; //首先加入的决策是 0
for(int i = 1; i <= n; i++) {
while(hh < tt && (Y(q[hh + 1]) - Y(q[hh])) >= (2 * a * sum[i] + b) * (sum[q[hh + 1]] - sum[q[hh]])) hh++; //弹掉过时决策
int j = q[hh];
dp[i] = dp[j] + f(sum[i] - sum[j]);
while(hh < tt && (Y(q[tt]) - Y(q[tt - 1])) * (sum[i] - sum[q[tt]]) <= (Y(i) - Y(q[tt])) * (sum[q[tt]] - sum[q[tt - 1]])) tt--; //维护上凸壳
q[++tt] = i;
}
printf("%lld\n", dp[n]);
memset(sum, 0, sizeof sum);
memset(dp, 0, sizeof dp);
}
return 0;
}