树状数组(求逆序对)

一、树状数组是什么

树状数组,又称二进制索引树,英文名Binary Indexed Tree

之前遇到一个求逆序对的题,看了很多题解都只说了这个树状数组,关于怎么实现的全都避而不谈,我研究了一下午,总算搞出个头绪了

一般用来求前缀和,可以把时间复杂度从O(n)降到O(log10 n)非常恐怖,举个例子,假如我们要求从1~1000的前缀和,普通方法需要遍历1000次,而树状数组只需要遍历5次,

先上图;

树状数组(求逆序对)_第1张图片

又称二进制索引树的几个特点:

  1. 底部确定,顶部无穷大
  2. 最外面的结点是2的n次方,如上图1,2,4,8的结点
  3. 奇数的结点一定是叶子结点
  4. 数组一定要从1开始

 

图中每一个非叶子节点都代表一个区间的和,

a[4]代表的是a[1]+a[2]+a[3]+a[4],

a[5]就是自己a[5]

例如我们需要求1~11的和,我们就只需要求出a[11] ,a[10] ,a[8]的和,

同样的,假如求1~1000的和,那么只需要求a[1000] ,a[992] ,a[960], a[896], a[768],a[512]的和,

可以看出每次最后的一个结点必定是2的次方

因为只需要跳到最外面的一个结点就不需要在往下求了,而最外面的结点一定是2的n次方,不懂得可以根据那个图模拟一下。

 

二、树状数组怎么实现:

lowbit:

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

 

相信很多童鞋看到这个东西都是一脸懵逼,而我就不一样了,我TM是十脸懵逼,那让我们来推测一下:

首先根据英语意思理解,low就是低,bit就是位,连起来就是最低位,其实就是求的一个数的二进制的第一个1的位置,

比如6,它的二进制:0110,从右往左第一个1的位置是1(从零开始),那么lowbit(6)就等于2的1次方就等于2,

再比如9,它的二进制:1001,从右往左第一个1的位置是0,那么lowbit(6)就等于2的0次方就等于1,

 

我们再来看一下实现原理:

x&-x到底是个什么玩意

要知道,二进制数的负数是正数取反加一,那么-6就是1010,-6&6 就是 1010 & 0110==0010,换算成十进制就是2,所以lowbit(6)==2

 

现在实现原理知道了,那么这玩意跟树状数组有什么关系,请听下回讲解:

lowbit(x)是用来跳到x的下一个结点或者上一个结点的的介质,

比如lowbit(6)==2,那么上一个结点就是6-lowbit(6)==4,下一个结点是是6+lowbit(6)==8,可以看一下上面的那个图

这里说的上一个结点不是父节点,而是你要求的前缀和的管辖区间的结点,比如我们要求1~6的和:

  1. 首先x=6,sum+=a[6],上图中a[6]代表的是a[5]+a[6]
  2. x=6-lowbit(6)==4,sum+=a[4],a[4]代表的是a[1]+a[2]+a[3]+a[4]
  3. x=4-lowbit(4)==0,0小于1不符合条件了,退出

此时sum==[1]+a[2]+a[3]+a[4]+a[5]+a[6],是不是很完美

 

如果我们要更新某一个点的值,那么所有把这个点包含在内的区间结点都需要更新,比如我们要使a[6]+1:

  1. 首先x=6,a[6]++
  2. x=6+lowbit(6)==8,a[8]++
  3. x=8+lowbit(8)==16, a[16]++
  4. .........直到x>n,(n是整个序列的长度)

由于这个树是没有上界的,题目一般都会给出一个序列的长度为n

 

是不是感觉很强大,我只能这样说明它,不懂怎么证明它,感觉发明这些算法的人简直是个天才,正所谓那句话:社会是由百分之一的天才创造的,百分之九十九的人推动的,

关于为什么只能求前缀和,不能求区间和,可以仔细看一下上图很快就会明白,如果一旦到最外面的结点(2的n次方),那么x必定等于lowbit(x),x-lowbit(x)必定等于0,就会跳出累加,而最外面的结点x,一定代表的是1~x的和,就是前缀和,如果求区间和只能拿右边界的前缀和减去左边界的前缀和了

但是这个结构还有一个缺点,就是a[8]代表的是a[1~8]的和,所以我们并不知道a[8]具体是多少,要避免这种情况,就需要另外开一个与原序列长度相同的数组 t,用来存放树,原数组不动,

