树状数组和线段树都是对于一个区间查询和修改的时间复杂度比较低(log(n))的数据结构,主要用于查询任意两位之间的所有元素之和。
树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。
树状数组
生成树状数组:
设a[1…N]为原数组,定义c[1…N]为对应的树状数组:
c[i] = a[i - 2^k + 1] + a[i - 2^k + 2] + … + a[i]
其中k为i的二进制表示末尾0的个数,所以2^k即为i的二进制表示的最后一个1的权值.
所以2^k可以表示为n&(n^(n-1))或更简单的n&(-n).
int lowbit(int n)
{
return n& (-n);
// return n&(n^(n-1));
}
也就是说,把k表示成二进制1***10000,那么c[k]就是1***00001 + 1***00010 + … + 1***10000这一段数的和。
举例:
可以看出:设节点编号为x,那么这个节点管辖的区间为2^k个元素。(其中k为x二进制末尾0的个数)
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
…
C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16
修改
修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。
对a[n]进行修改后,需要相应的修改c数组中的p1, p2, p3…等一系列元素
其中p1 = n, pi+1 = pi + lowbit(pi)
所以修改原数组中的第n个元素可以实现为:
void Modify(int n, int delta)
{
while(n <= N)
{
c[n] += delta;
n += lowbit(n);
}
}
求和
当要查询a[1],a[2]…a[n]的元素之和时,需要累加c数组中的q1, q2, q3…等一系列元素
其中q1 = n,qi+1 = qi - lowbit(qi)
所以计算a[1] + a[2] + .. a[n]可以实现为:
int Sum(int n)
{
int result = 0;
while(n != 0)
{
result += c[n];
n -= lowbit(n);
}
return result;
}
总的来说
若需改变a[i],则c[i]、c[i+lowbit(i)]、c[i+lowbit(i)+lowbit(i+lowbit(i)]……就是需要改变的 c数组中的元素。
若需查询s[i],则c[i]、c[i-lowbit(i)]、c[i-lowbit(i)-lowbit(i- lowbit(i))]……就是需要累加的c数组中的元素。
线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)
写的超级详细的一个线段树专题
直接贴模板
建立树
void build(int l,int r,int k)
{
if(l==r)
{
scanf("%d",&num[k]);
return ;
}
int mid=(l+r)/2;
build(l,mid,k*2);
build(mid+1,r,k*2+1);
num[k]=max(num[k*2],num[k*2+1]);
}
更新节点
void update(int i,int date,int l,int r,int k)
{
if(l==r)
{
num[k]=date;
return ;
}
int mid=(l+r)/2;
if(i<=mid)
update(i,date,l,mid,k*2);
else
update(i,date,mid+1,r,k*2+1);
num[k]=max(num[k*2],num[k*2+1]);
}
区间更新
void Change(node *p, int a, int b) /* 当前考察结点为p,修改区间为(a,b]*/
{
if (a <= p->Left && p->Right <= b)
/* 如果当前结点的区间包含在修改区间内*/
{
...... /* 修改当前结点的信息,并标上标记*/
return;
}
Push_Down(p); /* 把当前结点的标记向下传递*/
int mid = (p->Left + p->Right) / 2; /* 计算左右子结点的分隔点
if (a < mid) Change(p->Lch, a, b); /* 和左孩子有交集,考察左子结点*/
if (b > mid) Change(p->Rch, a, b); /* 和右孩子有交集,考察右子结点*/
Update(p); /* 维护当前结点的信息(因为其子结点的信息可能有更改)*/
}
区间查询
int query(int node, int begin, int end, int left, int right)
{
int p1, p2;
/* 查询区间和要求的区间没有交集 */
if (left > end || right < begin)
return -1;
/* if the current interval is included in */
/* the query interval return segTree[node] */
if (begin >= left && end <= right)
return segTree[node];
/* compute the minimum position in the */
/* left and right part of the interval */
p1 = query(2 * node, begin, (begin + end) / 2, left, right);
p2 = query(2 * node + 1, (begin + end) / 2 + 1, end, left, right);
/* return the expect value */
if (p1 == -1)
return p2;
if (p2 == -1)
return p1;
if (p1 <= p2)
return p1;
return p2;
举例区间最大值查询
int query(int L,int R,int l,int r,int k)
{
if(L<=l&&R>=r)
{
return num[k];
}
int mid=(l+r)/2;
int ans=0;
if(L<=mid)
ans=max(ans,query(L,R,l,mid,k*2));
if(R>mid)
ans=max(ans,query(L,R,mid+1,r,k*2+1));
return ans;
}