分级(左偏树做法和dp做法)

89 篇文章 0 订阅

给定长度为 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 B1B2...BN B 1 ≥ B 2 ≥ . . . ≥ B N B_1 \ge B_2 \ge ... \ge B_N B1B2...BN
  • 最小化 S = ∑ i = 1 N ∣ A i − B i ∣ S = \sum_{i=1}^N|A_i-B_i| S=i=1NAiBi

只需要求出这个最小值 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 1N2000,
0 ≤ A i ≤ 1 0 6 0 \le A_i \le 10^6 0Ai106

输入样例:
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;
    
}
  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

green qwq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值