1. Problem Description
Given an integer array nums, find the sum of the elements between indices i and j (i≤j), inclusive.
The update(i, val) function modifies nums by updating the element at index i to val.
Example:
Given nums = [1, 3, 5]
sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
Note:
The array is only modifiable by the update function.
You may assume the number of calls to update and sumRange function is distributed evenly.
线段树,单点更新,区间查询
2. 线段树解法
2.1线段树区间求和模板
首先是胡浩大神的单点更新,区间求和模板:
1.maxn是题目给的最大区间,而节点数要开4倍,确切的来说节点数要开大于maxn的最小2x的两倍
2.lson和rson分辨表示结点的左儿子和右儿子,由于每次传参数的时候都固定是这几个变量,所以可以用预定于比较方便的表示
3.PushUP(int rt)是把当前结点的信息更新到父结点
PushDown(int rt)是把当前结点的信息更新给儿子结点
4.rt表示当前子树的根(root),也就是当前所在的结点
补充我对模板中几个常见问题的解释:
1. rt<<1|1 的意义:rt乘2后,其末尾必为0,故rt<<1|1与rt<<1+1,rt<<1^1答案相同。
2. build函数
线段树的本质是完全二叉搜索树,后序生成二叉搜索树的过程,与二分查找类似。
得到中值,递归生成左子树,右子树,然后回溯更新每个节点(pushup)
L==R相当于到达叶子。
以这个数组为例:
[1,3,5,4,2]
我们刚开始的查询范围是1~5,从根节点1开始往下走。节点内数字表示该节点在数组中的编号,和其容纳的信息范围。比如”1 [1:5]”表示该节点容纳的是下标1到5的数字的和,由于线段树是完全二叉树,我们用数组顺序存储,这个节点在数组中被编号为1.
Ps:线段树数组的起始节点下标必须是1,不能是0,原因见5.
3. update函数
与build类似,同样通过中序遍历进行更新。
4.query函数
向下递归查找,只要我们当前到达的区间,在所要查询的区间内,它即是我们想要得到的一个子区间。
这里L:R是我们要查找的区间,l:r是当前走到的区间。
if (L <= l && r <= R)
{
return sum[rt];
}
计算m=l和r的平均数,m在大L和大R之间时分别搜索其左子树和右子树,然后用ret保存并返回当前节点左右子树之和。
rt << 1和rt << 1|1分别为当前rt节点左子树右子树的下标。
比如我们仍然以[1,3,5,4,2]为例
我们要查询[1:4]的区间和,我们递归的整个过程,是从1号节点[1:5]开始。
查询过程:1[1:5] - > 2[1:3] -> 4[1:2] -> 5[3:3]
这时,4[1:2] 和 5[3:3]都在我们要查询的区间[1:4]内,我们返回他俩的结果给2[1:3] = 4[1:2] + 5[3:3] = 4 + 5 = 9
然后继续查询:3[4:5] -> 6[4:4] (7[5:5]不会被查询)
同样这里6[4:4] 在我们要查询的区间[1:4]内,我们返回结果给3[4:5] = 6[4:4] = 4
最后返回1[1:5] = 2[1:3] + 3[4:5] = 9 + 4 =13
4. 为什么一定要从1到n而不是从0到n-1?
我们在进行递归构造线段树时,要填满这棵完全二叉树,走的递归顺序是1 -> 2 -> ……
填充数组的顺序是rt的左子树(rt*2)然后是rt的右子树(rt*2+1),如果从0开始,那么0*2=0,0*2+1=1本来其左子树是1号节点,右子树是2号节点,但求得的却是0和1。那么构造的二叉树就会有问题。
模板代码如下:
#include <cstdio>
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
const int maxn = 55555;
int sum[maxn<<2];
void PushUP(int rt) {
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}
void build(int l,int r,int rt) {
if (l == r) {
scanf("%d",&sum[rt]);
return ;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
PushUP(rt);
}
void update(int p,int add,int l,int r,int rt) {
if (l == r) {
sum[rt] += add;
return ;
}
int m = (l + r) >> 1;
if (p <= m) update(p , add , lson);
else update(p , add , rson);
PushUP(rt);
}
int query(int L,int R,int l,int r,int rt) {
if (L <= l && r <= R) {
return sum[rt];
}
int m = (l + r) >> 1;
int ret = 0;
if (L <= m) ret += query(L , R , lson);
if (R > m) ret += query(L , R , rson);
return ret;
}
int main() {
int T , n;
scanf("%d",&T);
for (int cas = 1 ; cas <= T ; cas ++) {
printf("Case %d:\n",cas);
scanf("%d",&n);
build(1 , n , 1);
char op[10];
while (scanf("%s",op)) {
if (op[0] == 'E') break;
int a , b;
scanf("%d%d",&a,&b);
if (op[0] == 'Q') printf("%d\n",query(a , b , 1 , n , 1));
else if (op[0] == 'S') update(a , -b , 1 , n , 1);
else update(a , b , 1 , n , 1);
}
}
return 0;
}
2.2 My AC code
PS:
1. 给的数组是从0开始的,为了方便构造线段树,这里用一个cnt计数。
2. sum要开到len的5倍,一开始就要开辟好空间。
3. 给的函数头无法递归,调用自己重写的函数
class NumArray
{
#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
public:
int cnt;
int len;
vector<int>sum;
void PushUP(int rt)
{
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}
void build(int l,int r,int rt,vector<int>& nums)
{
if(l>r)
return ;
if (l == r)
{
sum[rt]=nums[cnt++];
return ;
}
int m = (l + r) >> 1;
build(lson,nums);
build(rson,nums);
PushUP(rt);
}
void update(int p,int add,int l,int r,int rt)
{
if(l>r)
return ;
if (l == r)
{
sum[rt] = add;
return ;
}
int m = (l + r) >> 1;
if (p <= m) update(p , add , lson);
else update(p , add , rson);
PushUP(rt);
}
int query(int L,int R,int l,int r,int rt)
{
if (L <= l && r <= R)
{
return sum[rt];
}
int m = (l + r) >> 1;
int ret = 0;
if (L <= m) ret += query(L , R , lson);
if (R > m) ret += query(L , R , rson);
return ret;
}
//function
NumArray(vector<int> &nums)
{
len=nums.size();
for(int i=0; i<len*5; i++)
sum.push_back(0);
cnt=0;
build(1,len,1,nums);
}
void update(int i, int val)
{
update(i+1,val,1,len,1);
}
int sumRange(int i, int j)
{
return query(i+1,j+1,1,len,1);
}
};
3.树状数组解法
3.1 树状数组区间求和模板(HDU 1166 敌兵布阵)
题意大致是先输入num个数字构造树状数组,然后有三种查询。
Add 将第a个元素增加b
Sub 将第a个元素减少b
Q a到b区间和
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAX=50000+10;
int c[MAX];
int N;
string op;
int lowbit(int x)
{
return x&(-x);
}
//查询【1,x】区间和
int query(int x)
{
int s=0;
while(x>0)
{
s+=c[x];
x-=lowbit(x);
}
return s;
}
//单点更新,对节点x加data
void update(int x,int data)
{
while(x<=N)
{
c[x]+=data;
x+=lowbit(x);
}
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int T;
scanf("%d",&T);
int tcase=1;
while(T--)
{
scanf("%d",&N);
memset(c,0,sizeof(c));
for(int i=1;i<=N;i++)
{
int val;
scanf("%d",&val);
update(i,val);
}
printf("Case %d:\n",tcase++);
while(cin>>op&&op!="End")
{
int a,b;
scanf("%d%d",&a,&b);
if(op=="Query") printf("%d\n",query(b)-query(a-1));
if(op=="Add") update(a,b);
if(op=="Sub") update(a,-b);
}
}
return 0;
}
3.2 My AC code
Ps:
1. 因为这里需要更新值,而不是在原有的值上加,必须再开一个数组保存原有值。每次update后必须修改回来!
2. Sum开到n即可
3. 注意索引从0还是从1开始的问题。
class NumArray
{
private:
int len;
vector<int>sum;
vector<int>tmp;
public:
int lowbit(int x)
{
return x&(-x);
}
//查询【1,x】区间和
int query(int x)
{
int s=0;
while(x>0)
{
s+=sum[x];
x-=lowbit(x);
}
return s;
}
//function
//单点更新,对节点i加val
void update(int i, int val)
{
//为了依照题意把第i个变成val
int ti=i+1;
int add=val-tmp[ti];
//这里一定要改回来!
tmp[ti]=val;
while(ti<=len)
{
sum[ti]+=add;
ti+=lowbit(ti);
}
}
NumArray(vector<int> &nums)
{
len=nums.size();
for(int i=0; i<=len; i++)
{
sum.push_back(0);
tmp.push_back(0);
}
for(int i=0; i<len; i++)
{
update(i,nums[i]);
tmp[i+1]=nums[i];
}
}
int sumRange(int i, int j)
{
i++,j++;
return query(j)-query(i-1);
}
};
4.线段树与树状数组的区别
1.外观上树状数组比较简洁,常数复杂度低,空间复杂度低。
2.可以用树状数组解决的问题,线段树均可解决,但可以用线段树解决的,树状数组不一定可以解决。
3.常见的三种查询方法实现方式(给a加b,查询a到b的区间):
线段树 |
树状数组 | |
单点更新,区间查询(HDU 1166) |
update(a , b , 1 , n , 1) query(a , b , 1 , n , 1) |
update(a,b) query(b)-query(a-1) |
区间更新,单点查询 (HDU 1556) |
Push down懒惰标记+递归更新 |
update(b,c); update(a-1,-c); query(j) |
区间更新,区间查询(POJ 3468) |
Push down懒惰标记+递归更新 |
两树状数组,分别表示修改前、修改后,分别加一次减一次完成更新(4次)。加一次减一次完成查询。 |
时间复杂度 |
logn |
logn |
空间复杂度 |
4*n |
n |