题目地址:
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必然是非负整数,而任意自然数集合一定是有最小值的。
法1:这个问题需要用到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<t_{i-1}
ai<ti−1,由于是加上了
∣
x
−
a
i
∣
|x-a_i|
∣x−ai∣这个函数,这个时候
t
i
t_i
ti的增加值其实就是
t
i
−
1
−
a
i
t_{i-1}-a_i
ti−1−ai。那么
f
i
f_i
fi的拐点集中,
f
i
−
1
f_{i-1}
fi−1的最大拐点实际上拐的次数会少
1
1
1(因为
∣
x
−
a
i
∣
|x-a_i|
∣x−ai∣在那个点的斜率为
1
1
1),所以要将这个拐点从
f
i
−
1
f_{i-1}
fi−1拐点集中删除一次;而在
(
a
i
,
t
i
−
1
)
(a_i,t_{i-1})
(ai,ti−1)这个区间内的拐点,次数是不变的,因为此区间内斜率都是增加
1
1
1;然后
a
i
a_i
ai的拐点次数增加
2
2
2;
(
−
∞
,
a
i
)
(-\infty, a_i)
(−∞,ai)里的拐点次数也是不变,因为斜率都是减
1
1
1。
所以具体算法就是用一个最大堆维护拐点集,以求 s i s_i si。代码如下:
#include <iostream>
#include <algorithm>
#include <queue>
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)。
法2:动态规划。可以证明,存在一个最优解 b b b使得 b b b中只出现 a a a中的数字。证明参考https://blog.csdn.net/qq_46105170/article/details/126434918。设 a a a从小到大排好序之后是 a ′ a' a′,设 f [ i ] [ j ] f[i][j] f[i][j]是只考虑 a 1 ∼ i a_{1\sim i} a1∼i的,且 b i = a j ′ b_i=a'_j bi=aj′的时候,差的绝对值之和的最小值。那么可以按照 b i − 1 b_{i-1} bi−1等于多少来分类,那么 b i − 1 ∈ { a 1 ′ , . . . , a i ′ } b_{i-1}\in\{a'_1,...,a'_i\} bi−1∈{a1′,...,ai′},所以 f [ i ] [ j ] = min k ≤ j { f [ i − 1 ] [ k ] } + ∣ a j − a j ′ ∣ f[i][j]=\min_{k\le j}\{f[i-1][k]\}+|a_j-a'_j| f[i][j]=k≤jmin{f[i−1][k]}+∣aj−aj′∣可以用一个变量维护 f [ i − 1 ] [ . ] f[i-1][.] f[i−1][.]的前缀最小值。代码如下:
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 2010, INF = 2e9;
int n, a[N], b[N];
int f[N][N];
int work() {
for (int i = 1; i <= n; i++) b[i] = a[i];
sort(b + 1, b + 1 + n);
for (int i = 1; i <= n; i++) {
int minv = INF;
for (int j = 1; j <= n; j++) {
minv = min(minv, f[i - 1][j]);
f[i][j] = minv + abs(b[j] - a[i]);
}
}
int res = INF;
for (int i = 1; i <= n; i++) res = min(res, f[n][i]);
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
int res = work();
reverse(a + 1, a + 1 + n);
res = min(res, work());
printf("%d\n", res);
}
时空复杂度 O ( n 2 ) O(n^2) O(n2)。