注意:数组的长度不满足题意,仅提供模板!!
目录
1 简介
顾名思义就是一个结构为树形结构的数组,于二叉树的结构类似但又不同,它是在二叉树的结构上删除了一些中间节点,来看两幅图就明白了.
1.这是二叉树的结构
2.这是树状数组的结构
不难发现,树状数组相比于二叉树删除了一些节点,但是为什么要删除呢?这就和树状数组的一些性质(lowbit)有关了,不懂没关系,继续往下看.
树状数组可以解决什么问题呢?
可以解决大部分区间上面的修改以及查询的问题,例如1.单点修改,单点查询,2.区间修改,单点查询,3.区间查询,区间修改,换言之,线段树能解决的问题,树状数组大部分也可以,但是并不一定都能解决,因为线段树的扩展性比树状数组要强.
树状数组和线段树的区别在哪?
有人会问了既然线段树的问题能够用树状数组解决而且线段树还比树状数组扩展性强,那为什么不直接用线段树呢?问的很好,树状数组的作用就是为了简化线段树,举个例子:一个问题可以用线段树解决写代码半个小时,但是用树状数组只需要10分钟,那么你会选择哪一个算法呢?没错,基于某些简单的问题,我们没必要用到功能性强但实现复杂的线段树(杀鸡焉用宰牛刀).
树状数组的优点
优点:修改和查询操作复杂度于线段树一样都是logN,但是常数比线段树小,并且实现比线段树简单
缺点:扩展性弱,线段树能解决的问题,树状数组不一定能解决.
树状数组能快速求解信息的原因:我们总能将一段前缀[1,n] 拆成 不多于logn段区间,使得这logn段区间的信息是 已知的。
举个例子:我们要求a[1.....7]的前缀合怎么办?
最暴力的方法就是计算a[1]+a[2]+...+a[7],但是这要算7次,有没有更好的方法?——当然有(于是树状数组闪亮登场。。。可能还有线段树)
如果我告诉你三个数X,Y,Z,且X=a[1....4]的合,Y=a[5....6]的合,Z=a[7...7]的合(其实就是a[7])
此时你会怎么算?你一定会回答:X+Y+Z(因为这样只需算3次)
小结:我们只需合并这logn 段区间的信息,就可以得到答案。相比于原来直接合并n个信息,效率有了很大的提高。
操作规则:
1.将数组中某个数进行修改(加减运算)
2.将数组从下标a到下标b这个区间的数进行修改(都同时进行加或减运算)
3.查询修改后的某个数或者某个区间
其实,我想读者也应该猜到了一种做法:“前缀和”
当然,这类问题可以用前缀和来做,但是前缀和做法最坏的情况下的复杂度的会达到O(n^2),当然,这对于大数据而言,是肯定不能接受的,那么我们就需要用“树状数组”的知识来解决这类问题。
接下来我们来逐步引入树状数组的概念!
前置知识点:1.前缀和 2.差分数组 3.lowbit()函数
样题1:修改单点值+输出区间和
代码:
#include<bits/stdc++.h>
#define lowbit(x) (x&(-x))
typedef long long ll;
using namespace std;
int c[100];
int n,m;
ll ans;
int add_dandian(int x,int k)//单点修改值
{
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=k;
}
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;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)//初始化数组
{
int number;
cin>>number;
add_dandian(i,number);
}
for(int i=1;i<=m;i++)
{
int choice,x,y;
cin>>choice>>x>>y;
if(choice==1) add_dandian(x,y);//在第x个数上加上k
else
{
ans=0;
search(x,y);
cout<<ans;
}
}
return 0;
}
样题2:修改区间值+输出单点和
代码:
#include<bits/stdc++.h>
#define lowbit(x) (x&(-x))
using namespace std;
typedef long long ll;
int a[100];
int d[100];//d[i]的值,d[i]表示第i和i-1个数的差值
ll c[100];
int n,m;
int update(int pos,int k)//pos表示修改点的位置,K表示修改的值也即+K操作
{
for(int i=pos;i<=n;i+=lowbit(i)) c[i]+=k;
}
ll ask_qujian(int pos)//返回区间pos到1的总和
{
ll ans=0;
for(int i=pos;i;i-=lowbit(i)) ans+=c[i];
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)//初始化数组
{
cin>>a[i];
d[i]=a[i]-a[i-1];//d[i]的值,d[i]表示第i和i-1个数的差值
update(i,d[i]);
}
for(int i=1;i<=m;i++)
{
int choice,x,y,k;
cin>>choice;
if(choice==1)
{
cin>>x>>y>>k;
update(x,k);
update(y+1,-k);
}
else
{
cin>>x;
cout<<ask_qujian(x);
}
}
return 0;
}
样题3:求逆序对
代码:
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
int t[100];
typedef struct node
{
int val,ind;
}Node;
Node stu[100];
int Rank[100];
typedef long long ll;
int n;
/*单点修改*/
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;
}
样题4:求区间最值
最大值
#include <bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
int a[10]={1,10,5,3,15,14,13,10,-5000,4};
int query(int x, int y)
{
int ans = 0;
while (y >= x)
{
ans = max(a[y], ans);
y --;
for (; y-lowbit(y) >= x; y -= lowbit(y))
ans = max(a[y], ans);
}
return ans;
}
int main()
{
cout<<query(0,9);
}
最小值
#include <bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
int a[10]={1,10,5,3,15,14,13,10,-5000,4};
int query(int x, int y)
{
int ans = INT_MAX;
while (y >= x)
{
ans = min(a[y], ans);
y --;
for (; y-lowbit(y) >= x; y -= lowbit(y))
ans = min(a[y], ans);
}
return ans;
}
int main()
{
cout<<query(0,9);
}
背记诀窍: