用于数组区间求和,相比于前缀和的方式,树状数组的优点是能有效应对单点修改

树状数组区间求和以及单点修改的时间复杂度都是O(logn),而前缀和虽然区间求和时间复杂度为O(1),但更新一个元素后需要重新计算前缀和,时间复杂度为O(n),如果需要频繁更新数组元素,则效率不如树状数组。

树状数组的思想是将区间分成大小不同,相互包含的一些节点,在区间求和或单点修改时,只需要查询或更新较大的节点,而不需要处理较小的节点。

以下是一个树状数组的示例:

draw.io

Diagram attachment access error: cannot display diagram

原数组是a1~a8,数组状数组是c1~c8,它们拥有的元素个数完全一样,并且都从下标1开始存储元素。

分析树数组和原数组的关系,列出对应的二进制:

树状数组节点下标节点下标的二进制对应原数组节点范围最后一个元素下标元素个数
10001111
200101~222
30011331
401001~444
50101551
601105~662
70111771
810001~888

这里首先可以推导出cx管理的元素个数,推导公式如下:

int lowbit(int x) {
    return x & -x;
}

这个公式可以返回x的二进制右侧第一个非0的部分,比如:0001,0010,0011,0100,0101,0110,01111000

推导出元素个数后,就可以知道cx管理的元素范围为[x-lowbit(x)+1, x]。,比如c8管理的元素范围是[1, 8]

根据上面的二进制关系,同样可以推导出原数组ax归哪些树状数组节点管理,比如从上面的例子可知,a1归c1, c2, c4, c8管理,那么在更新a1的值时,就要同步更新c1, c2, c4, c8 的值。

假设节点数为n,则更新节点ak的过程如下:

void update(int k, int val) {
    for(int i = k; i <= n; i += lowbit(i); {
        c[i] += val;
    }
}

代入上面的表格进行验证即可。

接下来是求前缀和,假设要求区间[1, x]的和,那么倒着上面的计算公式进行求解即可:

int getsum(int x) {
    int ans = 0;
    while(x >= 1) {
		ans += c[x];
		x -= lowbit(x);
	}
	return ans;
}

同样代入上面的表格进行验证即可。











  • 无标签