树状数组

数据结构是个好东西…我希望我能学的会

脑子也是个好东西…我希望我有

目录

一、概念

二、原理

1、lowbit()函数

2、单点修改和区间查询

3、区间修改和单点查询

4、区间修改和区间查询

三、特点


 

一、概念

树状数组或者二叉索引树也称作Binary Indexed Tree,又叫做Fenwick树;它的查询修改的时间复杂度都是log(n),空间复杂度则为O(n),这是因为树状数组通过将线性结构转化成树状结构,从而进行跳跃式扫描。通常使用在高效的计算数列的前缀和,区间和。

其中a数组就是原数组,c数组则是树状数组,可以发现

C1 = A1
C2 = A1+A2
C3 = A3
C4 = A1+A2+A3+A4
C5 = A5
C6 = A5+A6
C7 = A7
C8 = A1+A2+A3+A4+A5+A6+A7+A8

对照式子可以发现  C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k为i的二进制中从最低位到高位连续零的长度)例如i=8时,k=3;

 

二、原理

将树状数组转换成二进制(嗯...二进制是个好东西)

 

转换成二进制后可以发现,树状数组的节点深度其实就是它的节点编号的二进制形式中,从右往左数第一个1出现的位置。

例如:

6的二进制形式中(110),从右往左数第一个1出现的位置是2 

lowbit(6)=010=2
7的二进制形式中(111),从右往左数第一个1出现的位置是1 

lowbit(7)=001=1
8的二进制形式中(1000),从右往左数第一个1出现的位置是4

lowbit(8)=1000=8

那么现在引入久lowbit(x) ,lowbit(x) 其实就是取出x的最低位1 ,换言之,lowbit(x)=2^k

所以 C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+......A[i];

所以该结构满足以下性质:

1.每个内部结点c[x]保存以它为根的子树中所有叶结点的和

2.每个内部结点c[x]的子结点个数等于lowbit(x)的大小

3.除树根外,每个内部结点c[x]的父结点是c[x+lowbiet(x)]

4.树的深度为O(logN)

 

1、lowbit()函数

-t 代表t的负数,计算机中负数使用对应的正数的补码来表示
例如 :
 t=6(0110) 此时 k=1
-t=-6=(1001+1)=(1010)
 t&(-t)=(0010)=2=2^1

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

 

2、单点修改和区间查询

当修改一个结点之后,需要修改所有包含这个元素的结点,也就是说要修改一个元素就得修改这个元素及其父结点的值,由性质3易得

void add(int x,int v)
{
    while(x<=len)
    {
        c[x]+=v;
        x+=lowbit(x);
    }
}

 

查询是为了得到a[1]+a[2]+a[3]+...a[x]的值

int query(int x)
{
    int sum=0;
    while(x>0)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}

合并后

void add(int x,int v)
{
    while(x<=len)
    {
        c[x]+=v;
        x+=lowbit(x);
    }
}
int ask(int x)
{
    int sum=0;
    while(x>0)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
int range_ask(int l, int r){ //区间求和
    return ask(r) - ask(l - 1);
}

 

3、区间修改和单点查询

这里要用到差分的思想

创建一个差分数组c[],令c[i] = a[i] - a[i-1] (a[i] 表示原本的第i个数) 

则a[i] = ( a[i] - a[i-1] ) + ( a[i-1] - a[i-2] ) + ...... + ( a[2] - a[1] ) +a[1] 

         = c[i] + c[i-1] + ...... + c[2] + c[1] 

所以单点查询变成了区间求和那么区间修改怎么办呢 ?

我们看这样一个例子:

a 1 3 4 5 7 10

c 1 2 1 1 2 3

若我们令区间[2,4]加2,则

a 1 5 6 7 9 10

c 1 4 1 1 2 1 

我们可以发现只有c[2]和c[5]的数值改变了,其实原理也很好想,区间内的前后元素差是不变的,只有(区间第一个元素与前一个元素的差) 和 (区间后第一个元素与区间末尾元素的差)改变了。所以区间修改问题变成了单点修改问题。

假如原来数组都为A[6]=0 0 0 0 0 0,如果要在[2,4]区间加上5,那么只需将数组变成0 5 0 0 -5 0 ,i<2是不受+5的影响的,受+5影响的只有i>=2,也因为i>4都受到了+5的影响,所以i>4之后的数都要-5,那么原来的前缀和也变成了单点查询。

也就是说这里运用了差分思想,假设原本的数据存在a数组中,那么c数组储存的就是c[i]=a[i]-a[i-1],如果c[1]=a[1],那么很明显
a[i]=c[i]+c[i-1]+c[i-2]+...+c[2]+c[1]。这样我们每次单点查询的时候只要加上c数组的前缀就可以了

int lowbit(int x)
{
	return x&(-x);
}
void update(int x,int v)
{
	while(x<=n){
		c[x]+=v;
		x+=lowbit(x);
	}
}
int ask(int x)
{
	int sum=0;
	while(x){
		sum+=c[x];
		x-=lowbit(x);
	}
	return sum;
}
void range_update(int x,int y,int t)
{
	update(x,t);
	update(y+1,-t);
}

 

4、区间修改和区间查询

理解了上面的,这个也就很好理解啦

这里还是用到差分的思想,因为a[i]=sigma(c,i)

a[1]+a[2]+...+a[n]

= (c[1]) + (c[1]+c[2]) + ... + (c[1]+c[2]+...+c[n]) 

= n*c[1] + (n-1)*c[2] +... +c[n]

= n * (c[1]+c[2]+...+c[n]) - (0*c[1]+1*c[2]+...+(n-1)*c[n])    (式子①)

那么我们就维护一个数组c2[n],其中c2[i] = (i-1)*c[i]   

每当修改c的时候,就同步修改一下c2,这样复杂度就不会改变,那么

式子①=n*sigma(c,n) - sigma(c2,n)

int lowbit(int x)
{
	return x&(-x);
}
void update(int x,int v)
{
	while(x<=n){
		c1[x]+=v;
		c2[x]+=v*x;
		x+=lowbit(x);
	}
}
int ask(int x)
{
	int sum=0;
	while(x){
		sum+=(x+1)*c1[x]-c2[x];
		x-=lowbit(x);
	}
	return sum;
}
void range_update(int x,int y,int t)
{
	add(x,t);
	add(y+1,-t);
}
int range_ask(int l, int r){ //区间求和
    return ask(r) - ask(l - 1);
}

三、特点

树状数组的优点:

  1. 代码短小,实现简单;
  2. 容易扩展到高纬度的数据;

树状数组的缺点:

  1. 只能用于求和,不能求最大/小值;
  2. 不能动态插入;
  3. 数据多时,空间压力大;

注意树状数组能处理的是下标为1...n的数组,绝对不能出现下标为0的情况,因为lowbit(0)=0,会陷入死循环。

 

分享:https://www.cnblogs.com/RabbitHu/p/BIT.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值