【Baltic 2004】数字序列(有改动)

一道关于数字序列的题目,要求通过最小代价将其变为不降序列。解题思路是使用单调栈,当遇到递减序列时,将序列改为中位数,以最小化代价。通过维护大根堆来动态求解中位数,并在必要时合并区间。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原题在BZOJ上的链接

题目描述

题意:给你一个长度为n的整数序列A。把它变成一个不降的序列。求改动的最小代价,代价为更改前后的数字大小的差值。

就是没有了原题中改动次数最小的限制。

题解

考试时看到这到题就以为是数字序列了…
不说废话了。
既然没有操作次数最小的要求,那么就只用考虑最小代价了(废话)
怎么使代价最小呢?对于一个序列,如果它是单调不降的,我们显然不用去单独管它。
如果有减小的呢? 若单独考虑考虑一段递减序列,那么自然是变成它们的中位数。为什么呢?求函数f(x)=|x-h1|+|x-h2|+|x-h3|….+|x-hn|的最小值总会吧,一根数轴即可搞定。
但我们显然不能只考虑一段区间,对于我们分出的每一段区间,必须要他们的中位数不降,否则不满足题意。对于这样的区间,可以用一个栈来维护。

那么只用考虑在栈顶前后两个区间不合法时如何合并两段区间了。
这里似乎只要能求出合并后的中位数即可,其他的数据(统计答案时要用,具体看代码)都可以直接开数组记录。
那么讨论如何求中位数,看出来是个动态求中位数了吧,是不是想到用两个堆,保证他们的size差不大于1。这道题要支持合并的话就左偏树。

但观察到这道题还有一个性质,因为只有在后面的区间的中位数小于前一个区间时才会合并,那么合并后两者的中位数必然相对与前一个减小,那么开个大根堆的话就只用在合并之后保留一半多一个元素即可,堆顶就是中位数了!
统计答案的话暴力O(n)就可以了。
至于这样做正确性的证明,可以参考 国家集训队2005论文集_黄源河 的论文(orz)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<queue>
#include<cmath>
using namespace std;
inline int read()
{
    int x=0;char ch=getchar();int t=1;
    for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') t=-1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+(ch-48);
    return x*t;
}
const int N=1e5+10;
typedef long long ll;
const ll INF=1e18;
ll h[N];
int n;
#define __ NULL
#define Dis(a) (a==__? -1:a->dis)
#define Size(a) (a==__? 0:a->size)
inline ll Abs(ll a){return a>0? a:-a;}
struct node{
    node* ls;node* rs;int x;int dis;int id;int size;
    void clear(){ls=rs=__;x=0;dis=0;id=0;size=1;}
}*tr[N];
node pool[N];int cnt=0;
int st[N];
int si[N];
int top=0;
inline void updata(node* p)
{
    if(p==__) return;
    if(Dis(p->ls)<Dis(p->rs)) swap(p->ls,p->rs);
    p->size=1+Size(p->ls)+Size(p->rs);
    p->dis=Dis(p->rs)+1;
    return ;
}
inline node* merge(node* p,node* q)
{
    if(p==__) return q;
    if(q==__) return p;
    if(p->x<q->x)  swap(p,q);
    p->rs=merge(p->rs,q);
    updata(p);
    return p;
}
inline void Maintain()
{
    while(233){
        if(top<2||tr[st[top]]->x>=tr[st[top-1]]->x) return;
        register int S=si[top]+si[top-1];
        register node* p=merge(tr[st[top]],tr[st[top-1]]);
        top--;si[top]=S;
        while(p->size>S/2+1) {
            register node* q=p;p=merge(p->ls,p->rs);
            q->ls=q->rs=__;
        }
        st[top]=p->id;
    }
}
int main()
{
    n=read();
    for(register int i=1;i<=n;i++){
        h[i]=1ll*read();tr[i]=&pool[++cnt];tr[i]->clear();tr[i]->x=h[i];tr[i]->id=i;
    }
    st[1]=top=1;si[1]=1;
    for(register int i=2;i<=n;i++){
        st[++top]=i;si[top]=1;
        Maintain();
    }
    int now=1;
    ll ans=0;register int H=0;
    while(now<=top&&H<n){
        for(register int i=1;i<=si[now]&&H<n;i++){
            H++;
            ans+=Abs(1ll*(h[H]-tr[st[now]]->x));
        }
        now++;
    }
    printf("%lld\n",ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值