树状数组求逆序对

树状数组求逆序对
任意给定一个集合a,如果用b[val]保存数值val在集合a中出现的次数,那么数组b在[l,r]上的区间和就表示集合a中范围在[l,r]内的数有多少个。

我们可以在集合a的数值范围上建立一个树状数组,来维护b的前缀和。这样即使在集合a中插入或删除一个数,也可以高效的统计。

也就是以前用普通方法维护b[ ]的前缀和,现在用树状数组来维护t的前缀和。

对于一个序列a,若i<j且a[i]>a[j],则称a[i]与a[j]构成逆序对。

利用树状数组求逆序对的算法思路:
1、在序列a的数值范围上建立树状数组,初始化全为0。
2、倒序扫描给定的序列a,对于每个数a[i]:
(1)、在树状数组中查询前缀和[1,a[i]-1],累加到答案ans中。
(2)、执行“单点增加”操作,即把位置a[i]上的数加1
(相当于b[a[i]]++),同时正确维护t的前缀和,这表示a[i]又出现一次。
3、ans即为所求。

算法原理:
在这个算法中,因为倒序(就是从后往前扫描数组,并没有排序)扫描,“已经出现过的数"就是原序列a[i]后边的数,所以我们通过树状数组查询的内容就是”每个a[i]后边有多少个比它小“。也就是查询[1,a[i]-1]区间有多少个比a[i] 小(1到a[i]-1区间分别统计的是1的个数、2的个数…a[i]-1的个数),每次查询的结果之和当然就是逆序对的个数。
举例:假设a[i]=10,那么就是用树状数组统计1的个数,2的个数
3的个数,4的个数…9的个数,把这些个数加起来,那就是用树状数组维护前缀和。

就像我在树状数组基础那篇文章中,树状数组起初为0,那个值有变动,就用单点修改来维护树状数组。这样每扫描到一个a[i],
这时a[i]其实代表的是树状数组的一个区间,树状数组就会更新区间a[i]~n,每次增加lowbit(),如下面代码
void add(int x){
while(x<500005){
b[x]++;
x+=lowbit(x);
}
}

树状数组中存放数的形式就像“桶排序”,如果a[i]的值是10,那么就调用单点修改,把b[a[i]]也就是b[10]这个小区间,同时把10以后相关的小区间都修改(区间变化x=x+lowbit(x))

用树状数组的单点修改,区间求和来
完整代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
int a[500005],b[500005],n;
//a是原序列,b是树状数组,当a[]数组元素值很大时,b数组开的很大
//假如a[i]的最大值是2100000000,那么b数组就得开b[2100000000+7]
//超出范围,需要离散化 
int lowbit(int x){
 return x&(-x);
}
void add(int x){
 while(x<500005){
  b[x]++;
  x+=lowbit(x);
 }
}
int  getsum(int x){
 long long sum=0;
 while(x>0){
  sum+=b[x];
  x-=lowbit(x);
 }
 return sum;
}
int main()
{
 long long n,i,ans;
 while(cin>>n){//n为数的总数.
  ans=0;
  memset(b,0,sizeof(b));
  /* 顺着扫描原序列 
  for(i=1;i<=n;i++){
   cin>>a[i];
   add(a[i]);
   ans+=i-(getsum(a[i]));//当前加入b的元素个数减去a[i]及其前面的元素个数
  }
  */
  //倒序扫描原序列 
  for(i=1;i<=n;i++)
     cin>>a[i];
  for(i=n;i>=1;i--){
     add(a[i]);
     ans+=getsum(a[i]-1);//统计出比a[i]小的数的个数,只不过是用树状数组统计
  }
  cout<<ans<<endl;
 }
}

有的不明白为什么要用树状数组?
当数据的范围较小时,比如maxn=100000,那么我们可以开一个数组c[maxn],来记录前面数据的出现情况,初始化为0;

当数据a出现时,就令c[a]+=1。这样的话,欲求某个数a的逆序数,只需要算出在当前状态下c[a+1,maxn]中有多少个1,因为这些位置的数在a之前出现且比a大。
但是若每添加一个数据a时,就得从a+1到 maxn搜一遍,复杂度太高了。

树状数组却能很好的解决这个问题,可以把数一个个插入到树状数组中, 每插入一个数, 统计比他小的数的个数,对应的逆序为 i - getsum( c[i] ),其中 i 为当前已经插入的数的个数, getsum( c[i] )为比 c[i] 小的数的个数,i- getsum( c[i] ) 即比c[i] 大的个数, 即逆序的个数。最后需要把所有逆序数求和,就是在插入的过程中,边插入边求和(统计比他小的个数是顺序扫描数组)。

前面的方法很简单,但是限制实在太多,比如只能求正整数的逆序对(b的下标不能为负数),如果a中的元素最大值很大,就会超时。
那么就需要进行离散化。
首先明白一个概念叫做离散化(Discretization) 在上面介绍的树状数组中,只需要开一个与原序列中最大元素相等的长度数组就行,那么如果我的序列是1,5,3,8,999,本来5个元素,却需要开到999这么大,造成了巨大的空间浪费。

离散化就是另开一个数组d, d[i]用来存放第i大的数在原序列的什么位置。
比如原序列
a={5,3,4,2,1},
对应位置 1, 2,3,4,5
排序后a={5,4,3,2,1}
d[ ] 位置 1,3,2,4,5
d数组保存的是从大到小排序后,这个数在序列的位置。
然后用树状数组t[ ]来维护d数组

