一. 树状数组介绍
1)性质
树状数组本质上就是一个数组,它与普通数组不同之处在于它的某些元素维护的是一段区间的信息,已区间和为例,若i为奇数,则第i个元素就是源数据的第i个元素,若i为偶数,则第i个元素维护的是[i-2^k+1, i]这段区间的和,k代表i的二进制末尾0的个数。
2)作用
树状数组常用来求解一些查询区间和相关的问题,还可以用来高效的求解逆序数问题。
二. 树状数组基本操作
1)找到x二进制下等于1的最低位
int lowbit(int x) {
return x & (-x);
}
证明:负数在计算机中是以补码的形式存在的,通过原码取反加1得到
1. 若x的第一位为1,取反加1后仍为1,1&1=1,即如果x是一个奇数则lowbit(x)=1
2. 若x的第一位为0,取反加1后等于2,需要进位,若x的第二位也为0,则继续进位,直到x的某一位为1为止,假设其第i位为1,取反后值为0,0加上进位的1等于1,1&1=1,即如果x是一个偶数,lowbit(x)=其末尾连续0的个数+1
2)更新操作,将第pos个位置的值加上val
void Update(int pos, int val) {
for (int i = pos; i <= n; i += lowbit(i)) {
c[i] += val;
}
}
3)查询操作,查询区间[1, pos]的和
int Query(int pos) {
int ans = 0;
for (int i = n; i > 0; i -= lowbit(i)) {
ans += c[i];
}
return ans;
}
三. 树状数组求逆序数
1)逆序数问题定义
给一个1~n的排列,求满足i
2)树状数组求逆序数的原理
首先明确树状数组在此问题中维护信息是某个区间中数字出现的个数,将源数据按其原本顺序插入树状数组,第i个数字插入的方式为将树状数组的第a[i]位设为1,同时更新覆盖到它的父区间,Query(a[i])可求得[1, a[i]]的区间和,这恰好代表第i个数字前小于等于它的个数,等于的只可能是自身,故小于它的有Query(a[i])-1个,那么大于它的显然就有i-1-(Query(a[i])-1) = i-Query(a[i])个
for (int i = 1; i <= n; i++) {
Update(a[i], 1);
ans += i - Query(a[i]);
}
3)模拟计算过程
源数据
下标 1 2 3 4 5
数值 4 2 1 5 3
第一步:
Update(4, 1),树状数组:0 0 0 1 0
Query(4)=1, 1-Query(4)=0
(4是第1个插入的, 其前面没有1)
第二步:
Update(2, 1),树状数组:0 1 0 2 0
Query(2)=1, 2-Query(2)=1
(2是第2个插入的, 其前面没有1, 故第1个插入的数字必然比它大, 找到1组逆序<4,2>)
第三步:
Update(1, 1),树状数组:1 2 0 3 0
Query(1)=1, 3-Query(1)=2
(1是第3个插入的, 其前面没有1, 故前2个插入的数字必然比它大, 找到2组逆序<4,1>, <2,1>)
第四步:
Update(5, 1),树状数组:1 2 0 3 1
Query(5)=4, 4-Query(5)=0
(5是第4个插入的, 其前面有3个1, 故它是当前最大的数字, 不和之前的数构成逆序)
第五步:
Update(3, 1),树状数组:1 2 1 4 1
Query(3)=3, 5-Query(3)=2
(3是第5个插入的, 其前面有2个1, 故前4个数中2个比它大, 2个比它小, 找到2组逆序<4,3>, <5,3>)
累加每步的结果后可得逆序数为5,用树状数组的好处在于Update操作和Query的操作时间复杂度都为O(logn),非常的巧妙