307 Range Sum Query - Mutable

這題是Google出題率蠻高的308 Range Sum Query 2D - Mutable的簡單版。

Input是一個1D int array,和一個integer pair,代表array的起始index和終止index。目標是要算出起始index和中止index之間(inclusive)所有element的總和。

程式分成三個部分:Constructor, update, sumRange。

update讓我們可以更改input array的內容,sumRange讓我們可以算出範圍內的總和。

int sum = 0;

for(int idx = i; idx <= j; idx++)
    sum+=input[i];

這樣的時間複雜度是O(n)

為了再降低時間複雜度,學了一個新的資料結構:Binary indexed tree,有一篇很好的教學可以參考。

宣告全域變數:

int[] tree 儲存binary indexed tree中每個node的值

int[] numsCopy 儲存input array

1) public NumArray(int[] nums)

初始化tree和numsCopy,需注意nums[]固定由0開始,而binary indexed tree的第0個element是0,從1開始放值,為了配合bit operation,numsCopy也要和tree一樣從1開始。

tree[idx] = numsCopy[idx-2^r+1]+...+numsCopy[idx]

r表示idx的binary expression中最後一個1後面的0的數量

實作上先new出nums.length+1的空間(第0格的值定為0)給tree和numsCopy,並用for迴圈從1跑到nums.length+1,每個迴圈中先給定numsCopy[idx]=nums[idx-1],再進入第二層while迴圈從numsCopy[idx-2^r+1]一直累加到numsCopy[idx]存入tree[idx]。

2) void update(int i, int value)

因為nums[i]已經更新為val,必須更新有使用到nums[i]的tree node。

當tree[i]有用到nums[i],它的ancestor就也有用到nums[i],所以必須找出這些tree[i]所有的ancestor並更新。找到ancestor的方法為:idx = i, idx = i+(i&-i), ... 直到idx超過nums.length,根據binary indexed tree的定義,tree[i]的parent即為tree[i+(i&-i)]。

實作上先宣告變數int diff = val - numsCopy[i+1]。用for迴圈從j=i跑到tree.length,j+= j&-j,迴圈中tree[j]+=diff。

最後把numsCopy[i+1]的值更新為value。

3) int sumRange(int i, int j)

sumRange要計算 nums[i] + nums[i+1] + … + nums[j], 我們可以用cumulative[j]-cumulative[i-1]得到這個和。

如何利用binary indexed tree得到cumulative[j]和cumulative[i-1]?

舉例來說,13(1101)是三個區間的和:[1101,1101], [1001,1100], [0001,1000],分別是tree[13], tree[12], tree[8]內存的值。

1101 - (1101 & -1101) = 1101 - 1 = 1100 -> 12

1100 - (1100 & -1100) = 1100 - 100 =1000 -> 8

實作上我們要算出cumulative[j]和cumulative[i-1] 最後將兩者相減並return。

cumulative的計算用一個while迴圈來做,在迴圈外宣告一個cumulativeJ=tree[j],並將idx設為j-(j&-j),迴圈中cumulativeJ+=tree[idx],並更新idx為idx-(idx&-idx)。迴圈終止條件設為idx>0。

用了這個方法後,因為是binary search,可以將時間複雜度降到O(logn)。

public class NumArray {
    int[] tree;
    int[] numsCopy;
    public NumArray(int[] nums) {
        /*
        For each i in nums
        Calculate 2^r (by idx & -idx)
        Int i=1
        while(idx-2^r+i<=idx)
        tree[idx]+=nums[idx-2^r+i]
        i++
        */
        tree = new int[nums.length+1];
        numsCopy = new int[nums.length+1];
        for(int idx=1; idx<nums.length+1; idx++){
            numsCopy[idx] = nums[idx-1];
            int r = idx & -idx;
            int i=1;
            while(idx-r+i<=idx){
                tree[idx]+=numsCopy[idx-r+i];
                i++;
            }
        }
    }

    void update(int i, int val) {
        /*
        diff = val - nums[i];
        For int j=i; j<nums.length; j+= j & (-j)
            Tree[j] += diff
        nums[i] = val
        */
        int diff = val - numsCopy[i+1];
        for(int j=i+1; j<numsCopy.length; j+=(j & -j)){
            tree[j] += diff;
        }
        numsCopy[i+1] = val;
    }

    public int sumRange(int i, int j) {
        //cumulative j
        int cumulateJ = tree[j+1];
        int idx = j+1 - ((j+1) & -(j+1));
        while(idx>0){
            cumulateJ += tree[idx];
            idx -= idx & -idx;
        }
        //cumulative i
        int cumulateI = tree[i];
        idx = i - (i & -i);
        while(idx>0){
            cumulateI += tree[idx];
            idx -= idx & -idx;
        }
        return cumulateJ-cumulateI;
    }
}


// Your NumArray object will be instantiated and called as such:
// NumArray numArray = new NumArray(nums);
// numArray.sumRange(0, 1);
// numArray.update(1, 10);
// numArray.sumRange(1, 2);

為什麼idx & -idx可以取出idx的最後一位1呢? 舉個例子,如果idx == 0110,那-idx就是1001+0001 == 1010,所以以最後一位1為中心: 1. 他之後的bit一定都是0(因為取負號會+1) 2. 他之前的bit跟原本是反向的,例如原本是01,取負號之後會變成10,&之後一定都是0

所以0110 & 1010 => 0010,可以取出最後一位1。

results matching ""

    No results matching ""