给定长度为 N N N 的序列 A A A,构造一个长度为 N N N 的序列 B B B,满足:
- B B B 非严格单调,即 B 1 ≤ B 2 ≤ . . . ≤ B N B_1 \le B_2 \le... \le B_N B1≤B2≤...≤BN 或 B 1 ≥ B 2 ≥ . . . ≥ B N B_1 \ge B_2 \ge ... \ge B_N B1≥B2≥...≥BN。
- 最小化 S = ∑ i = 1 N ∣ A i − B i ∣ S = \sum_{i=1}^N|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 \le N \le 2000
1≤N≤2000,
0
≤
A
i
≤
1
0
6
0 \le A_i \le 10^6
0≤Ai≤106
输入样例:
7
1
3
2
4
5
3
9
输出样例:
3
做法1——dp
引理:
一定存在一组最优解,使得每个 B i B_i Bi 都是原序列中的某个值。
也就是说左右皆可能出现每一个 B i B_i Bi 都在 A [ ] A[] A[] 出现过。
证明简介版:就是我们随机选定一些数,如果在两条线下面的比上面多,我们就往下移,此时就有可能移到线上。(以上只是我的理解)具体点击链接
那么此时我们就可以状态表示:已经选好了前 i i i 个 b i b_i bi,且最后一个数等于 a i a_i ai 的所有最小值。
集合划分(集合划分都是以最后一个不同点划分的,但本道题最后一点都相同,因此我们看倒数第二个),此时倒数第二个可以将集合划分成 j j j 个。
此时我们可以优化掉第三维度:
此时代码就很好写了。
- 注意1:因为可能单调递增和递减,因此我们先求递增然后取反就可以了。
代码
//f[i][j]表示已经选好了前i个b_i,且a[i]==b[j]
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 2010,INT=0x3f3f3f3f;
int f[N][N];
int n,a[N],b[N];
int work(){
int res=INT;
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=INT;
for(int j=1;j<=n;j++){
minv=min(minv,f[i-1][j]);
f[i][j]=minv+abs(a[i]-b[j]);
}
}
for(int i=1;i<=n;i++)res=min(res,f[n][i]);
return res;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int res=work();
reverse(a+1,a+1+n);
res=min(res,work());
cout<<res;
return 0;
}
做法2——左偏树
为什么能用左偏树,具体可以看这里。
因为那道题根这道题很像。
代码
//首先,我们将严格上升子序列转化成非严格的,就减i。
//这里的推导很复杂,这边给出结论:如果单调升了又降了,此时我们就
//从右往左取中位数,取中位数是因为之前的货仓选址那道题的灵感
//绝对值之和最小,取所有的中位数
//这边要注意的是:我们是a是固定的,求得是b
//此时我们用左偏树维护中位数
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 2010;
int w[N];
int n;
int l[N],r[N],v[N],dist[N];
int res[N];
struct E{
int end,root,size;//终点、根节点、大小
}stk[N];
//这边的根节点是中位数
int merge(int x,int y){
if(!x||!y)return x+y;
if(v[x]<v[y])swap(x,y);
r[x]=merge(r[x],y);
if(dist[r[x]]>dist[l[x]])swap(r[x],l[x]);
dist[x]=dist[r[x]]+1;
return x;
}
int pop(int x){
return merge(l[x],r[x]);
}
LL get(){
int tt=0;//栈
memset(stk,0,sizeof stk);
memset(res,0,sizeof res);
memset(l,0,sizeof l);
memset(r,0,sizeof r);
memset(dist,1,sizeof dist);
for(int i=1;i<=n;i++){
E cur={i,i,1};
//如果当前区间的中位数 < 前一个区间的中位数,则需要将两个区间合并
while(tt&&v[cur.root]<v[stk[tt].root]){
cur.root=merge(cur.root,stk[tt].root);//将两个区间合并
if(cur.size%2&&stk[tt].size%2){//二者均为奇数,要弹出一个元素
cur.root=pop(cur.root);
}
cur.size+=stk[tt].size;
tt--;
}
stk[++tt]=cur;//把当前区间放入栈中
}
// 把答案从单调栈里面提取出来
for(int i=1,j=1;i<=tt;i++){
while(j<=stk[i].end)res[j++]=v[stk[i].root];
}
LL sum=0;
for(int i=1;i<=n;i++)sum+=abs(v[i]-res[i]);
// cout<<sum<<endl;
return sum;
}
signed main(){
// cin>>n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
// cin>>v[i];
scanf("%d",&w[i]);
dist[i]=1;
}
for(int i=1;i<=n;i++)v[i]=w[i];
LL res=get();
for(int i=1;i<=n;i++)v[i]=-w[i];
printf("%lld",min(res,get()));
return 0;
}