https://www.acwing.com/problem/content/275/
给定长度为 n n n的序列 a a a,构造一个长度为 n n n的序列 b b b,满足: b b b非严格单调,即 b 1 ≤ b 2 ≤ … ≤ b n b_1≤b_2≤…≤b_n b1≤b2≤…≤bn或 b 1 ≥ b 2 ≥ … ≥ b n b_1≥b_2≥…≥b_n b1≥b2≥…≥bn。最小化 s = ∑ i = 1 n ∣ a i − b i ∣ s=∑^n_{i=1}|a_i−b_i| s=∑i=1n∣ai−bi∣。只需要求出这个最小值 s s s。
输入格式:
第一行包含一个整数 N N N。
接下来 N N N行,每行包含一个整数 A i A_i Ai。
输出格式:
输出一个整数,表示最小 S S S值。
数据范围:
1 ≤ N ≤ 2000 1≤N≤2000 1≤N≤2000
0 ≤ A i ≤ 1 0 6 0≤A_i≤10^6 0≤Ai≤106
只考虑非降情形,可以证明最优解是存在的,这个较为显然,因为 S S S必然是非负整数,而任意自然数集合一定是有最小值的。
这个问题需要用到Slope Trick的技巧。先给几个定义:
考虑一元实值函数 f ( x ) f(x) f(x),如果 f f f满足以下三条,我们就称其Slope Trickable:
1、 f ( x ) f(x) f(x)连续;
2、 f ( x ) f(x) f(x)可以分解为有限的若干子段,使得每一段里导数 f ′ ( x ) f'(x) f′(x)恒等于一个整数;
3、 f ( x ) f(x) f(x)下凸,即 f ′ ( x ) f'(x) f′(x)单调增(严格的下凸函数定义并不等价导函数单调增,甚至可以导数不存在。这里不需要细究)。
不妨称这种函数为“好函数”。最经典的好函数是 ∣ x − a ∣ |x-a| ∣x−a∣,其满足上面的三个条件。容易证明,若干个好函数之和依然是好函数。
回到题目。设 f i ( x ) f_i(x) fi(x)是只考虑 a 1 ∼ i a_{1\sim i} a1∼i的情况下,并且 a i ≤ x a_i\le x ai≤x,求得的 s s s最小值。令 f 0 ( x ) = 0 f_0(x)=0 f0(x)=0。设 g i ( x ) g_i(x) gi(x)也是只考虑 a 1 ∼ i a_{1\sim i} a1∼i的情况下,并且 a i = x a_i=x ai=x,求得的 s s s最小值。显然有: { f i ( x ) = min { g i ( t ) : t ≤ x } g i ( x ) = f i − 1 ( x ) + ∣ x − a i ∣ \begin{cases}f_i(x)=\min\{g_i(t): t\le x\} \\ g_i(x)=f_{i-1}(x)+|x-a_i|\end{cases} {fi(x)=min{gi(t):t≤x}gi(x)=fi−1(x)+∣x−ai∣我们证明 f i ( x ) f_i(x) fi(x)也是好函数。可以用数学归纳法。 f 0 f_0 f0是好函数。假设对于 i ≤ k − 1 i\le k-1 i≤k−1, f i f_i fi是好函数,那么可以知道 g i g_i gi也是好函数(好函数之和依然是好函数)。只需要证明如果 g g g是好函数,则 h ( x ) = min { g ( t ) : t ≤ x } h(x)=\min\{g(t):t\le x\} h(x)=min{g(t):t≤x}也是好函数。直观理解是很显然的,其实 h h h满足,当 g g g单调下降的时候, h = g h=g h=g,当 g g g开始不下降的时候, h h h把那一段开始“拉平”,即斜率拉平为 0 0 0。综上 f i f_i fi是好函数。我们定义 f i f_i fi的拐点多重集(下面简称“拐点集”),为其所有导数从左向右增加 1 1 1的点,如果某个点处导数增加 k k k,则将该点计入 k k k次。例如 ∣ x − 1 ∣ |x-1| ∣x−1∣的拐点集为 { 1 , 1 } \{1,1\} {1,1},因为在 1 1 1的位置斜率由 − 1 → 1 -1\to 1 −1→1,变了 2 2 2,所以 1 1 1出现两次。
令 s i = ∣ a 1 − b 1 ∣ + ∣ a 2 − b 2 ∣ + ⋅ ⋅ ⋅ + ∣ a i − b i ∣ s_i=|a_1−b_1|+|a_2−b_2|+···+|a_i−b_i| si=∣a1−b1∣+∣a2−b2∣+⋅⋅⋅+∣ai−bi∣,那么 s i s_i si能取到的最小值即为 t i = lim x → + ∞ f i ( x ) t_i=\lim_{x\to +\infty}f_i(x) ti=limx→+∞fi(x),所以我们本质上只需要每次求出 t i t_i ti即可,最后返回 s = t n s=t_n s=tn。注意到 f i − 1 f_{i-1} fi−1右边充分远处导数一定为 0 0 0, g i g_i gi右边充分远处导数一定为 1 1 1。考虑 f i − 1 f_{i-1} fi−1的最右边的不可导点(即 t i − 1 t_{i-1} ti−1),分两种情况:
1、如果 a i ≥ t i − 1 a_i\ge t_{i-1} ai≥ti−1,则 t i = t i − 1 t_i=t_{i-1} ti=ti−1,比较显然。那么 f i f_i fi的拐点集即在 f i − 1 f_{i-1} fi−1的拐点集中加上 a i a_i ai即可;
2、如果 a i < t i − 1 a_i
所以具体算法就是用一个最大堆维护拐点集,以求 s i s_i si。代码如下:
#include
#include
#include
using namespace std;
const int N = 2010;
int n, a[N];
int solve() {
int res = 0;
priority_queue<int> heap;
for (int i = 1; i <= n; i++) {
heap.push(a[i]);
if (a[i] < heap.top()) {
res += heap.top() - a[i];
heap.pop();
// a[i]拐点次数需要增加2,所以这里要额外增加1次
heap.push(a[i]);
}
}
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
int res = solve();
reverse(a + 1, a + 1 + n);
printf("%d\n", min(res, solve()));
}
时间复杂度 O ( n log n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)。