树状数组
之前找到一个数据结构与算法的课程,虽然是培训机构出的,不过听了下感觉还是不错的,主要详细介绍了一些高级数据结构与算法,视频还包含习题解析,后续会利用业余时间完成相应的内容,整理一些笔记。
本次回顾树状数组。
学习资料:
https://ke.qq.com/course/480225
https://www.bilibili.com/video/BV1rJ411Q7K8?from=search&seid=380339915135149037
参考资料:
《算法笔记》——胡凡
知识回顾
lowbit函数
假设
那么lowBit函数返回
实现如下:
int lowBit(int x){
return x & (-x);
}
备注:将$x$表示为二进制,那么$\mathrm{lowBit}(x)$返回$x$二进制最靠右边的$1$,以及其右边的$0$,所以
消去了$x$最靠右侧的$1$。
基本介绍
假设原数组为$A[1],\ldots, A[n]$,定义树状数组$C[1],\ldots,C[n]$(下标从$1$开始),其中
即$C[x]$管理位置$x$之前(包括$x$)$\text {lowbit}({x})$个元素。
树状数组结构:
适用场景:单点更新,区间查询。
前缀和
树状数组的主要应用场景是计算前缀和:
定义前缀和
那么
所以可以利用$C[n]$计算前缀和,代码如下:
int sum(int i){
//计算a[1] + ... + a[i]
int s = 0;
while (i > 0){
s += c[i];
i -= lowBit(i);
}
return s;
}
时间复杂度:
每次消除最靠右侧的$1$,利用二进制表示可得时间复杂度为$O(\log n)$。
区间和
利用前缀和不难得到区间和的计算方式:
int sumRange(int i, int j) {
//计算a[i] + ... + a[j] = sum(j) - sum(i - 1)
return sum(j) - sum(i - 1);
}
单点更新
假设现在要对数组$A$进行更新, 特别的,我们要给$A[i]$增加$d$,那么所有包含$A[i]$的$C[j]$都要增加$d$,如果图中表示如下(虚线方向):
显然$C[i]$要增加$d$,考虑下一个包含$A[i]$的$C[i+d]$,由树状数组的定义可得$C[i+d]$管理的元素数量必然要大于$C[i]$管理的元素数量,即
如果
那么由lowbit的定义可得
如果
那么
所以最小的
因此可得更新规则:
void add(int i, int x) {
//nums[i] += x
while (i <= n){
c[i] += x;
i += lowBit(i);
}
}
二维情形
增加一重循环即可:
int c[maxn][maxn]; //二维树状数组
//二维add函数位置为(x,y)的整数加上v
void add(int x, int y, int v){
for(int i = x; i < maxn; i += lowbit(i)){
for(int j = y; j < maxn; j+= lowbit(j)){
c[i][j] += v;
}
}
}
//二维getSum函数返回(1, 1)到(x, y)的子矩阵中元素之和
int getSum(int x, int y){
int sum = 0;
for(int i = x; i > 0; i -= lowbit(i)){
for(int j = y; j > 0; j -= lowbit(j)){
sum += c[i][j];
}
}
return sum;
}
例题
307. 区域和检索 - 数组可修改
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
#include <stack>
#include <queue>
#include <cstring>
#include <cstdlib>
#include <utility>
#include <climits>
using namespace std;
class NumArray {
public:
int n;
int* c;
vector<int> nums;
NumArray(vector<int>& nums) {
this->nums = nums;
n = nums.size();
c = new int[n + 1];
fill(c, c + n + 1, 0);
//建bit
for (int i = 1; i <= n; i++){
c[i] += nums[i - 1];
int j = i + lowBit(i);
//后继节点在,则更新
if (j <= n){
c[j] += c[i];
}
}
}
int lowBit(int x){
return x & (-x);
}
void add(int i, int x) {
//nums[i] += x
i++;
while (i <= n){
c[i] += x;
i += lowBit(i);
}
}
void update(int i, int val) {
//nums[i] = val
add(i, val - nums[i]);
//注意要更新nums, 否则后续更新有误
nums[i] = val;
}
int sum(int i){
//计算a[0] + ... + a[i]
//增加索引
i++;
int s = 0;
while (i > 0){
s += c[i];
i -= lowBit(i);
}
return s;
}
int sumRange(int i, int j) {
//计算a[i] + ... + a[j] = sum(j) - sum(i - 1)
return sum(j) - sum(i - 1);
}
};
int main(){
return 0;
}
304. 二维区域和检索 - 矩阵不可变
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <unordered_map>
#include <unordered_set>
#include <stack>
#include <queue>
#include <cstring>
#include <cstdlib>
using namespace std;
class NumMatrix {
public:
int** cursum;
NumMatrix(vector<vector<int>>& matrix) {
int n = matrix.size();
if (n == 0){
return;
}
int m = matrix[0].size();
int** c;
c = new int*[n + 1];
cursum = new int*[n + 1];
for (int i = 0; i <= n; i++){
c[i] = new int[m + 1];
cursum[i] = new int[m + 1];
}
for (int i = 0; i <= n; i++){
for (int j = 0; j <= m; j++){
c[i][j] = 0;
cursum[i][j] = 0;
}
}
//建立二维树状数组
//第一轮按行求每一维的树状数组
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
c[i][j] += matrix[i - 1][j - 1];
int k = j + lowBit(j);
//后继点存在
if (k <= m){
c[i][k] += c[i][j];
}
}
}
//第二轮按列求
for (int j = 1; j <= m; j++){
for (int i = 1; i <= n; i++){
cursum[i][j] += c[i][j];
int k = i + lowBit(i);
//后继点存在
if (k <= n){
cursum[k][j] += cursum[i][j];
}
}
}
}
int lowBit(int x){
return x & (-x);
}
int sumCorner(int i, int j){
int res = 0;
for (int x = i; x > 0; x -= lowBit(x)){
for (int y = j; y > 0; y -= lowBit(y)){
res += cursum[x][y];
}
}
return res;
}
int sumRegion(int row1, int col1, int row2, int col2) {
return sumCorner(row2 + 1, col2 + 1) + sumCorner(row1, col1) - sumCorner(row1, col2 + 1) - sumCorner(row2 + 1, col1);
}
};
int main(){
return 0;
}