题目链接
思路:树状数组
首先:直接用前缀和数组肯定是会超时的,假设每次修改的都是第一个数组,那么后面的每个前缀和都要修改,最坏情况下,就是O(n2),显然就超时了,所以换个思路,使用树状数组。
树状数组是一种可以维护序列前缀和的数据结构,并且:
单点修改add(index,val):在index位置上加上val,它的时间复杂度是O(log(n))
前缀和prefixSum(index):查询[0,index]范围内的累加和,它的时间复杂度是O(log(n))
介绍一下树状数组这个数据结构:
一个数组A
这里的数组A就是装原始数据的数组
可以看出:
t[1]=a[1]
t[2]=a[1]+a[2]
t[3]=a[3]
t[4]=a[1]+a[2]+a[3]+a[4]
t[5]=a[5]
t[6]=a[5]+a[6]
t[7]=a[7]
t[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
这里T数组和A数组的关系就要找找了。
看下图:
T[i]=A[i-2k+1] +A[i-2k+2]+A[i-2k+3]+…+A[i];
这里的K的值表示i的二进制从最低位开始有几个连续的0 。
例如:i=4的时候,4的二进制是(0100),后面有两个0,那么k就等于2;
那么T[4]=A[4-4+1]+A[4-4+2]+A[4-4+3]+A[4-4+4]
也即T[4]=A[1]+A[2]+A[3]+A[4]
其他的可自行验证。
那么这里的2k指的其实也就是i这个数字换算成二进制之后,最低位的1形成的数字。
再来看看前缀和怎么求
SUM[1]=T[1]
SUM[2]=T[2]
SUM[3]=T[3]+T[2]
SUM[4]=T[4]
SUM[5]=T[5]+T[4]
SUM[6]=T[6]+T[4]
SUM[7]=T[7]+T[6]+T[4]
SUM[8]=T[8]
这样如果看不出规律,将SUM后的下标换算成二进制看看。
SUM[0001]
SUM[0010]
SUM[0011]
SUM[0100]
SUM[0101]
SUM[0110]
SUM[0111]
SUM[1000]
第一个规律: 二进制里面有几个1,那么后面就有几个T元素。
第二个规律: T元素里面的下标每次都是将前一个T元素的下标去掉最低位的1,然后再累加。(当然这个下标不能为0)也就是说,将上一个T元素的下标减去上一个T元素下标最低位的1形成的数字。
上面计算T数组的时候,也涉及到要计算最低为的1形成的数字。
那么这里前缀和已经可以求了。
现在还有一个问题,如果A数组中某个元素更新了,应该怎么更新?
这里也就是,修改A数组的某个元素,T数组如何修改。
T[i]=A[i-2k+1] +A[i-2k+2]+A[i-2k+3]+…+A[i];
那么被A[i]影响的位置有
T[i+2k]、T[i+2k+2k1]、…
这里解释一下k、k1、k2、k3…
k指的是i的最低位的1形成的数字
k1指的是i+2k最低位的1形成的数字
下面依次类推
举个例子:
上面那个图中:
假设更新了a[5]的值,那么被影响的就是t[5]、t[6]、t[8]
看看二进制就能发现规律了,
更新的是a[0101]
被影响的是t[0101]、t[0110]、t[1000]。
也就是后面一个下标等于前面一个下标加上最低位1形成的数字。
这里又是与最低位1形成的数字有关。
那么如何去计算一个数字的最低位形成的数字呢?
这里使用到了lowbit函数,这个函数功能就是得到一个数字最低位1形成的数字。
当然,也可以使用其他的方法去求得,只不过这个方法很快。
这里就显示了前人的智慧了。
//功能是x加上 x的二进制最低的1形成的数字
private int lowbit(int x){
return x&(-x);
}
为什么可以这么算呢?
这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有
●当x为0时,即 0 & 0,结果为0;
●当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。结果为1。
●当x为偶数,且为2的m次方时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。
●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的二进制表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为0。结果为2^k。
总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。
到此,解决的问题有
- 建立了T数组与A数组的对应关系,即找到了T数组的计算方法。
- 建立了前缀和与T数组的关系,即找到了计算前缀和的计算方法。
- 找到了更新A数组,被影响的T数组的规律,即找到了因更新A数组,而更新T数组的计算方法。
到此,就可以解决这个题目了,其实这里就是用到了树状数组数据结构的特点。
代码:
class NumArray {
//树状数组
int[] A =null;
int[] T = null;
int len = 0;
public NumArray(int[] nums) {
len = nums.length;
this.A = new int[len+1];
for(int i=0; i<len; i++){
this.A[i+1] = nums[i];
}
//初始化T数组
init();
}
public void update(int index, int val) {
int i=index+1;
//在原来的i位置上加上了change就变成了val,也就说变化值是change
int change = val-A[i];
A[i]= val;
while(i<=len){
T[i]+=change;
i=i+lowbit(i);
}
}
public int sumRange(int left, int right) {
//这里要注意一个小地方,就是这里的left和right是包含0的,但是我们的T数组是从1开始的。所以right+1,left-1+1
int sum1 = getSum(right+1);
int sum2 = getSum(left);
return sum1-sum2;
}
//计算[1,end]的和,这里就是利用前缀和与T数组的关系
private int getSum(int end){
int res=0;
int i=end;
while(i>0){
res = res + T[i];
i=i-lowbit(i);
}
return res;
}
//初始化T
private void init(){
this.T = new int[len+1];
for(int x=1; x<=len; x++){
int i=x-lowbit(x)+1;
while(i<=x){
T[x]+= A[i];
i++;
}
}
}
//功能是x加上 x的二进制最低的1形成的数字
private int lowbit(int x){
return x&(-x);
}
}