树状数组(吃不懂私聊我)

一.什么是树状数组

树状数组(Binary Indexed Tree, Fenwick Tree),是一种用于高效处理对一个存储数字的列表进行更新及求前缀和、区间和的数据结构。
什么是前缀和以及区间和?前缀和是一个数组的某项下标之前(包括此项元素)的所有数组元素的和。 区间和就是给定区间的数组的和。
它的查询和修改的时间复杂度都是log(n),空间复杂度则为O(n)。这正是使用树状数组的原因。
(碰到更新元素、求前缀和、区间和就可以考虑树状数组)。

顾名思义就是一个结构为树形结构的数组,于二叉树的结构类似但又不同,它是在二叉树的结构上删除了一些中间节点,来看两幅图就明白了。

树状数组相比于二叉树删除了一些节点,但是为什么要删除呢?这就和树状数组的一些性质(lowbit)有关了,不懂没关系,继续往下看。

树状数组可以解决什么问题呢?
可以解决大部分区间上面的修改以及查询的问题,例如1.单点修改,单点查询,2.区间修改,单点查询,3.区间查询,区间修改,换言之,线段树能解决的问题,树状数组大部分也可以,但是并不一定都能解决,因为线段树的扩展性比树状数组要强.

树状数组和线段树的区别在哪?
有人会问了既然线段树的问题能够用树状数组解决而且线段树还比树状数组扩展性强,那为什么不直接用线段树呢?问的很好,树状数组的作用就是为了简化线段树,举个例子:一个问题可以用线段树解决写代码半个小时,但是用树状数组只需要10分钟,那么你会选择哪一个算法呢?没错,基于某些简单的问题,我们没必要用到功能性强但实现复杂的线段树(杀鸡焉用宰牛刀).

树状数组的优点
优点:修改和查询操作复杂度于线段树一样都是logN,但是常数比线段树小,并且实现比线段树简单

缺点:扩展性弱,线段树能解决的问题,树状数组不一定能解决。

二.树状数组讲解

一.lowbit运算
如何计算一个非负整数n在二进制下的最低为1及其后面的0构成的数?
 二.树状数组结构分析

上面时树状数组的结构图,t[x]保存以x为根的子数中叶子节点值的和,原数组为a[]
那么原数组前4项的和t[4]=t[2]+t[3]+a[4]=t[1]+a[2]+t[3]+a[4]=a[1]+a[2]+a[3]+a[4],看似没有什么特点,别着急往下看

 我们通过观察节点的二进制数,进一步发现,树状数组中节点x的父节点为x+lowbit(x),例如t[2]的父节点为t[4]=t[2+lowbit(2)]

三.单点修改,区间查询

所以我们在单点修改的同时,更新父节点就变得尤为简单,,例如我们对a[1]+k,那么祖先节点t[1],t[2],t[4],t[8]都需要+k更新(因为t[]表示前缀和),此时我们就可以用lowbit操作实现.

那么单点修改实现了,如何实现区间查询呢?
例如:我们需要查询前7项的区间和sum[7]

 

 通过图中不难看出,sum[7]=t[7]+t[6]+t[4] ,我们进一步发现,6=7-lowbit(7),4=6-lowbit(6),所以我们可以通过不断的-lowbit操作来实现求和

这只能求区间[ 1 , x ] 的区间和,那么如何求 [ L , R ] 的区间和呢? [1,x]的区间和,那么如何求[L,R]的区间和呢?[1,x]的区间和,那么如何求[L,R]的区间和呢?,这时候利用前缀和相减的性质就可以了,[ L , R ] = [ 1 , R ] − [ 1 , L − 1 ] [L,R]=[1,R]-[1,L-1][L,R]=[1,R]−[1,L−1]

 四.区间修改,单点查询

对于这一类操作,我们需要构造出原数组的差分数组b,然后用树状数组维护b数组即可

对于区间修改的话,我们只需要对差分数组进行操作即可,例如对区间[L,R]+k,那么我们只需要更新差分数组add(L,k),add(R+1,-k),这是差分数组的性质.

代码:

对于单点查询操作,求出b数组的前缀和即可,因为a[x]=差分数组b[1]+b[2]+…+b[x]的前缀和,这是差分数组的性质之一.

代码:

 三.例题加代码