第一大就是5,他在a中的位是1,所以d[1]=1,同理d[2]=3,········所以d数组为{1,3,2,4,5}, 转换之后,空间复杂度就没这么高了,但不是求d中的逆序对了,而是求d中的正序对,来看一下怎么求的:
首先把1放到树状数组t中,此时t只有一个数1,t中比1小的数没有,sum+=0
再把3放到树状数组t中,此时t只有两个数1,3,比3小的数只有一个,sum+=1
把2放到树状数组t中,此时t只有两个数1,2,3,比2小的数只有一个,sum+=1
把4放到树状数组t中,此时t只有两个数1,2,3,4,比4小的数有三个,sum+=3
把5放到树状数组t中,此时t只有两个数1,2,3,4,5,比5小的数有四个,sum+=4
最后算出来,总共有9个逆序对,可以手算一下原序列a,也是9个逆序对

离散化代码

#include<bits/stdc++.h>
#define M 500005
using namespace std;
int a[M],d[M],t[M],n;//原数组/ 离散化后的数组/ 树状数组 
bool cmp(int x,int y)
{	if(a[x]==a[y]) 
       return x>y;//避免元素相同 	
       return a[x]>a[y];//按照原序列第几大排列 
}
int mian()
{	cin>>n;	
for(int i=1;i<=n;i++)	
cin>>a[i],
d[i]=i;//初始化	
sort(d+1,d+n+1,cmp);	
//排序时候d就是离散化的数组了        
 return 0;
 } 

用树状数组维护d数组过程演示:
根据上面的步骤每一次把一个新的数x放进去之后,都要求比他小的元素有几个,而比他小的元素个数一定是1到x中存在数的个数,也就是[1 , x-1]中有几个数,是不是很耳熟,有点像之前讲的前缀和了,只不过树状数组t表是的不是前缀和了。t[x]表示的是[1,x]中有几个数已经存在,这样我们每次把一个新的数x放进去的时候,都需要把包含这个数的结点更新,然后查询[1,x-1]有几个数已经存在。
还是拿上一个例子:
把1放进去,包含t[1]的结点t[1]++,t[2]++、t[4]++, 由于n==5,算到t[8]的时候就已经跳出,查询[1,1-1]中比他小的数为0
把3放进去, 包含t[3]的结点t[3]++, t[4]++ ,然后查询[1 , 3-1]中有几个数已经存在,t[2]==1, sum+=1,lowbit一直等于0跳出
把2放进去, 包含t[2]的结点t[2]++, t[4]++ , 然后查询[1 , 2-1]中有几个数已经存在,t[1]==1, sum+=1,lowbit一直等于0跳出
把4放进去,包含t[4]的结点t[4]++, t[8]大于n 跳出, 查询[1 , 4-1]中有几个数已经存在,t[3]==1, sum+=1,t[2]==2,所以sum+=2,lowbit一直等于0跳出
把5放进去, 包含t[5]的结点t[5]++, t[6]大于n跳出 , 查询[1 , 5-1]中有几个数已经存在,t[4]==4, sum+=4,lowbit一直等于0跳出最后答案就出来了。
关键是要理解加粗了的那句话,不是前缀和,而是有几个数已经存在,假如a[8]等于4,那么就表示[1,8]中只有4个数存在。

完整代码:

#include<bits/stdc++.h>
#define M 500005
using namespace std;
int a[M],d[M],t[M],n;
int lowbit(int x)
{ return x&-x;
}
int add(int x)//把包含这个数的结点都更新 
{ while(x<=n)//范围 
 { t[x]++;
  x+=lowbit(x);
 }
}
int sum(int x)//查询1~X有几个数加进去了 
{ int res=0;
 while(x>=1)
 { res+=t[x];
  x-=lowbit(x);
 }
 return res;
}
bool cmp(int x,int y)//离散化比较函数 
{ if(a[x]==a[y]) return x>y;//避免元素相同 
 return a[x]>a[y];//按照原序列第几大排列 
}
int main()//402002139
{ //freopen("in.txt","r",stdin);
 long long ans=0;
 cin>>n;
 for(int i=1;i<=n;i++)
 cin>>a[i],d[i]=i;
 sort(d+1,d+n+1,cmp);//离散化 
 for(int i=1;i<=n;i++)
 { add(d[i]);//把这个数放进去 
  ans+=sum(d[i]-1);//累加 
 }
 cout<<ans;
 return 0;
}

可以用结构体代替,少开一个d数组,看下面的代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
int b[500005],n,t[500005];
struct node{
 int pos,v;
}a[500005];
bool cmp(node x,node y){
 return x.v<y.v;
}
int lowbit(int x){
 return x&(-x);
}
void add(int x){
 while(x<500005){
  b[x]++;
  x+=lowbit(x);
 }
}
int  getsum(int x){
 long long sum=0;
 while(x>0){
  sum+=b[x];
  x-=lowbit(x);
 }
 return sum;
}
int main()
{
 long long n,i,ans,cnt;
 while(cin>>n){//n为数的总数.
  ans=0;
  memset(b,0,sizeof(b));
  for(i=1;i<=n;i++){
   cin>>a[i].v;
   a[i].pos=i;
  }
  sort(a+1,a+n+1,cmp);
  cnt=1;
  for(i=1;i<=n;i++){
   if(i!=1&&a[i].v!=a[i-1].v)
   cnt++;
   t[a[i].pos]=cnt;
  }
  for(i=1;i<=n;i++){
   add(t[i]);
   ans+=(i-getsum(t[i]));
  }
  cout<<ans<<endl;
 }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值