根据上图,a数组是最下面的一排叶子结点,用 t 数组来存储除叶子结点的树,

细心的同学可以发现,树结点的序号都是偶数,奇数的叶子结点都是一个单独的区间

如果要查询区间和的话,只需要判断t中的这个结点是不是偶数或者是不是等于0,这里就不再bb了

 

怎么求逆序对

逆序对的定义:ia[j]

要求一个数组的逆序对,可以用归并排序的概念加一个累加就行,具体看这个例子https://blog.csdn.net/qq_41431457/article/details/88944840,我们这里主要介绍的是树状数组求逆序对

首先明白一个概念叫做离散化(Discretization)

在上面介绍的树状数组中,只需要开一个与原序列中最大元素相等的长度数组就行,那么如果我的序列是1,5,3,8,999,本来5个元素,却需要开到999这么大,造成了巨大的空间浪费,

离散化就是另开一个数组,d, d[i]用来存放第i大的数在原序列的什么位置,比如原序列a={5,3,4,2,1},第一大就是5,他在a中的位是1,所以d[1]=1,同理d[2]=3,········所以d数组为{1,3,2,4,5},

转换之后,空间复杂度就没这么高了,但不是求d中的逆序对了,而是求d中的正序对,来看一下怎么求的:

  1. 首先把1放到树状数组t中,此时t只有一个数1,t中比1小的数没有,sum+=0
  2. 再把3放到树状数组t中,此时t只有两个数1,3,比3小的数只有一个,sum+=1
  3. 把2放到树状数组t中,此时t只有两个数1,2,3,比2小的数只有一个,sum+=1
  4. 把4放到树状数组t中,此时t只有两个数1,2,3,4,比4小的数有三个,sum+=3
  5. 把5放到树状数组t中,此时t只有两个数1,2,3,4,5,比5小的数有四个,sum+=4

最后算出来,总共有9个逆序对,可以手算一下原序列a,也是9个逆序对,

具体实现:

  1. 另d[1]=1, d[2]=2········a[n]=n
  2. 根据原数组a中的元素的大小进行排序

实现代码:

#include
#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;
} 

 

离散化之后,就是求和了,

根据上面的步骤每一次把一个新的数x放进去之后,都要求比他小的元素有几个,而比他小的元素个数一定是1到x中存在数的个数,也就是[1 , x-1]中有几个数,是不是很耳熟,有点像之前讲的前缀和了,只不过树状数组t表是的不是前缀和了,t[x]表示的是[1,x]中有几个数已经存在,这样我们每次把一个新的数x放进去的时候,都需要把包含这个数的结点更新,然后查询[1,x-1]有几个数已经存在。

还是拿上一个例子:

  1. 把1放进去,包含t[1]的结点t[1]++,t[2]++、t[4]++, 由于n==5,算到t[8]的时候就已经跳出,查询[1,1-1]中比他小的数为0
  2. 把3放进去, 包含t[3]的结点t[3]++,  t[4]++  ,然后查询[1 , 3-1]中有几个数已经存在,t[2]==1, sum+=1,lowbit一发等于0跳出
  3. 把2放进去, 包含t[2]的结点t[2]++,  t[4]++ , 然后查询[1 , 2-1]中有几个数已经存在,t[1]==1, sum+=1,lowbit一发等于0跳出
  4. 把4放进去,包含t[4]的结点t[4]++,   t[8]大于n 跳出, 查询[1 , 4-1]中有几个数已经存在,t[3]==1, sum+=1,lowbit一发t[2]==2,所以sum+=2
  5. 把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
#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<

求前缀和版本:

#include
#define lowbit(x) (x&-x)
using namespace std;

const int N = 1000;
int t[N<<2], n;

void add(int x, int id)
{
	for(int i = id; i <= n; i += lowbit(i))
	t[i] += x;
}

int sum(int id)
{
	int res = 0;
	for(int i = id; i > 0; i-= lowbit(i))
	res += t[i];
	
	return res;
}

int main()
{
	n = 10;
	for(int i = 1; i <= 10; i++)
	{
		int x;
		cin >> x;
		add(x, i);
	}
	
	while(1)
	{
		int x;
		cin >> x;
		cout << sum(x) << endl;
	}
	
	return 0;
}

不要以为那个while循环会执行很多次,就算x==1000,也只会遍历5次!!!,

天才创造了世界~

码字不易,点个赞再走呗

你可能感兴趣的:(经典算法)