目录
一:树状数组简介
二:线段树原理----
1.结构简介
2.单点修改
3.求前缀和和区间和
三:模板例题
四:二维树状数组的应用
五:个人模板
给定一个数组a[maxn],提出如下问题
问题1:求这个数组的所有值的总和。
方法一:定义变量sum,遍历数组使sum+=a[i]。
方法二:定义另一数组pre,使pre[i]为原数组的前缀和。a[0]+a[1]+...a[i],则pre[last]=pre[last-1]+a[last]即为答案。
问题2:在问题1条件下,修改任意一个a[i]值(设+=change)的同时修改总和
方法一:直接令sum+=change。
方法二:使pre[i],pre[i+1]...pre[last]所有的值都+=change。
问题3:求连续区间[l,r]的总和
方法一:遍历数组,使ans+=a[l]+a[l+1]...a[r]。
方法二:令ans=pre[r]-pre[l]。
问题4:修改连续区间[l,r]的值(+=change)
方法一:直接令sum+=(l-r+1)*change。
方法二:令pre[l]+=(l-r+1)*change,pre[l+1]+=(l-r)*change,pre[l+2]+=(l-r+1)*change...
可以发现,如上例子中的问题讨论的都是连续区间或者是单点修改的问题。传统的遍历数组或者是前缀和数组在处理不同问题的时候都有不同的优势或劣势。那么,是否有数据结构可以解决上述类型问题的同时,还有着不错的时间空间复杂度呢?
树状数组与线段树往往是理想的选择。但要提前说明的是,树状数组并不能很好的应对问题4这类区间操作问题,相比于线段树而言,树状数组只能维护前缀“操作和”(前缀和,前缀积,前缀最大最小),而线段树可以维护区间操作和。
树状数组
简介:树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
------来源于百度百科
结构与建立
上图则为树状数组的简图,其中a为原数组,c为树状数组。那么我们由图可得出规律:
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
可以发现,树状数组下标所代表的指即为对应的前缀和。
那么,为什么树状数组是这样存放数据的呢?
其实,树状数组的本质,和文章最开始提到的遍历累加和没有太大的区别,只是进行了一点小小的加速:利用了二进制信息。
在建树的时候,我们人为的令下标和在二进制下,最右边的‘1’的位置(越往左,在‘1’右方的0越多)代表了他所能控制的树状数组中结点的个数。比如,3在二进制下为011,最右方的1的右方没有0,那么他就不能控制其他的结点(当然他可以代表原数组A3)。再比如6在二进制下为110,最右方的1的右方有一个0,那么他就可以控制一个结点C5(为什么是C5,因为6减去最右边的1即是5)以及原数组A6。
建树其实就是一个不断的单点更新的过程。通过不断的加最右方的‘1’的值推断出所有包含这一个点的前缀,更新数值直到越界(就像是传统前缀数组,只不过传统前缀数组是令每一个位置都更新,而树状数组是跳跃性的更新)。
如此我们便来看如何得到最右方‘1’的位置和单点更新的代码。
int lowbit(int x)
{
return x & (-x);
}
void update(int x,int value)
{
//a[x] += value;
while(x <= maxn)
{
c[x] += value;
x += lowbit(x);
}
}
求前缀和和区间和
前缀和很明显就是树状数组下标所代表的值,而区间和则可以使他们相减而得。
int GetSum(int x)
{
int ans = 0;
while(x > 0)
{
ans += c[x];
x -= lowbit(x);
}
return ans;
}
int ask(int l,int r)//求l-r区间和
{
return GetSum(r)-GetSum(l-1);
}
模板例题
HDU1166 敌兵布阵
基本上可以体现所有树状数组的基本用法了
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
#include <algorithm>
#include <vector>
#define inf 0x3f3f3f3f
#define sd(a) scanf("%d",&a)
#define mem0(a) memset(a,0,sizeof(a))
#define lowbit(x) ((x)&(-x))
typedef long long ll;
const int mod = 1e9+7;
const int maxn = 50010;
using namespace std;
int a[maxn],TA[maxn];//原数组,树状数组
int GetSum(int x)
{
int ans = 0;
while(x > 0)
{
ans += TA[x];
x -= lowbit(x);
}
return ans;
}
void update(int x,int value)
{
//a[x] += value;
while(x <= maxn)
{
TA[x] += value;
x += lowbit(x);
}
}
int main(){
int T;
int cur = 0;
sd(T);
while(T--)
{
mem0(TA),mem0(a);
printf("Case %d:\n",++cur);
int num;
sd(num);
for(int i = 1;i <= num;i++)
{
cin>>a[i];
update(i,a[i]);
}
string temp;
while(cin>>temp)
{
if(temp == "End")break;
int a,b;
sd(a),sd(b);
if(temp == "Query")
{
ll ans = GetSum(b) - GetSum(a - 1);
cout<<ans<<endl;
}
else if(temp == "Add")
{
update(a,b);
}
else{
update(a,-b);
}
}
}
}
二维树状数组的应用
可以发现,在二维数组中,+=Lowbit()和-=Lowbit()的操作和二维数组仍然相同,都是寻找所包含自己的节点和自己所管理的树状数组节点。不同问题赋予不同意义,对症下药的讨论才能很好的了解二位树状数组的应用。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
#include <algorithm>
#include <vector>
#define inf 0x3f3f3f3f
#define sd(a) scanf("%d",&a)
#define mem0(a) memset(a,0,sizeof(a))
typedef long long ll;
const int mod = 1e9+7;
const int maxn = 1010;
using namespace std;
int a[maxn],TA[maxn][maxn];//原数组,树状数组
int lowbit(int x)
{
return x & (-x);
}
/*int GetSum(int x)
{
int ans = 0;
while(x > 0)
{
ans += TA[x];
x -= lowbit(x);
}
return ans;
}*/
void add(int x, int y, int n, int v){
while(x <= n){
int cur = y;
while(cur <= n){
TA[x][cur] += v;
cur += lowbit(cur);
}
x += lowbit(x);
}
}
int query(int x, int y){
int ans = 0;
while(x){
int cur = y;
while(cur){
ans += TA[x][cur];
cur -= lowbit(cur);
}
x -= lowbit(x);
}
return ans;
}
/*void update(int x,int value)
{
//a[x] += value;
while(x <= maxn)
{
TA[x] += value;
x += lowbit(x);
}
}*/
int main(){
int T;
sd(T);
while(T--)
{
mem0(TA);
int n,t;
sd(n),sd(t);
int x1,y1,x2,y2;
while(t--)
{
char cur;
cin>>cur;
if(cur == 'C')
{
scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
add(x1, y1, n, 1);
add(x1, y2 + 1, n, 1);
add(x2 + 1, y1, n, 1);
add(x2 + 1, y2 + 1, n, 1);
}
else{
sd(x1),sd(x2);
cout<<query(x1,x2)%2<<endl;
}
}
if(T)cout<<endl;
}
}
树状数组的代码是简洁明了的,这使得他在适用的问题背景下有着良好的修改性,简易性和良好的运行速度。
但问题4依然没有解决。树状数组只能维护前缀“操作和”(前缀和,前缀积,前缀最大最小),树状数组面对区间操作仍然十分吃力,什么方法可以优秀的解决维护区间操作和问题?
答案是线段树。
https://blog.csdn.net/qq_42937838/article/details/104553971
个人模板
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
#include <algorithm>
#include <vector>
#define inf 0x3f3f3f3f
#define sd(a) scanf("%d",&a)
#define mem0(a) memset(a,0,sizeof(a))
#define lowbit(x) ((x)&(-x))
typedef long long ll;
const int mod = 1e9+7;
const int maxn = 1e5+5;
using namespace std;
int a[maxn],TA[maxn];//原数组,树状数组
int GetSum(int x)
{
int ans = 0;
while(x > 0)
{
ans += TA[x];
x -= lowbit(x);
}
return ans;
}
void update(int x,int value)
{
//a[x] += value;
while(x <= maxn)
{
TA[x] += value;
x += lowbit(x);
}
}
int query(int k)//求第几大
{
int ans=0,cnt=0;
for(int i=20;i>=0;i--)
{
ans+=(1<<i);
//cout<<ans<<endl;if(ans<maxn)cout<<c[ans]<<endl;
if(ans>=maxn||cnt+TA[ans]>=k)
{
ans-=(1<<i);
}
else
{
cnt+=TA[ans];
}
}
return ans+1;
}