树状数组作为一种数据结构,在OI竞赛中也是一项常用常考点,博主为使自己不忘记此数据结构,来写篇小博文
Above all,树状数组(Binary Indexed Tree(BIT), Fenwick Tree)是一个查询和修改复杂度都为 log2n 的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在 log2n 的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
树状数组的核心代码
关于树状数组有一段核心代码,即lowbit函数
int lowbit(int x)
{
return x&-x;
};
eg.1+lowbit(1)=2;
2+lowbit(2)=4;
//详见上图
此函数用来找当前树状数组的上一个或下一个数组位置,具体原理博主表示也不清楚,背过就好。
树状数组的经典操作
修改操作
void add(int x,int sum)//给a[x]加上个sum
{
while(x<=n)
{
c[x]+=sum;//给树状数组加上一个值
x+=lowbit(x);//找下一个数组的位置
};
};
查询操作
int read(int x)//查询(1,x)内的区间和
{
int ret=0;
while(x>=1)
{
ret+=c[x];
x-=lowbit(x);
};
return ret;
};
还有一个看似比较高端的操作
int read(int l,int r)//查询(l,r)内的区间和
{
int ret=0;
while(r>=l)
{
if(r-lowbit(r)<l)
{
ret+=a[r];
r-=1;
}else
{
ret+=c[r];
r-=lowbit(r);
};
}
return ret;
}
树状数组的实际应用
关于区间求和的程序
#include <cstdio>
#include <cstring>
#define nmax 10
using namespace std;
int a[nmax],c[nmax];
int n;
int lowbit(int x)
{ return x&-x;
}
void modify(int k,int num) //将第k个数修改成num
{
while(k<=n)
{
c[k]+=num;
k+=lowbit(k) ;
}
}
int add(int k)//1~k的区间和
{ int sum=0;
while(k>0)
{
sum+=c[k];
k-=lowbit(k);
}
return sum;
}
int main()
{ int m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{scanf("%d",&a[i]);
modify(i,a[i]);
}
int t,x,y;
for(int i=1;i<=m;i++)
{ scanf("%d%d%d",&t,&x,&y);
if(t==0)
{ modify(x,-a[x]);
modify(x,y);
a[x]=y;
}
else
printf("%d",add(y)-add(x-1));
}
return 0;
}
关于逆序对
求逆序的思路: 可以把数一个个插入到树状数组中, 每插入一个数, 统计比他小的数的个数getsum( data[i] ),对应的逆序对个数为 i- getsum( data[i] ),其中 i 为当前已经插入的数的个数, getsum( data[i] )为比 data[i] 小的数的个数,i- getsum( data[i] ) 即比 data[i] 大的个数, 即逆序的个数。最后需要把所有逆序对个数求和,就是在插入的过程中边插入边求和。
但如果数据比较大,就必须采用离散化方法, 假设输入的数组是9 1 0 5 4, 离散后的结果aa[] = {5,2,1,4,3};
在离散结果中间结果的基础上,那么其计算逆序数的过程是这么一个过程:
- 输入5, 调用upDate(5, 1),把第5位设置为1
1 2 3 4 5
0 0 0 0 1
计算1-5上比5小的数字存在么? 这里用到了树状数组的getSum(5) = 1操作,现在用输入的下标1 - getSum(5) = 0 就可以得到对于5的逆序数为0。
- 输入2, 调用upDate(2, 1),把第2位设置为1
1 2 3 4 5
0 1 0 0 1
计算1-2上比2小的数字存在么? 这里用到了树状数组的getSum(2) = 1操作,现在用输入的下标2 - getSum(2) = 1 就可以得到对于2的逆序数为1。
- 输入1, 调用upDate(1, 1),把第1位设置为1
1 2 3 4 5
1 1 0 0 1
计算1-1上比1小的数字存在么? 这里用到了树状数组的getSum(1) = 1操作,现在用输入的下标 3 - getSum(1) = 2 就可以得到对于1的逆序数为2。
- 输入4, 调用upDate(4, 1),把第4位设置为1
1 2 3 4 5
1 1 0 1 1
计算1-4上比4小的数字存在么? 这里用到了树状数组的getSum(4) = 3操作,现在用输入的下标4 - getSum(4) = 1 就可以得到对于4的逆序数为1。
- 输入3, 调用upDate(3, 1),把第3位设置为1
1 2 3 4 5
1 1 1 1 1
计算1-3上比3小的数字存在么? 这里用到了树状数组的getSum(3) = 3操作,现在用输入的下标5 - getSum(3) = 2 就可以得到对于3的逆序数为2。
- 0+1+2+1+2 = 6 这就是最后的逆序数
离散化的方式:
struct Node
{
int val;
int pos;
};
Node node[500005];
int reflect[500005];
val存放原数组的元素,pos存放原始位置,即node[i].pos = i。
把这些结构体按照val的大小排序。
reflect数组存放离散化后的值,即reflect[node[i].pos] = i。
这样从头到尾读入reflect数组中的元素,即可以保持原来的大小关系,又可以节省大部分空间。
- 下面来看具体程序
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 500005;
struct Node
{
int val;
int pos;
};
Node node[N];
int c[N], reflect[N], n;
bool cmp(const Node& a, const Node& b)
{
return a.val < b.val;
}
int lowbit(int x)
{
return x & (-x);
}
void update(int x)
{
while (x <= n)
{
c[x] += 1;
x += lowbit(x);
}
}
int getsum(int x)
{
int sum = 0;
while (x > 0)
{
sum += c[x];
x -= lowbit(x);
}
return sum;
}
int main()
{
while (scanf("%d", &n) != EOF && n)
{
for (int i = 1; i <= n; ++i)
{
scanf("%d", &node[i].val);
node[i].pos = i;
}
sort(node + 1, node + n + 1, cmp);//排序
for (int i = 1; i <= n; ++i) reflect[node[i].pos] = i;//离散化
for (int i = 1; i <= n; ++i) c[i] = 0;//初始化树状数组
long long ans = 0;
for (int i = 1; i <= n; ++i)
{
update(reflect[i]);
ans += i - getsum(reflect[i]);
}
printf("%lld\n", ans);
}
return 0;
}
最后,希望大家能够看懂
感谢惠读