洛谷 P3157 [CQOI2011]动态逆序对

4 篇文章 0 订阅
1 篇文章 0 订阅

建议做这题之前先做   洛谷 P2617 Dynamic Rankings

what?什么是逆序对?   题目

逆序对定义为满足i<j,且Ai>Aj的数对(i,j),则称(A[ i ],A[ j ])为数组A中的一个逆序对。

题目大意

给1到n的一个排列,按照某种顺序依次删除m个元素,输出每次删除一个元素之前统计整个序列的逆序对数。

10%的数据中,m,n≤100;

20%的数据中,m,n≤1000;

50%的数据中,m,n≤10000;

对于所有数据,m,n≤100000。

题目分析

找出未删除前所有的逆序对(用树状数组来完成比较快)。

当删除某个数时,删除与其有关逆序对即可。

这里是统计逆序对!   

    long long int ans=0;
    memset(sum,0,sizeof(sum));
    for(int i=n;i>=1;i--)
    {
        add(a[i],1);
        r[i]=getsum(a[i]-1);
        ans+=r[i]; 
    }
    memset(sum,0,sizeof(sum));
    for(int i=1;i<=n;i++)
    {
        add(n-a[i]+1,1);
        l[i]=getsum(n-a[i]);
    }

看懂了请自行跳过这一段!!!


没看懂的进来啊!!!

我们来举个例子吧!

such as:1 5 3 4 2。

表格是这样的!r表示在a[i]与a[i]有关的逆序对的值,aa表示出现的次数,zz表示数的值。

r00000
aa00000
zz12345

是从后面开始插入的哦!

for(int i=n;i>=1;i--)

插入2。

r00000
aa01000
zz12345

插入4。

r00010
aa01010
zz12345

插入3。

r00110
aa01110
zz12345

插入5。

r00113
aa01111
zz12345

插入1。

r00113
aa11111
zz12345

ans=1+1+3=5;

是的就是解释的就是这一段

    long long int ans=0;//记录逆序对的总数 
    for(int i=n;i>=1;i--)//记录数列右边比我小的,从右边开始插入 
    {
        add(a[i],1);
        r[i]=getsum(a[i]-1);//记录在它后面且比它小的数出现的次数,就是逆序对的个数 
        ans+=r[i]; 
    }

然后后面还有一段也怪怪的!l表示在a[i]与a[i]有关的逆序对的值。

表格的顺序是反过来的。就像这样。

r00000
aa00000
zz54321

还有一点不一样就是这次是从左边开始插入

for(int i=1;i<=n;i++)

插入1。

l00000
aa00001
zz54321

插入5。

l00000
aa10001
zz54321

插入3。

l00100
aa10101
zz54321

插入4。

l01100
aa11101
zz54321

插入2。

l01130
aa11111
zz54321

通过这些统计出与a[i]有关的逆序对。


但是你觉得出题人会有这么善良?!这道题就这样完了?!so easy?!

手动模拟发现删除的其实是有重复的,what?

在删除里的数找出其对应多删的逆序对,把减多的加回来就好啦!

        ans=ans-l[w]-r[w];//减去有关这个数的逆序对 
        ans+=solve(w);//把删去的重复的加回来 

问:哪里有重复的?!

比如说1 5 4 3 2,有6对逆序对(5,4),(5,3),(5,2),(4,3),(4,2),(3,2)。删去4,剩下逆序对为6-1-2=3。再删去3,剩下的逆序对为3-2-1=0。没了!?但是剩下的明明就是1 5 2,还有一对。怎么回事?因为(4,3)这组逆序对减了两次。要加回来的啊。
 

long long int solve(int x)
{
    long long int sss=0;//sss用来记录加回来的逆序对
    
    //分两部分处理
    sss+=ll(x);//处理前面的多减的逆序对数
    sss+=rr(x);//处理后面的多减的逆序对数
    
    op=1; v=a[x];
    for(int i=x;i<=n;i+=lowbit(i)) updata(root[i],1,n);//在序列上加上这个数 
    return sss; 
}

每删去一个数在相应有关的树状数套主席树组进行处理,根表示维护的范围,维护区间中数的个数,用主席树来维护计算即可。这么说可能还有点懵,我根本不知道你在说什么,那就抽出处理前面多减逆序对的代码,结合思路讲讲吧。

