求解连续子数组和全解析-常规解法VS树状数组!
本文将介绍几求解数组前缀和和连续子数组和的三种方法,分别是遍历法、辅助数组法、树状数组法。
先来定义我们的问题,假设数组为A=[a[0],a[1],a[2],...,a
],我们想要求解A[from:to]的和,即求解a[from]+a[from+1]+....+a[to]。
1、遍历法
遍历法很简单,即从from的位置循环到to的位置,将元素加起来,代码如下:
package RangeSum; public class Traversal { public static int traversal(int[] arr,int start,int end){ int res = 0; if(start < 0 || start >arr.length || end > arr.length){ System.out.println("index error"); return -999; } for(int i=start;i<=end;i++){ res += arr[i]; } return res; } public static void main(String[] args){ int[] arr = {5,3,2,8,7,10,13,6}; //2 + 8 + 7 + 10 + 13 = 40 System.out.println(traversal(arr,2,6)); } }
遍历法很简单,若区间长度为m,那么求解的时间复杂度为O(m),空间复杂度为O(1)。
遍历法求解简单,单次求解的情况下非常适用。但是当我们需要频繁求解连续子数组和时,就不是那么适用了,这时候,我们便有了辅助数组法。
2、辅助数组法
辅助数组法比较适用于频繁求解连续子数组和的情况,此时,我们增加辅助数组s,s[m]代表0到m的元素和,代码如下:
package RangeSum; public class AuxiliaryArr { public static int[] auxiliary(int[] arr){ if(arr == null || arr.length <= 0) return null; int[] aux = new int[arr.length]; aux[0] = arr[0]; for(int i=1;i<arr.length;i++){ aux[i] = aux[i-1] + arr[i]; } return aux; } public static int sumRange(int[] arr,int start,int end){ if(start < 0 || start >arr.length || end > arr.length){ System.out.println("index error"); return -999; } else if(start == 0){ return arr[end]; } else{ return arr[end] - arr[start-1]; } } public static void main(String[] args){ int[] arr = {5,3,2,8,7,10,13,6}; //2 + 8 + 7 + 10 + 13 = 40 int[] auxiliaryArr = auxiliary(arr); System.out.println(sumRange(auxiliaryArr,2,6)); } }
辅助数组法的建数组时间复杂度时O(n),空间复杂度为O(n),当频繁求解子数组和时,求和复杂度为O(1)。
但是,当我们回频繁修改数组a时,辅助数组法也不是那么适用,因为修改数组a,辅助数组s也是要更新的,最坏的情况下,我们更新a[0],那么辅助数组的每一个元素都需要修改。如果实时对数组a进行M次修改和求和,那么最坏情况下的时间复杂度时O(M * n)。
这种情况下,有没有更好的解决方法呢!本文的重头戏,树状数组法就要出马了,如果实时对数组a进行M次修改和求和,树状数组的时间复杂度可以达到O(M * logn)。我们一起来看一下。
3、树状数组法
假设我们有数组A={1,2,3,4,5,6,7,8},我们首先构造一颗二叉树,如下图所示:
随后进行变形,其实什么都没做,只是为了后面看的清晰:
随后,我们定义树状数组C:
可以看到,树状数组中根节点和所有的左子节点都被赋予了相应的值,而且:
C[1]=A[1]; C[2]=A[1]+A[2]; C[3]=A[3]; C[4]=A[1]+A[2]+A[3]+A[4]; C[5]=A[5]; C[6]=A[5]+A[6]; C[7]=A[7]; C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
怎么知道左子节点在树状数组中的索引呢?其实就是从叶子结点开始往上找,比如C[4],从叶子结点的第四个,由于它是右子节点,所以向上找父节点,发现父节点仍然是右子节点,所以再往上找,发现此时父节点为左子节点,停止寻找。
上面树状数组中的元素,分别是原数组中连续子数组求和得到的,那么怎么知道是哪些元素的求和呢?可以看到,C[m]对应的连续子数组的末尾元素一定是A[m],关键是如何找到起始的元素。
我们首先将十进制数字转换为二进制表示,看能不能发现一些规律:
1=(001) C[1]=A[1]; 2=(010) C[2]=A[1]+A[2]; 3=(011) C[3]=A[3]; 4=(100) C[4]=A[1]+A[2]+A[3]+A[4]; 5=(101) C[5]=A[5]; 6=(110) C[6]=A[5]+A[6]; 7=(111) C[7]=A[7]; 8=(1000) C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
结合树来看一下:
发现规律了没:
1、最后一个1出现在末位,即二进制末尾有0个0,C[m]是一个元素的和,即C[m]=A[m]
2、最后一个1出现在倒数第二位,即二进制末尾有1个0,C[m]是两个元素的和,即C[m] = A[m] + A[m-1]
3、最后一个1出现在倒数第三位,即二进制末尾有2个0,C[m]是4个元素的和....
4、最后一个1出现在倒数第k+1位,即二进制末尾有k个0,C[m]是2^k个元素的和。
因此,我们可以得到计算公式,k为m的二进制中从最低位到高位连续零的长度:
C[m]=A[m-2^k+1]+A[to-2^k+2]+......A[m];
好了,回到我们最开始的问题,如何在原数组更新频繁和多次求解的情况下,快速解决连续子数组求和的问题呢?
那么求解子数组和的问题可以转化为如下的递归形式:
sumRange[from:to]
= A[from] + A[from + 1] +..+A[to]
= sum[from:to - 2^k] + C[to]
C[to] = A[to-2^k+1]+A[to-2^k+2]+......A[to];
好了,来看下代码实现吧。
我们首先定义一个lowbit函数,lowbit(m)=2^k:
public static int lowbit(int t){ return t & (-t); }
随后我们定义两个数组,分别是原数组和树状数组,这里,我们在初始化的时候,在传入的数组前面增加了一个元素,这样,就跟我们上面的讲解保持一致,即数组的元素下标从1开始:
int[] arr = null; int[] bitArr = null; int n = 0; public BIT(int[] arr){ this.arr = new int[arr.length + 1]; for(int i=0;i<arr.length;i++){ this.arr[i+1] = arr[i]; } this.n = arr.length; this.initBitArr(); } public void initBitArr(){ bitArr = new int[this.arr.length]; for(int i=1;i<=this.arr.length;i++){ this.update(i); } }
在初始化树状数组时,我们使用了更新树状数组元素这个函数,更新的时间复杂度是O(logn),因为只需要从叶子结点开始,不断向上直到根节点即可:
public void update(int index){ for(int i=index;i <=n ;i += lowbit(i)){ this.bitArr[i] += this.arr[index]; } }
随后便是分段求和的函数:我们想要求原数组from到to位置的元素和,那么使用树状数组中的对应位置便是from+1到to+1位置,我们这里使用两段和相减得到最终结果,分别是[1,to+1],[1,from],这样,二者相减便是最终结果。
public int sum(int from1,int to1){ int ans1 = 0; int ans2 = 0; int from = from1 + 1; int to = to1 + 1; for(int i=to;i>0;i-= lowbit(i)){ ans1 += this.bitArr[i]; } if(from1 > 0){ for(int i=from-1;i>0;i-= lowbit(i)){ ans2 += this.bitArr[i]; } } return ans1-ans2; }
完整的代码如下:
package RangeSum; public class BIT { int[] arr = null; int[] bitArr = null; int n = 0; public BIT(int[] arr){ this.arr = new int[arr.length + 1]; for(int i=0;i<arr.length;i++){ this.arr[i+1] = arr[i]; } this.n = arr.length; this.initBitArr(); } public void initBitArr(){ bitArr = new int[this.arr.length]; for(int i=1;i<=this.arr.length;i++){ this.update(i); } } public void update(int index){ for(int i=index;i <=n ;i += lowbit(i)){ this.bitArr[i] += this.arr[index]; } } public int sum(int from1,int to1){ int ans1 = 0; int ans2 = 0; int from = from1 + 1; int to = to1 + 1; for(int i=to;i>0;i-= lowbit(i)){ ans1 += this.bitArr[i]; } if(from1 > 0){ for(int i=from-1;i>0;i-= lowbit(i)){ ans2 += this.bitArr[i]; } } return ans1-ans2; } public static int lowbit(int t){ return t & (-t); } public static void main(String[] args){ int[] arr = {1,2,3,4,5,6,7,8}; //System.out.println(traversal(arr,2,6)); BIT bit = new BIT(arr); System.out.println(bit.sum(0,6)); } }
- poj 2481 Cows 树状数组解法,详细解析。
- POJ 3468 A Simple Problem with Integers 树状数组解法
- POJ 3468 的树状数组解法
- 树状数组解析与例题
- hdu 1166 敌兵布阵 树状数组解法
- PKU 2299 求解逆序数(使用归并或者树状数组) 树状数组及入门知识
- poj3468--A Simple Problem with Integers(树状数组解法)
- 求解最大连续子数组的算法
- 树状数组解析
- HDU 2852 树状数组解决第K小数 附线段树解法
- 分治算法求解最大连续子数组
- 【a703】求逆序对(树状数组的解法)
- codevs 1082 一维树状数组区间修改区间查询
- Poj 2299 Ultra-QuickSort 树状数组 解法
- POJ 1804 Brainman(5种解法,好题,【暴力】,【归并排序】,【线段树单点更新】,【树状数组】,【平衡树】)
- 动态规划求解连续子数组最大和问题(应该是新的描述方法?)
- HDU 1166 敌兵布阵 (树状数组和线段树解法)
- hdu 1166 线段树树状数组 Java解法
- 【codevs1081】【树状数组】区间修改 单点查询
- ural1890、Timus1890【树状数组解法】