中文名:树状数组
英文名:Binary Indexeds Tree
别名:二进制索引树
一、树状数组是干什么的?
例:在一个【1,1000000000】的区间上改变一个位置上的值,并且求n次【x,y】内(共n个元素)的区间和;
正常思路(反正我就这么做T-T):更改进行1次操作,时间复杂度O(1),查询n次操作,时间复杂度O(n),并且n次查询区间和,所以时间复杂度O(n^2); 肯定会TLE。提供两种方法一种差分数组一种树状数组。
树状数组:对数组进行维护查询的操作,比较常见的如,修改某点的值、求某个区间的和,而这两种恰恰是树状数组的强项!当然,数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(MN),当规模增大后这是划不来的!而树状数组干同样的事复杂度却是O(MlgN)。
二、原理
a数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到a数组,c数组才是我们全程关心和操纵的重心。先由图来看看c数组的规则,其中c8 = c4+c6+c7+a8,c6 = c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明她的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被lg了呢?可以看到,c8可以看作a1~a8的左半边和+右半边和,而其中左半边和是确定的c4,右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分。
这时候先介绍一个函数:lowbit函数
lowbit这个函数的功能就是求某一个数的二进制表示中最低的一位1。把这个数的二进制写出来,然后从右向左找到第一个1(这个1就是我们要求的结果,但是现在表示不出来,后来的操作就是让这个1能表示出来),这个1不要动和这个1右边的二进制不变,左边的二进制依次取反,这样就求出的一个数的补码,举个例子,x = 6,它的二进制为0110,那么lowbit(x)就返回2,因为最后一位1表示2。6的二进制位0110,-6的二进制为-0110 + 1 = 1010, 0110&(1010)= 0010 = 2;
lowbit的用处:用来联系数组a, c。数组c[x]表示对a数组从a[x]开始从左到右的lowbit[x]个数的和。比如c[0110]=a[0110]+a[0101],就是从110开始计算了0010个数的和,因为lowbit(0110)=0010,可以看到其实只有低位的1起作用,因为很显然可以写出c[0010]=a[0010]+a[0001]。
int lowbit(int x)
{
return x&(-x);
}
既然关系建立好了,看看如何实现a某一个位置数据跟改的,她不会直接改的(开始就说了,a根本不存在),她每次改其实都要维护c数组应有的性质,因为后面求和要用到。而维护也很简单,比如更改了a[3],我们接着要修改c[3],c4],c[8],这是很容易从图上看出来的,但是你可能会问,他们之间有申明必然联系吗?每次求解总不能总要拿图来看吧?其实从0011——>0100——>1000的变化都是进行“去尾”操作,又是自己造的词- -’’,就是把尾部应该去掉的1都去掉转而换到更高位的1,记住每次变换都要有一个高位的1产生,所以0100是不能变换到0101的,因为没有新的高位1产生,这个变换过程恰好是可以借助我们的lowbit进行的,k +=lowbit(k)。
c的构成性质(其实是分组性质)决定了c[0011]只会直接影响c[0100],而c[0100]只会直接影响[1000],而下表之间的关系恰好是也必须是k +=lowbit(k)。此时我们就是写出跟新维护树的代码:
void add(int k,int num)
{
while(k<=n)
{
tree[k]+=num;
k+=k&-k;
}
}
求和
int read(int k)//1~k的区间和
{
int sum=0;
while(k)
{
sum+=tree[k];
k-=k&-k;
}
return sum;
}
区域求和
int query_read(int l, int r)//区间查询
{
return read(r) - ask(l - 1);
}
例题:2014年第五届蓝桥杯C/C++程序设计本科B组省赛第十题 小朋友排队(编程大题)
题目链接:http://www.dotcpp.com/oj/problem1439.html
小朋友排队
n 个小朋友站成一排。现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。
每个小朋友都有一个不高兴的程度。开始的时候,所有小朋友的不高兴程度都是0。
如果某个小朋友第一次被要求交换,则他的不高兴程度增加1,如果第二次要求他交换,则他的不高兴程度增加2(即不高兴程度为3),依次类推。当要求某个小朋友第k次交换时,他的不高兴程度增加k。
请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。
如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。
【数据格式】
输入的第一行包含一个整数n,表示小朋友的个数。
第二行包含 n 个整数 H1 H2 … Hn,分别表示每个小朋友的身高。
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。
例如,输入:
3
3 2 1
程序应该输出:
9
【样例说明】
首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。
【数据规模与约定】
对于10%的数据, 1<=n<=10;
对于30%的数据, 1<=n<=1000;
对于50%的数据, 1<=n<=10000;
对于100%的数据,1<=n<=100000,0<=Hi<=1000000。
资源约定:
峰值内存消耗 < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意: main函数需要返回0
注意: 只使用ANSI C/ANSI C++ 标准,不要调用依赖于编译环境或操作系统的特殊函数。
注意: 所有依赖的函数必须明确地在源文件中 #include , 不能通过工程设置而省略常用头文件。
提交时,注意选择所期望的编译器类型。
思路:树状数组
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define N 100010
#define MAX 1000100
int C[MAX], S[MAX], b[N];
long long total[N], ans;
int num[N], T, s, t, i, j;
int Lowbit(int x){
return x&(x^(x-1));
}
void add(int pos,int num,int *P) {
while(pos <= MAX) {
P[pos] += num;
pos += Lowbit(pos);
}
}
int Sum(int end,int *P) {
int cnt = 0;
while(end > 0) {
cnt += P[end];
end -= Lowbit(end);
}
return cnt;
}
void init(){
total[0] = 0;
for(i = 1; i < N; ++i){
total[i] = total[i-1] + i;
}
}
int main() {
init();
while(~scanf("%d",&T)) {
memset(C,0,sizeof(C));
memset(S,0,sizeof(S));
//memset(num,0,sizeof(num));
//memset(b,0,sizeof(b));
//ans = 0;
for(j = 0; j < T; j ++) {//因为第一个数前面比它小的数没有,所以j要从0开始
scanf("%d",&num[j]);
add(num[j]+1,1,C);
b[j] = j - Sum(num[j], C);//Sum(num[j],C)求的就是小于s的个数,j - Sum(num[j],C)就是前j个数中大于num[j]的个数
b[j] -= Sum(num[j]+1,C) - Sum(num[j],C)-1;
//printf("%d ",b[j]);
}
//printf("\n");
ans = 0;
for(j = T-1; j > -1; --j){//反过来求第j个数右边中小于它的数的个数。
add(num[j]+1 ,1, S);
b[j] += Sum(num[j] ,S);//Sum(num[j],S)求的就是小于num[j]的个数
//b[j] -= Sum(num[j]+1,S) - Sum(num[j],S)-1;
//printf("%d ",b[j]);
ans += total[b[j]];
}
//printf("\n");
printf("%I64d\n",ans);
}
return 0;
}