题意简述
有 n ( 0 ≤ n ≤ 5 × 1 0 5 ) n(0 \leq n \leq 5 \times 10^5) n(0≤n≤5×105)个单词与一常数 m ( 0 ≤ m ≤ 1 0 3 ) m(0 \leq m \leq 10 ^ 3) m(0≤m≤103),第 i i i个单词有一属性 C i C_i Ci(原题未给出 C i C_i Ci的取值范围,但应为正整数或非负整数),需要将所有这些单词按顺序分为任意组,若将第 a a a至 b b b个单词分为一组,则花费为 ( ∑ i = a b C i ) 2 + m (\sum^b_{i = a}C_i)^2 + m (∑i=abCi)2+m,求出最少花费。有多组数据,以EOF结束输入。
思路
斜率优化板子题。设 f i f_i fi为将前 i i i个单词分组后的最小花费, s i s_i si为 ∑ k = 1 i C k \sum^i_{k = 1}C_k ∑k=1iCk(即 C C C的前缀和),可写出状态转移方程为 f i = min ( f j + ( s i − s j ) 2 + m ) ( 0 ≤ j < i ) f_i = \min(f_j + (s_i - s_j)^2 + m)(0 \leq j < i) fi=min(fj+(si−sj)2+m)(0≤j<i), f n f_n fn即为最优解。若对于每一个 i i i,暴力枚举 j j j,时间复杂度是 O ( n 2 ) O(n^2) O(n2),显然无法通过本题。
优化
将方程移项,得 f i − s i 2 − m = min ( f j + s j 2 − 2 s i s j ) ( 0 ≤ j < i ) f_i - s_i^2 - m = \min(f_j + s_j^2 - 2s_is_j)(0 \leq j < i) fi−si2−m=min(fj+sj2−2sisj)(0≤j<i)。作如下代换:
- y j = f j + s j 2 y_j = f_j + s_j^2 yj=fj+sj2
- k i = 2 s i k_i = 2s_i ki=2si
- x j = s j x_j = s_j xj=sj
- b i = f i − s i 2 − m b_i = f_i - s_i^2 - m bi=fi−si2−m
得 b i = min ( y j − k i x j ) b_i = \min(y_j - k_ix_j) bi=min(yj−kixj),恰好化为了直线方程的斜截式形式,且对于每个 x j , y j x_j, y_j xj,yj,都能够将其转换为平面直角坐标系上的一点 ( x j , y j ) (x_j, y_j) (xj,yj)。于是,此时的任务变为已知直线的斜率 k i k_i ki,需寻找一点 ( x j , y j ) (x_j, y_j) (xj,yj)使得该直线过该点的截距 b i b_i bi最小。
注意到 C i ≥ 0 C_i \geq 0 Ci≥0,可知 s i s_i si单调递增,则 k i k_i ki同样单调递增。我们可以将斜率为 k i k_i ki的直线从下往上移,该直线碰到的第一个点 ( x j , y j ) (x_j, y_j) (xj,yj)即为使得截距 b i b_i bi最小的点,此时 f i = f j + ( s i − s j ) 2 + m f_i = f_j + (s_i - s_j)^2 + m fi=fj+(si−sj)2+m,即为此时的最优解。如图:
此时该直线由下往上运动碰到的第一个点即为所求。易知该直线碰到的第一个点一定是这些点所组成的下凸壳的顶点,如图:
所以我们只需要维护之前所有的点所组成的下凸壳,每次判断直线与凸壳相交的顶点即可,可以使用单调队列维护每个点的编号。由于
k
i
k_i
ki单调递增,所以这次碰到的点一定是上次碰到的点或者其右边的点。判断碰到的点可以通过比较一个顶点相邻两条边的斜率和直线的斜率,设
s
l
o
p
e
(
i
,
j
)
\mathop{\mathrm{slope}}(i, j)
slope(i,j)为编号为
i
,
j
i, j
i,j的两点的斜率(非横坐标为
i
,
j
i, j
i,j!),
q
i
q_i
qi为队列中第i个元素,若
s
l
o
p
e
(
q
j
−
1
,
q
j
)
≤
k
i
<
s
l
o
p
e
(
q
j
,
q
j
+
1
)
\mathop{\mathrm{slope}}(q_{j - 1}, q_j) \leq k_i < \mathop{\mathrm{slope}}(q_j, q_{j + 1})
slope(qj−1,qj)≤ki<slope(qj,qj+1),则
q
i
q_i
qi即为所求点的编号。由于下次的点编号一定大于等于这次的点编号,所以可以定义一个变量表示这次在队列中的下标,下次直接判断递增即可。每次求完
f
i
f_i
fi之后需要更新此时的凸壳。总时间复杂度
O
(
n
)
O(n)
O(n)。
代码:
#include <cstdio>
#define y(g) (f[g] + sqr(x(g)))
#define x(g) (sum[g])
#define sqr(x) ((x) * (x))
using namespace std;
typedef long long ll;
int n, m, q[500005], s, e;
ll sum[500005], f[500005];
int main() {
while (~scanf("%d %d", &n, &m)) {
s = e = 1; //此处隐含q[1] = 0,表示第一个编号是0,对应将前i个单词全部分为一组的情况
for (int i = 1; i <= n; ++i) {
scanf("%lld", &sum[i]);
sum[i] += sum[i - 1];
while (s < e && y(q[s + 1]) - y(q[s]) <= 2 * x(i) * (x(q[s + 1]) - x(q[s]))) ++s;
f[i] = f[q[s]] + sqr(x(i) - x(q[s])) + m;
while (s < e && (y(q[e]) - y(q[e - 1])) * (x(i) - x(q[e])) >= (y(i) - y(q[e])) * (x(q[e]) - x(q[e - 1])))
--e;
q[++e] = i;
}
printf("%lld\n", f[n]);
}
return 0;
}