例题一(单点修改、区间查询):

代码: 

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define lowbit(x) (x&(-x))
int c[2000006];
int n,m;
int ans;
//单点修改
int add_dandian(int x,int k){
    for(int i=x;i<=n;i+=lowbit(i)){
        c[i]+=k;
    }
    return 0;
}
//区间查询
int search(int begin,int end){
    for(int i=end;i;i-=lowbit(i)){
        ans+=c[i];
    }
    for(int i=begin-1;i;i-=lowbit(i)){
        ans-=c[i];
    }
    return 0;
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int num;
        cin>>num;
        add_dandian(i,num);
    }
    while(m--){
        int a;
        cin>>a;
        int x,y;
        cin>>x>>y;
        if(a==1){
            add_dandian(x,y);
        }else{
            ans=0;
            search(x,y);
            cout<<ans<<endl;
        }
    }
    system("pause");
}
例题二(区间修改、单点查询):

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+7;
#define lowbit(x) (x&(-x))
int a[500005];
int d[N]={0};//d[i]的值,d[i]表示第i和i-1个数的差值
int c[N];
int n,m;
//区间修改
int update(int pos,int k){//pos表示修改点的位置,K表示修改的值也即+K操作
    for(int i=pos;i<=n;i+=lowbit(i)){
        c[i]+=k;
    }
    return 0;
}
//单点查询
int ask_qujian(int pos){//返回区间pos到1的总和,由于是使用差分数组维护因此使用前缀和
    int ans=0;
    for(int i=pos;i;i-=lowbit(i)){
        ans+=c[i];
    }
    return ans;
}
signed main(){
    memset(c,0,sizeof(c));
    cin>>n>>m;
    a[0]=0;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        d[i]=a[i]-a[i-1];
        update(i,d[i]);
    }
    while(m--){
        int a;
        int x,y,k;
        cin>>a;
        if(a==1){
            cin>>x>>y>>k;
            update(x,k);
            update(y+1,-k);
        }else{
            cin>>x;
            cout<<ask_qujian(x)<<endl;
        }
    }
    system("pause");
}
例题三.求逆序对

分析:对于原序列,比当前位置数大的数前出现在序列中,就会构成逆序对,例如:5 3 2 1,5,3,2比1先出现且都比1大,那么此时就构成了3个逆序对数,那么我们可以对以及出现的数字进行标记,枚举序列中每一个位置的数,统计有多少比它大的数字以及出现,然后累加进答案即可,这就是一个单点修改+区间查询的操作,树状数组实现,不过需要注意的是这道题数字很大,需要离散化存储.

注意:这道题自定义排序参数cmp的实现,不能单纯的a.val<b.val,如果相等的话也要保证位置不变,不然贡献会增多,想想为什么?

#include<bits/stdc++.h>
using namespace std;
const int Maxn = 5e5+10;
int t[Maxn]={0};//树状数组 
typedef struct node{
   int val,ind;
}Node;
Node stu[Maxn];
int Rank[Maxn];
typedef long long ll;
int n; 
int lowbit(int x){return x&(-x);}
/*单点修改*/
void add(int pos){
	for(int i=pos;i<=n;i+=lowbit(i)) t[i]+=1;
}
/*区间求和*/
int ask(int pos){
	int ans = 0; 
	for(int i=pos;i;i-=lowbit(i)) ans+=t[i];
	return ans;
} 
/*不能单纯的a.val<b.val,如果相等的话也要保证位置不变,不然贡献会增多*/
int cmp(Node a,Node b){
	if(a.val==b.val)
	return a.ind<b.ind;
	
	return a.val<b.val;
}
int main()
{
	ll ans = 0;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>stu[i].val;
		stu[i].ind=i;
	}
	sort(stu+1,stu+n+1,cmp);
	/*离散化操作*/
	for(int i=1;i<=n;i++){
		Rank[stu[i].ind] = i;
	} 
	for(int i=1;i<=n;i++){
		int pos = Rank[i];

		ans+=ask(n)-ask(pos);//digit+1~n中有多少数字已经出现就贡献多少逆序对数,累加到答案 
			add(pos);//单点修改
	}
	cout<<ans;
	return 0;
}

如果你代码看到这里都看懂了的话,说明你的树状数组基本知识已经了解了,加油!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值