[BZOJ3333]排队计划(线段树)

本文介绍了一种使用线段树解决逆序对计数问题的方法,通过维护区间最小值的位置来跟踪序列的变化,实现了O(nlogn)的时间复杂度。

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

=== ===

这里放传送门

=== ===

题解

题目大意:给出一个序列,每次选择一个位置,记位置上的数为c,把这个位置及这个位置之后小于等于c的数全都挑出来排好序再放回空出的位置,求初始及每次操作之后序列的逆序对数。
无论是用归并排序还是树状数组,求一次逆序对数的复杂度都是 O(nlogn)  的,如果每次都重新求一次逆序对数的话肯定不行。考虑每次操作对序列逆序对数产生的影响,可以发现如果把一个位置的“贡献”记作这个位置后面小于它的数的个数,那么每次操作以后操作过的这些位置上的贡献就消失了,而其他位置上的贡献不变。原因是因为操作中把它后面小于等于它的数都挑出来排序了,所以被挑出来的每个位置之后都不会再存在小于它的数了;而对于没有被挑出来的那些数,分成选定位置之前和选定位置之后两种情况讨论:选定位置之前的那些数,它们后面的数相对顺序虽然变了,但还是那些数,也就是每个位置的“贡献”没有变;选定位置之后的那些数都是比选定的数大的,也就是说被挑出来排序的所有数都比没被挑出来的数要小,那么操作过以后,对于没被挑出来的那些数,虽然它们后面的那些数的种类变了,但原来比它们小的操作过以后还是比它们小,也就是“贡献”没有变。
并且从上面的事实我们还可以得出一个结论就是“贡献”的消失是不可逆的,一个位置的贡献只会消失不会回来。这样的话如果我们可以快速定位到当前操作将要被挑出来的,并且以前的操作中没有处理过的位置,就可以在 O(nK)  的时间内完成这个问题。 K  是一个多项式,代表每次定位所需要的时间。
这里利用了线段树来维护区间最小值的位置。因为每个位置只会被操作一次,所以操作过的位置直接赋值为极大值,这样下一次查找就不会找到它了。不断在选定位置之后的区间中查找直到选定的位置也被修改为极大值。时间复杂度O(nlogn) 

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define inf 2000000000
using namespace std;
int n,m,cnt,b[500010],Min[2000010];
long long h[500010],p[500010],ans;
int comp(int x,int y){
    return h[x]<h[y];
}
long long query(int i){
    long long sum=0;
    for (;i!=0;i-=i&(-i)) sum+=p[i];
    return sum;
}
void add(int i){for (;i<=n;i+=i&(-i)) p[i]++;}
void update(int i){
    if (b[Min[i<<1]]<b[Min[(i<<1)+1]])
      Min[i]=Min[i<<1];//注意线段树里维护的是位置信息
    else Min[i]=Min[(i<<1)+1];
}
void build(int i,int l,int r){
    if (l==r){
        Min[i]=l;return;
    }
    int mid=(l+r)>>1;
    build(i<<1,l,mid);
    build((i<<1)+1,mid+1,r);
    update(i);
}
int ask(int i,int l,int r,int left,int right){
    if (left<=l&&right>=r) return Min[i];
    int mid=(l+r)>>1,lc=0,rc=0,ans;
    if (left<=mid) lc=ask(i<<1,l,mid,left,right);
    if (right>mid) rc=ask((i<<1)+1,mid+1,r,left,right);
    if (lc!=0&&b[lc]<b[rc]) ans=lc;//如果左子树有答案并且左子树严格更优就取左子树
    else ans=rc;//否则取右子树
    return ans;
}
void change(int i,int l,int r,int x,int v){
    if (l==r){b[l]=v;return;}
    int mid=(l+r)>>1;
    if (x<=mid) change(i<<1,l,mid,x,v);
    else change((i<<1)+1,mid+1,r,x,v);
    update(i);
}
int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%I64d",&h[i]);p[i]=i;
    }
    sort(p+1,p+n+1,comp);
    for (int i=1;i<=n;i++)
      if (h[p[i]]==h[p[i-1]])
        b[p[i]]=cnt;
      else b[p[i]]=++cnt;
    memset(p,0,sizeof(p));
    for (int i=n;i>=1;i--){
        h[i]=query(b[i]-1);
        ans+=h[i];add(b[i]);
    }
    printf("%I64d\n",ans);
    build(1,1,n);
    for (int i=1;i<=m;i++){
        int pos;
        scanf("%d",&pos);
        while (b[pos]!=inf){
            int c=ask(1,1,n,pos,n);
            ans-=h[c];h[c]=0;
            change(1,1,n,c,inf);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

偏偏在最后出现的补充说明

因为题目里要求的是挑出小于等于选定位置的那些数,也就是线段树中维护的“最小值位置”应该尽量靠右,这就要求当左右子树的答案一样时,应该优先取右子树来更新父节点。
其实做这道题的原因是因为它的题号(눈_눈)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值