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
∣
{fi(x)=min{gi(t):t≤x}gi(x)=fi−1(x)+|x−ai|
令
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)。
法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
#include
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)。