参考题目:
HDU - 1754 ----- I Hate It ---------
https://cn.vjudge.net/contest/316365#problem/E
维护区间最大值+单点更新
POJ - 3468 ----- A Simple Problem with Integers ---------
https://cn.vjudge.net/contest/316365#problem/A
维护区间和+区间更新
POJ - 3264 ----- Balanced Lineup ---------
https://cn.vjudge.net/contest/316365#problem/C
维护区间极值差
以下以HDU - 1754 I Hate It为例:
前提:线段树用来高效解决连续区间的动态查询问题
概念:线段树擅长处理区间,根维护的是整个区间,每个节点维护的是父亲的区间二等分后的一个区间。
建树操作:建树的过程是基于递归实现的。(先纵后横)
1.非结构体的写法1。
void build(int l,int r,int o) {
if(l==r) {
scanf("%d",&sum[o]);//遍历到叶子结点的位置,输入一个数
return;
}
int mid=(l+r)>>1;
build(l,mid,o<<1);//左子树
build(mid+1,r,o<<1|1);//右子数
sum[o]=max(sum[o<<1],sum[o<<1|1]);//左右儿子有了,在父亲节点(即左右儿子所代表的区间)维护最大值
}
非结构体的写法2
//主函数
for(int i=1; i<=n; i++)
scanf("%d",&s[i]);//先在主函数中输入遍历的n个数
build(1,n,1);
//build函数
void build(int l,int r,int rt) {
if(l==r) { //l==r表示到叶子结点的位置
sum[rt]=s[l];
return ;
}
int mid=(l+r)>>1;
build(l,mid,rt<<1);
build(mid+1,r,rt<<1|1);
sum[rt]=max(sum[rt<<1],sum[rt<<1|1]);
}
2.结构体的写法
struct node {
int l,r,sum;//左右区间和区间所维护的最大值
} tree[N];
void build(int l,int r,int o) { //从主函数中传过来的l=1,r=n,o=1
tree[o].l=l;
tree[o].r=r;
if(l==r) {
scanf("%d",&tree[o].sum);//刚开始每个结构体数组中只有l和r有值,遍历到
return; //叶子节点的时候,才输入sum的值,再往上回溯,把sum的值补上
}
int mid=(l+r)>>1;
build(l,mid,o<<1);
build(mid+1,r,o<<1|1);
tree[o].sum=max(tree[o<<1].sum,tree[o<<1|1].sum);
}
维护和更改操作
以题例所描述:
当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。 (执行quary函数)
当C为’U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B(执行update函数)
update函数的更改操作:
void update(int L,int s,int l,int r,int o) { //将下标为L的位置上的值改为s,刚来时传过来l=1,r=n,o=1
if(l==r) {
sum[o]=s; //下标为L的位置肯定是叶子结点
return ;
}
int mid=(l+r)>>1;
if(L<=mid)
update(L,s,l,mid,o<<1);
else
update(L,s,mid+1,r,o<<1|1);
sum[o]=max(sum[o<<1],sum[o<<1|1]);//对儿子修改完后,相应的,父亲的值也要发生变化,再一步步回溯,改变父亲的值
}
quary函数输出最大值的操作
int query(int L,int R,int l,int r,int o) {//维护区间[L,R]的最大值,从总区间[l,r](即l=1,r=n)开始找目标区间
if(L<=l && R>=r)
return sum[o];//如果这个区间是维护区间的子区间,就返回这个区间的最大值
int mid=(l+r)>>1;
int ret=0;
if(L<=mid) ret=max(ret,query(L,R,l,mid,o<<1));//左相交
if(R>mid) ret=max(ret,query(L,R,mid+1,r,o<<1|1));//右相交
return ret;
}
树状数组的引人思路:
1.如果线段树每个节点维护的是对应区间的和,比如说计算从s到t的和(a(s)+…+a(t)),在基于线段树的实现中,这个和是可以直接求得到。如果计算(从1到t的和)-(从1到s-1的和),同样能得到s到t的和。也就是说,对于任意i,我们都能计算出1到i的部分和就行了。这就导致了线段树的右儿子的值不需要了。
2.信息学奥赛一本通
根据任意正整数关于2的不重复次幂的唯一分解定理,若一个正整数x的二进制表示为10101,其中等于1的位是0,2,4,则正整数x可以被“二进制分解”成2^4+2 ^2 + 2^0。进一步,区间[1,x]可以分成O(logx)个子区间。
子区间的特点:若区间结尾为R,则区间长度就等于R的“二进制分解”下最小的2的次幂,我们设为lowbit®。
lowbit®:求R的最低位的1的2的次幂。
数组c可以看成如上图所示的树形结构。
该结构满足以下性质:
1.每个内部结点c[x]保存以它为根的子树中所有叶结点的和。
2.每个内部结点c[x]的子结点个数等于lowbit(x)的位数。
3.除树根外,每个内部结点c[x]的父亲结点是c[x+lowbit(x)]。
4.树的深度为O(logN)。
注意:树状数组依照二进制的对应,都已经完美规划。
如何保证右儿子不要,左儿子要?
明白lowbit()的作用
c[1]=a[1];----(0001)
c[2]=a[1]+a[2];----(0010)
c[3]=a[3];----(0011)
c[4]=a[1]+a[2]+a[3]+a[4];-----(0100)
c[5]=a[5];-----(0101)
c[6]=a[5]+a[6];----(0110)
c[7]=a[7];-----(0111)
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8];-----(1000)
推出c[i]=a[i-2^k +1]+a[i-2^ k+2]+…+a[i];
k为i的二进制从低位到高位连续0的长度
lowbit()的原型
int lowbit(int x) {
return x&(-x);
}
树状数组的操作
int sum(int i) { //计算前i项的和
int s=0;
while(i>0) {
s+=c[i];
i-=bowbit(i);
}
return s;
}
void add(int i,int x) { //使第i项增加x
while(i<=n) {
c[i]+=x;/*不断找其祖先*/
i+=bowbit(i);
}
}
注意:树状数组能处理的下标为1…n的数组,绝对不能出现下标为0的情况。
为何说树状数组已经规划好了?
例如计算前7项的和。
c[7]代表的是它本身a[7],
7-bowbit(7)=6,c[6]表示的是a[5]+a[6],
6-bowbit(6)=4,c[4]表示的是a[1]+a[2]+a[3+a[4]。
一步步把前7项和准确定义出来。
扩展多维的树状数组:
有n*m的二维数组,树状数组为c
单点更新
int update(int x,int y,int z) //将(x,y)的值加上z
{
int i=x;
while(i<=n)
{
int j=y;
while(j<=m)
{
c[i][j]+=z;
j+=lowbit(j);
}
i+=lowbit(i);
}
}
查询前缀和
int sum(int x,int y)
{
int res=0,i=x;
while(i>0)
{
int j=y;
while(j>0)
{
res+=c[i][j];
j-=lowbit(j);
}
i-=lowbit(i);
}
return res;
}