※ Leetcode - Segment Tree - 307. Range Sum Query - Mutable (線段樹+樹狀數組兩種解法以及模板的常見問題解析)

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




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章