TonyYin's Blog

Back

题意#

给定一个长度为 nn 的序列,每个元素有权值 xix_i,现在要将序列分割为若干段,每段中元素连续。设 sis_i 表示前缀和,即 si=j=1ixjs_i=\sum_{j=1}^{i}{x_j},那么对于每段 [l,r][l, r],代价为:

a(sisj1)2+b(sisj1)+ca(s_{i}-s_{j-1})^2+b(s_i-s_{j-1})+c

其中 a,b,ca, b, c 为题目给定的常数。整个序列的代价为每段代价之和。

求序列的代价最大值

1n106,107b,c107,1xi1001\leq n\leq 10^6, -{10}^7\leq b, c\leq 10^7, 1\leq x_i\leq 100.

注意:5a1<0-5\leq \textcolor{red}{a} \leq -1\textcolor{red}{<0}.

DP#

容易想到时间复杂度为 O(n2)\mathcal{O}(n^2) 的 DP 做法。

fif_i 为前 ii 个物品,分若干段的最小代价,有状态转移方程:

fi=maxj<i{fj+a(sisj)2+b(sisj)+c}f_i=\max_{j<i}{\{f_j+a(s_i-s_j)^2+b(s_i-s_j)+c\}}

其中 sis_i 与题目中定义相同,为 xx 的前缀和。

但发现 1n1061\leq n\leq 10^6,所以这样的 DP 无法通过此题,考虑如何优化。

优化#

发现对于 fif_i 来说,一定存在唯一的 j<ij<ifif_i 进行了更新,使得 fif_i 取到最大值。所以不妨设 jj 为能使 fif_i 最大化的继承状态, 那么对暴力的状态转移方程进行整理:

fi=fj+a(sisj)2+b(sisj)+cfi=fj+a(si2+sj22sisj)+b(sisj)+cfi=fj+asi2+asj22asisj+bsibsj+c\begin{aligned} f_i&=f_j+a(s_i-s_j)^2+b_(s_i-s_j)+c\\ f_i&=f_j+a({s_i}^2+{s_j}^2-2s_is_j)+b(s_i-s_j)+c\\ f_i&=f_j+a\cdot {s_i}^2+a\cdot {s_j}^2-2a\cdot s_is_j+b\cdot s_i-b\cdot s_j+c \end{aligned}

(这就很斜率优化

考虑一次函数的斜截式 y=kx+by=kx+b,将方程转化为这个形式。其中变量 x,yx, yjj 有关,b,kb, kjj 无关,且要求 xxjj 单调递增。

那么:

fibsiasi2=(fj+asj2bsj+c)2asisj\textcolor{red}{f_i-b\cdot s_i-a\cdot {s_i}^2}=\textcolor{purple}{(f_j+a\cdot {s_j}^2-b\cdot s_j+c)}-\textcolor{green}{2a\cdot s_i}\textcolor{blue}{s_j}

所以:

{y=fj+asj2bsj+ck=2asix=sjb=fibsiasi2\left\{ \begin{array}{lr} y = f_j+a\cdot {s_j}^2-b\cdot s_j+c\\ k = 2a\cdot s_i\\ x = s_j\\ b = f_i-b\cdot s_i-a\cdot {s_i}^2 \end{array} \right.

观察 bb 这一项。要最大化 fif_i,而 bsiasi2-b\cdot s_i-a\cdot{s_i}^2 为常数,所以只要最大化 bb 即可。

由于 x,yx, y 只与 jj 有关,不妨设 (xj,yj)(x_j, y_j) 为平面直角坐标系上的一些点。那么问题就转化为,如何选取 jj,才能使得,斜率固定为 kk 的,且过点 (xj,yj)(x_j, y_j) 的直线的截距 bb 最大。

(斜率为定值 kk,是因为 2asi2a\cdot s_i 也都是题目给定的定值。

那么问题转化到了平面上。

由上图可以形象的看出,使直线截距最大的点(最优决策点),就是将直线向下平移时第一个碰到的点

而且显然对于任意的斜率 kk 来说,最优决策点只有可能在上凸包上,也就是上图中,绿色直线所覆盖的点。

继续观察,在上图中可以看出,对于斜率为 kk 的直线,最优解 jj 就是第一个 slope(j,j+1)<k\operatorname{slope}(j, j+1)<k 的点。(slope(a,b)\operatorname{slope}(a, b) 表示 (xa,ya),(xb,yb)(x_a, y_a), (x_b, y_b) 两点构成直线的斜率)

又因为本题中,kkii 递增而递减,与上凸包单调性相同,所以考虑使用单调队列解决。

单调队列#

设单调队列(递增)为 qq,队首为 headhead,队尾为 tailtail,当前要转移 ii 号点,具体步骤:

  1. slope(qhead,qhead+1)2asi\operatorname{slope}(q_{head}, q_{head+1})\textcolor{red}{\geq}2a\cdot s_i 时,headhead++,(保证最优解)

  2. 此时的 qheadq_{head} 就是最优转移点,根据它和转移方程计算出 fif_i

  3. slope(qtail1,qtail)slope(qtail,i)\operatorname{slope}(q_{tail-1}, q_{tail})\textcolor{red}{\leq}\operatorname{slope}(q_{tail}, i) 时,tailtail—,(维护上凸包

  4. 在队尾插入 ii.

代码#

注意多组数据即可。

#include <bits/stdc++.h>
#define int long long
#define x(X) (s[X])
#define y(X) (f[X] + a * s[X] * s[X] - b * s[X] + c)
using namespace std;
const int MAXN = 1e6 + 10;
int n;
int a, b, c;
int s[MAXN], f[MAXN];
int head, tail, q[MAXN];
double slope(int i, int j) {
	return 1.0 * (y(j) - y(i)) / (x(j) - x(i));
}
signed main() {
	int T; scanf("%lld", &T);
	while(T--) {
		scanf("%lld%lld%lld%lld", &n, &a, &b, &c);
		for(int i = 1; i <= n; i++) {
			int tmp; scanf("%lld", &tmp);
			s[i] = s[i - 1] + tmp;
		}
		head = tail = 1;
		for(int i = 1; i <= n; i++) {
			while(head < tail && slope(q[head], q[head + 1]) >= 2ll * a * s[i])
                head++;
			int j = q[head];
            f[i] = f[j] + a * (s[i]-s[j])*(s[i]-s[j]) + b*(s[i]-s[j]) + c;
			while(head < tail && slope(q[tail - 1], q[tail]) <= slope(q[tail], i))
                tail--;
			q[++tail] = i;
		}
		cout << f[n] << endl;
	}
	return 0;
}
cpp
【APIO2010】特别行动队
https://www.tonyyin.top/blog/oi-solution/p3628
Author TonyYin
Published at March 5, 2021
Comment seems to stuck. Try to refresh?✨