LeetCode 307. Range Sum Query - Mutable (范围求和查询)

原题

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:

  1. The array is only modifiable by the update function.
  2. You may assume the number of calls to update and sumRange function is distributed evenly.

Reference Answer

思路分析

解法一:

用最愚蠢的 brute force,能通过,就是效率低,代码如下:

class NumArray {
public:
    NumArray(vector<int> nums) {
        data = nums;
    }
    
    void update(int i, int val) {
        data[i] = val;
    }
    
    int sumRange(int i, int j) {
        int sum = 0;
        for (int k = i; k <= j; ++k) {
            sum += data[k];
        }
        return sum;
    }
    
private:
    vector<int> data;
};

咳咳,下面就开始闪亮的装比时间了,光芒必将盖过坂本大佬。上面的方法最大的问题,就是求区域和不高效,如果数组很大很大,每次求一个巨型的区间的和,都要一个一个的遍历去累加,累啊~但是一般的累加数组又无法应对这里的update操作,随便修改一个数字的话,那么其之后的所有累加和都会发生改变。所以解决方案就是二者折中一下,分块累加,各不干预。就是将原数组分为若干块,怎么分呢,这里就让每个block有sqrt(n)个数字就可以了,这个基本是让block的个数跟每个blcok中数字的个数尽可能相同的分割方法。然后我们就需要一个大小跟block个数相同的数组,来保存每个block的数字之和。在需要更新的时候,我们就先确定要更新的位置在哪个block里,然后只更新该block的和。而对于求区域和操作,我们还是要分别确定i和j分别属于哪个block,若属于同一个block,那么直接遍历累加即可,若属于不同的,则先从i累加到该blcok的末尾,然后中间横跨的那些block可以直接将和累加,对于j所在的blcok,则从该block的开头遍历累加到j即可,参见代码如下:

Code

class NumArray {
public:
    NumArray(vector<int> nums) {
        if (nums.empty())   return;
        data = nums;
        double root = sqrt(data.size());
        len = ceil(data.size() / root);
        block.resize(len);
        for (int i = 0; i < data.size(); ++i){
            block[i / len] += data[i];
        }
        
    }
    
    void update(int i, int val) {
        int idx = i / len;
        block[idx] += val - data[i];
        data[i] = val;
        
    }
    
    int sumRange(int i, int j) {
        int sum = 0;
        int start = i / len, end = j / len;
        if (start == end){
            for (int k = i; k <= j; ++k){
                sum += data[k];
            }
            return sum;
        }
        for (int k = i; k < (start + 1) * len; ++k){
            sum += data[k];
        }
        for (int k = start + 1; k < end; ++k){
            sum += block[k];
        }
        for (int k = end * len; k <= j; ++k){
            sum += data[k];
        }
        return sum;
        
        
    }
private:
    int len;
    vector<int> data, block;
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(i,val);
 * int param_2 = obj.sumRange(i,j);
 */
        

同样是利用分块区域和的思路,下面这种方法使用了一种新的数据结构,叫做 树状数组Binary Indexed Tree,又称Fenwick Tree,这是一种查询和修改复杂度均为O(logn)的数据结构。这个树状数组比较有意思,所有的奇数位置的数字和原数组对应位置的相同,偶数位置是原数组若干位置之和,假如原数组A(a1, a2, a3, a4 …),和其对应的树状数组C(c1, c2, c3, c4 …)有如下关系:

在这里插入图片描述

C1 = A1

C2 = A1 + A2

C3 = A3

C4 = A1 + A2 + A3 + A4

C5 = A5

C6 = A5 + A6

C7 = A7

C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

那么是如何确定某个位置到底是有几个数组成的呢,原来是根据坐标的最低位Low Bit来决定的,所谓的最低位,就是二进制数的最右边的一个1开始,加上后面的0(如果有的话)组成的数字,例如1到8的最低位如下面所示:

坐标 二进制 最低位

1 0001 1

2 0010 2

3 0011 1

4 0100 4

5 0101 1

6 0110 2

7 0111 1

8 1000 8

最低位的计算方法有两种,一种是x&(x^(x–1)),另一种是利用补码特性x&-x。

这道题我们先根据给定输入数组建立一个树状数组bit,比如,对于 nums = {1, 3, 5, 7, 9, 11, 13, 15, 17},建立出的bit数组为:

bit -> 0 1 4 5 18 11 24 15 74

注意到我们给bit数组开头padding了一个0,这样我们在利用上面的树状数组的性质时就不用进行坐标转换了。可以发现bit数组中奇数位上的数字跟原数组是相同的,参见上面标记蓝色的数字。偶数位则是之前若干位之和,符合上图中的规律。

现在我们要更新某一位数字时,比如将数字5变成2,即update(2, 2),那么现求出差值 diff = 2 - 5 = -3,然后我们需要更新树状数组bit,根据最低位的值来更新后面含有这一位数字的地方,一般只需要更新部分偶数位置的值即可。由于我们在开头padding了个0,所以我们的起始位置要加1,即 j=3,然后现将 bit[3] 更新为2,然后更新下一位,根据图中所示,并不是bit[3]后的每一位都需要更新的,下一位需要更新的位置的计算方法为 j += (j&-j),这里我们的j是3,则 (j&-j) = 1,所以下一位需要更新的是 bit[4],更新为15,现在j是4,则 (j&-j) = 4,所以下一位需要更新的是 bit[8],更新为71,具体的变换过程如下所示:

0 1 4 5 18 11 24 15 74

0 1 4 2 18 11 24 15 74

0 1 4 2 15 11 24 15 74

0 1 4 2 15 11 24 15 71

接下来就是求区域和了,直接求有些困难,我们需要稍稍转换下思路。比如若我们能求出前i-1个数字之和,跟前j个数字之和,那么二者的差值就是要求的区间和了。所以我们先实现求前任意i个数字之和,当然还是要利用树状数组的性质,此时正好跟update函数反过来,我们的j从位置i开始,每次将bit[j]累加到sum,然后更新j,通过 j -= (j&-j),这样就能快速的求出前i个数字之和了,从而也就能求出任意区间之和了,参见代码如下:

Note

  • 规律不是很好找,这道题还是建立在本身就知道这种排列结构比较容易做;
  • 注意内层循环中while nums[after_index] <= nums[pre_index]:,必须是小于等于,不然不能得到正确结果,做题时候在这浪费了不少时间。

参考文献

[1] http://www.cnblogs.com/grandyang/p/4985506.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值