整理:树状数组

关于树状数组的整理

1.何为树状数组?

\(OI\ Wiki\):

树状数组是一种支持 单点修改区间查询 的,代码量小的数据结构。

说人话,就是好写的,但是适用范围窄的线段树/分块

2.树状数组与其他类似数据结构的对比

时间复杂度 空间复杂度 适用范围 码量
\(分块\) \(O(n\sqrt n)\) \(O(n\sqrt n)\) 巨大
\(树状数组\) \(O(nlogn)(常数小)\) \(O(n)\)
\(线段树\) \(O(nlongn)(常数大)\) \(O(n)\) 巨大

3.树状数组的适用条件

\(OI\ Wiki\):

普通树状数组维护的信息及运算要满足 结合律可差分,如加法(和),乘法(积),异或等

但是,在某些时候,树状数组可以尝试维护区间最值

比如:使用两个树状数组/使用拓展树状数组(时间复杂度为 \(O(nlog^2n)\))

4.树状数组的基本思想

就是将原本储存单个元素的数组变成储存一段区间信息的数组,以此提高程序效率,类似于倍增st表,但是支持修改

树状数组所组成的树实际上是 \(i\) 的父亲为 \(i+lowbit(i)\) 的树

这样的树就满足了很多有意思的性质

具体性质见\(OI\;Wiki\)

5.树状数组的基本实现

我们首先来实现单点修改和区间和查询.

对于修改位置 \(x\),我们可以知道,这个位置将影响到这个节点和树中所有这个节点的祖先,而我们又知道 \(i\) 的父亲是 \(i+lowbit(i)\),所以我们同时对所有受影响的区间进行修改即可

void add(int x,int v){
    for(int i=x;i<=n;i+=lowbit(i))a[i]+=v;
}

然后我们来实现区间和查询

我们首先将 \([l,r]\) 的和转换成 \([1,r]\) 的和减去 \([1,l-1]\) 的和,也就变成了单点前缀和查询

又由于我们知道,对于 \(x\) 节点,它所维护的区间为 \([x-lowbit(x)+1,x]\),所以我们为了不重复统计,我们每次统计完之后,直接跳到下一个不交的区间即可

int query(int x){
    int res=0;
    for(int i=x;i;i-=lowbit(i))
        res+=a[i];
    return res;
}
int query(int l,int r){
    return query(r)-query(l-1);
}

区间乘/异或的操作与此类似

那么如果我们进行区间加呢?

首先,由于树状数组只能维护单点修改,因此我们考虑将区间修改转换为单点修改,也就是差分操作,但是我们怎么通过一个差分数组去求前缀和呢?

这里需要进行一些数学推导,我们设差分数组为 \(d\),要求的前缀和为 \(p_i\),经过修改后的原数组为 \(a\)

那么我们可以知道 \(a_i=\sum_{j=1}^{i} d_j\),又因为 \(p_i=\sum_{j=1}^{i} a_j\) 所以我们可以得到 \(p_i=\sum_{j=1}^{i} \sum_{k=1}^{j} d_k\)

但是这样依然不好处理,所以我们对上面的等式进行变形:我们不再求出每个数之后求和,而是考虑求出差分数组中的每一个元素对 \(p_i\) 的贡献

那么 \(p_i =\sum_{j=1}^{i}(d_j*\sum_{k=j}^{i} 1)\)

也就是 \(p_i=\sum_{j=1}^i[d_j*(i-j+1)]\)

我们再将括号拆开 \(p_i=\sum_{j=1}^{i} d_j*(i+1)-\sum_{j=1}^{i}d_j*j\)

所以我们维护两个数组 \(a_i=d_i,b_i=d_i*i\) 即可

struct Binary_tree{
	int a[N],b[N];
	int lowbit(int x){
		return x&-x;
	}
	void clear(){
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
	}
	void add(int x,int v){
		for(int i=x;i<=n;i+=lowbit(i)){
			a[i]+=v;
			b[i]+=x*v;
		} 
	}
	void add(int l,int r,int v){
		add(l,v),add(r+1,-v);
	}
	int query(int x){
		int res=0;
		for(int i=x;i;i-=lowbit(i)){
			res+=a[i]*(x+1);
			res-=b[i];
		}
		return res;
	}
	int query(int l,int r){
		return query(r)-query(l-1);
	}
}

6.权值树状数组

权值树状数组类似于一个计数数组,也就说,记录每一个值出现过多少次,这就可以以更优秀的时间复杂度解决许多问题

比如说求序列中第 \(k\) 小的值,我们可以使用一个类似于快速排序的方法来求出第 \(k\) 小,但是很显然这样子的做的时间复杂度为 \(O(n)\)

但是我们可以利用权值树状数组将这个时间复杂度优化到 \(O(logn)\)

这里运用了倍增的思想

int kth(int k) {
    int x=0;
    for (int i=log2(n);i>=0;i--){
        if(x+(1<<i)<n&&a[x+(1<<i)]<k){
            x+=(1<<i);
            k-=a[x];
        }
    }
    return x+1;
}
posted @ 2025-04-08 18:40  陈牧九  阅读(31)  评论(0)    收藏  举报