您的位置:首页 > 产品设计 > UI/UE

※ Leetcode - Segment Tree - 307. Range Sum Query - Mutable (线段树+树状数组两种解法以及模板的常见问题解析)

2016-08-12 13:30 721 查看

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息