BZOJ1112&&洛谷P3466 [POI2008]KLO-Building blocks(砖头)[对顶堆学习笔记]

版权声明:我这么弱的蒟蒻,虽然博文不是很好,但也请标明转发地址喵! https://blog.csdn.net/ACerAndAKer/article/details/82751502

坑爹的BZOJ因为不让 输出方案我WA了若干次

据说正解是平衡树/线段树,我不会,就用了个新东西—>对顶堆

对顶堆

一个维护中位数的东西,我们通过维护两个堆,来维所有数的中位数
(显然我们可以排个序)

具体实现:

我们维护一个大根堆,把所有小于等于中位数的数放进去,堆顶就是中位数,你会问,为什么会是中位数?我们保证这个堆中的元素个数始终为(n+1)/2个,两个堆元素个数一共为n所以这个堆的堆顶就是中位数,另一个堆是小根堆,维护的所有大于中位数的数放进去,中位数在填完最后一个数之前把那个不是固定的,所以我们要等到所有数加完后,取第一个堆的堆顶元素就好了

具体操作:

先考虑 整个数列 ,不带删除的情况,每加入一个元素,我们先判断放小数的堆是否够了(k+1)/2个,不够就要先放进去,然后够了之后,每次加入一个元素 ,判断和大根堆堆顶的关系,比他大说明比目前的中位数要大,所以放到小根堆中,小的话就放到大根堆中,然后我们发现这时大根堆的元素多了,那么我们就要把大根堆的堆顶放到小根堆中,这样仍然满足性质

然后是部分区间的中位数,就是我们这道题
不难发现 ,把一段连续区间修改为他们的中位数是最优的,所以我们就可以维护每个长度为k的区间修改为中位数的花费,区间移动就涉及到了删除元素的操作 ,但是我们又找不到上一个元素具体在什么位置,怎么办?我们不再用堆的size表示堆中元素个数,我们用两个变量siz1,siz2表示两个堆的大小,然后我们发现,只要删除元素不在堆顶,他在什么位置是没有关系的,所以我们只需呀给他打上标记,然后等他到堆顶时,直接pop掉 就好了,打标记 的同时也要给它所在堆的siz–,判断 所在堆的方法是比较删除元素于大根堆堆顶的大小关系,也就是和在中位数的大小关系
然后这时你可能会说,我们维护了中位数,怎么统计答案呢?我们将求花费的式子列出来并化简得到ans= i = 1 ( i + k + 1 ) / 2 m i d a [ i ] \sum_{i=1}^{(i+k+1)/2}mid-a[i] + i = ( i + k + 1 ) / 2 + 1 i + k \sum_{i=(i+k+1)/2+1}^{i+k} ,然后发现它等于比mid小的数的个数 \ast mid-比mid小的数的和+比mid大的数的和-mid \ast 比mid大的数的个数,个数很好说就是两个堆的大小,和呢?我们 也维护两个值sum1和sum2,表示两个堆的大小,插入和删除时,一块修改就好了

代码

//By AcerMo
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define t1 q1.top()
#define t2 q2.top()
#define grtr greater
#define lli long long int
using namespace std;
const int M=200500;
int n,m;
int a[M],vis[M*10];
lli si1,si2,su1,su2;
priority_queue<int,vector<int>,less<int> >q1;//大根堆,保证堆顶是所有小数的最大值
priority_queue<int,vector<int>,grtr<int> >q2;//小根堆,保证堆顶是所有大数的最小值
inline int read()
{
    int x=0;char ch=getchar();
    while (ch>'9'||ch<'0') ch=getchar();
    while (ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
    return x;
}
inline void write(lli x)
{
    if (x>9) write(x/10);
    putchar(x%10+'0');
    return ;
}
inline void update()
{
    if (q1.size()) 
        while (vis[t1]&&q1.size()) vis[t1]--,q1.pop();
    if (q2.size())
        while (vis[t2]&&q2.size()) vis[t2]--,q2.pop();
    return ;
}//清除堆顶的删除元素
inline void add(int x)
{
    update();
    while (si1<(m+1)/2&&si2) 
    {
        int y=t2;q2.pop();q1.push(y);
        si1++;si2--;su2-=y;su1+=y;
        update();
    }//将大根堆的元素加到中位数的位置上(m+1)/2中位数的位置
    if (si1==0)
    {
        si1++;su1+=x;
        return (void)(q1.push(x));
    }//这个情况只有在两个堆都是空的的时候会出现,优先放在小数的里
    if (x<q1.top()) si1++,su1+=x,q1.push(x);//比大根堆的最大值要小,那么肯定不能放在小根堆里
    else si2++,su2+=x,q2.push(x);//不然就放在小根堆里,因为我们已经调整过siz了
    update();
    while (si1>(m+1)/2&&si1)
    {
        int y=t1;q1.pop();q2.push(y);
        si1--;si2++;su1-=y;su2+=y;
        update();
    }//然后调整一下大根堆 ,防止多了
    update();
    while (si1<(m+1)/2&&si2)
    {
        int y=t2;q2.pop();q1.push(y);
        si1++;si2--;su1+=y;su2-=y;
        update();
    }//也可能是一开始没有调整,然后在这次加入了小根堆,那么我们在先满足大根堆的前提下 ,就把他拿过来就好
    return (void)(update());
}
inline void del(int x)
{
    vis[x]++;
    if (x>q1.top()) si2--,su2-=x;
    else si1--,su1-=x;
    return (void)(update());	
}//删除打个标记就行了
inline lli calc(lli x)
{
    lli tot=su2-(lli)m/2*x;
    tot+=(lli)(m+1)/2*x-su1;
    return tot;
}//计算公式:比中位数小的数的个数*中位数大小-比中位数小的数的和
			//+比中位数大的数的和-比中位数大的数的个数*中位数
signed main()
{
    n=read();m=read();
    for (int i=1;i<=n;i++) a[i]=read();
    for (int i=1;i<=m;i++) add(a[i]);
    lli mid,l=1,r=m,ans=t1;
    lli tot=su2-(lli)m/2*ans; 
    tot+=(lli)(m+1)/2*ans-su1;
    for (int i=m+1;i<=n;i++)
    {
        del(a[i-m]);add(a[i]);mid=t1;
        lli cnt=calc(mid);
        if (tot>=cnt&&cnt>=0) 
        tot=cnt,l=i-m+1,r=i,ans=mid;
    }
    write(tot);puts("");
    /*for (int i=1;i<=n;i++)
    if (i>=l&&i<=r) write(ans),puts("");
    else write(a[i]),puts("");*/ //BZ这里要删掉
    return 0;
}

猜你喜欢

转载自blog.csdn.net/ACerAndAKer/article/details/82751502