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

本文详细介绍了线段树和树状数组的基本概念、应用场景及实现方式,并对比了两者之间的区别,包括它们在不同查询任务中的表现,如单点更新区间查询等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. Problem Description

Given an integer array nums, find the sum of the elements between indices i and j (ij), 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.lsonrson分辨表示结点的左儿子和右儿子,由于每次传参数的时候都固定是这几个变量,所以可以用预定于比较方便的表示

3.PushUP(int rt)是把当前结点的信息更新到父结点

PushDown(int rt)是把当前结点的信息更新给儿子结点

4.rt表示当前子树的根(root),也就是当前所在的结点

 

补充我对模板中几个常见问题的解释:

1. rt<<1|1 的意义:rt2后,其末尾必为0,故rt<<1|1rt<<1+1rt<<1^1答案相同。

2. build函数

线段树的本质是完全二叉搜索树,后序生成二叉搜索树的过程,与二分查找类似。

得到中值,递归生成左子树,右子树,然后回溯更新每个节点(pushup

L==R相当于到达叶子。

以这个数组为例:

[1,3,5,4,2]

我们刚开始的查询范围是1~5,从根节点1开始往下走。节点内数字表示该节点在数组中的编号,和其容纳的信息范围。比如”1   [1:5]”表示该节点容纳的是下标15的数字的和,由于线段树是完全二叉树,我们用数组顺序存储,这个节点在数组中被编号为1.

Ps:线段树数组的起始节点下标必须是1,不能是0,原因见5.

3. update函数

build类似,同样通过中序遍历进行更新。

 

4.query函数

向下递归查找,只要我们当前到达的区间,在所要查询的区间内,它即是我们想要得到的一个子区间。

这里LR是我们要查找的区间,l:r是当前走到的区间。

if (L <= l && r <= R)
{
     return sum[rt];
}

计算m=lr的平均数,m在大L和大R之间时分别搜索其左子树和右子树,然后用ret保存并返回当前节点左右子树之和。

rt << 1rt << 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. 为什么一定要从1n而不是从0n-1

我们在进行递归构造线段树时,要填满这棵完全二叉树,走的递归顺序是1 -> 2 -> ……

填充数组的顺序是rt的左子树(rt*2)然后是rt的右子树(rt*2+1),如果从0开始,那么0*2=00*2+1=1本来其左子树是1号节点,右子树是2号节点,但求得的却是01。那么构造的二叉树就会有问题。

 

 

模板代码如下:

#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要开到len5倍,一开始就要开辟好空间。

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   ab区间和

#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.常见的三种查询方法实现方式(给ab,查询ab的区间):

线段树

树状数组

单点更新,区间查询(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




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值