long long int ll(int x)
{
    //这时候所有的数都是在x左边的,找出比x大的即为删除的逆序对中重复的
    long long int sss=0;
    
    int tr1=0,aa[31];
    for(int i=x-1;i>=1;i-=lowbit(i)) aa[++tr1]=root[i];//记住记住记住重要的事情说三遍是第x-1棵线段树,因为找比x大的不包括x本身嘛
    //记录下与x-1的有关的主席树
    int l=1,r=n;
    while(l<r)
    {
        int mid=(l+r)/2;
        if(a[x]<=mid)//往左边跳,说明右边的数都比我大
        {
            for(int i=1;i<=tr1;i++)
            {
                sss+=tot[rs[aa[i]]];//加起来
                aa[i]=ls[aa[i]];//继续向左边找
            }
            r=mid;
        }
        else//往右边跳,说明左边的数都比我小,暂时还没有重复的逆序对
        {
            for(int i=1;i<=tr1;i++) aa[i]=rs[aa[i]];//继续向右边找
            l=mid+1;
        }
    }
    return sss;
}

 

代码

#include<cstdio>
#include<cstring>

int n,m,a[100010],h[100010],sum[100010],l[100010],r[100010];
int op,v,ls[10000010],root[100010],rs[10000010],t=0;
long long int tot[10000010];

int lowbit(int x)
{
    return x&(-x);
}

void add(int x,int s)
{
    while(x<=n)
    {
        sum[x]+=s;
        x+=lowbit(x);
    }
}
//以上是树状数组 

int getsum(int x)
{
    int s=0;
    while(x>0)
    {
        s+=sum[x];
        x-=lowbit(x);
    }
    return s;
}

void updata(int &now,int l,int r)
{
    if(now==0) now=++t;
    tot[now]+=op;
    if(l==r) return ;
    int mid=(l+r)/2; 
    if(v<=mid) updata(ls[now],l,mid);
    else updata(rs[now],mid+1,r);
}

long long int ll(int x)
{
    //这时候所有的数都是在x前的,所以要找出比x大的即为删除的逆序对中重复的 
    
    long long int sss=0;
    
    int tr1=0,aa[31];
    for(int i=x-1;i>=1;i-=lowbit(i)) aa[++tr1]=root[i];
    
    int l=1,r=n;
    while(l<r)
    {
        int mid=(l+r)/2;
        if(a[x]<=mid)//在左边,说明右子树的个数都比x大 
        {
            for(int i=1;i<=tr1;i++)
            {
                sss+=tot[rs[aa[i]]];
                aa[i]=ls[aa[i]];
            }
            r=mid;
        }
        else
        {
            for(int i=1;i<=tr1;i++) aa[i]=rs[aa[i]];
            l=mid+1;
        }
    }
    
    return sss;
}

long long int rr(int x)
{
    //这时候所有的数都是在x后的,所以要找出比x小的即为删除的逆序对中重复的 
    long long int sss=0;
    
    int tr1=0,tr2=0,aa[31],bb[31];
    for(int i=x;i>=1;i-=lowbit(i)) aa[++tr1]=root[i];
    for(int i=n;i>=1;i-=lowbit(i)) bb[++tr2]=root[i];
    
    int l=1,r=n;
    while(l<r)
    {
        int mid=(l+r)/2;
        if(a[x]<=mid)//在左边,说明右子树的个数都比x大 
        {
            for(int i=1;i<=tr1;i++) aa[i]=ls[aa[i]];
        	for(int i=1;i<=tr2;i++) bb[i]=ls[bb[i]];
            r=mid;
        }
        else
        {
            for(int i=1;i<=tr2;i++)
            {
                sss+=tot[ls[bb[i]]];
                bb[i]=rs[bb[i]];
            }
            for(int i=1;i<=tr1;i++)
            {
                sss-=tot[ls[aa[i]]];
                aa[i]=rs[aa[i]];
            }
            l=mid+1;
        }
    }
    
    return sss;
}

long long int solve(int x)
{
    long long int sss=0;//sss用来记录加回来的逆序对
    
    //分两部分处理 
    sss+=ll(x);//处理前面的多减的逆序对数 
    sss+=rr(x);//处理后面的多减的逆序对数
    
    op=1; v=a[x];
    for(int i=x;i<=n;i+=lowbit(i)) updata(root[i],1,n);//在序列上加上这个数 
    return sss; 
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        h[a[i]]=i; 
    }
    
    long long int ans=0;//记录逆序对的总数 
    
    memset(sum,0,sizeof(sum));
    for(int i=n;i>=1;i--)
    {
        add(a[i],1);
        r[i]=getsum(a[i]-1);
        ans+=r[i]; 
    }
    
    memset(sum,0,sizeof(sum));
    for(int i=1;i<=n;i++)
    {
        add(n-a[i]+1,1);
        l[i]=getsum(n-a[i]);
    }
    
    while(m--)
    {
        int x,w; scanf("%d",&x);
        printf("%lld\n",ans);//出删除前的逆序对个数
        w=h[x];
        ans=ans-l[w]-r[w];//减去有关这个数的逆序对 
        ans+=solve(w);//把删去的重复的加回来 
    }
    return 0;
} 

请记住最爱你的long long

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值