数据结构--树状数组
树状数组(Fenwick Tree)
概述
树状数组是一种用于高效处理动态数组中前缀和查询的数据结构。它能够在 O ( l o g n ) O(log n) O(logn) 时间复杂度内完成以下操作:
- 更新数组中的元素
O(logn)
- 查询数组前缀和
O(logn)
数组: O(1)
更新,O(n)
前缀和
前缀和数组: O(n)
更新,O(1)
前缀和
如果问题同时要大量更新和求前缀和,上述两种数据结构均会寄掉
树状数组则采取折中思路,把整体复杂度降低至O(logn)
数据结构
先放张整体结构图:

核心思想:二进制
对任意数x
可将其二进制分解
x = 2 i k + 2 i k − 1 + 2 i k − 2 + ⋯ + 2 i 1 x = 2^{i_k} + 2^{i_{k-1}} + 2^{i_{k-2}} + \cdots + 2^{i_1} x=2ik+2ik−1+2ik−2+⋯+2i1
其中 i k > i k − 1 > i k − 2 > ⋯ > i 1 \text{其中}i_k>i_{k-1}>i_{k-2}>\cdots>i_1 其中ik>ik−1>ik−2>⋯>i1
从而将区间(0, x]
分为以下几个部分:
$(x - 2^{i_1}, x] \longrightarrow \text{长度 } 2^{i_1} $
( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] ⟶ 长度 2 i 2 (x - 2^{i_1} - 2^{i_2}, x - 2^{i_1}] \longrightarrow \text{长度 } 2^{i_2} (x−2i1−2i2,x−2i1]⟶长度 2i2
( x − 2 i 1 − 2 i 2 − 2 i 3 , x − 2 i 1 − 2 i 2 ] ⟶ 长度 2 i 3 (x - 2^{i_1} - 2^{i_2} - 2^{i_3}, x - 2^{i_1} - 2^{i_2}] \longrightarrow \text{长度 } 2^{i_3} (x−2i1−2i2−2i3,x−2i1−2i2]⟶长度 2i3
( 0 , 2 i k ] ⟶ 长度 2 i k (0, 2^{i_k}] \longrightarrow \text{长度 } 2^{i_k} (0,2ik]⟶长度 2ik
容易发现,对于任意一段区间(L,R]
区间长度为lowbit(x)
,区间左端点L = R - lowbit(R)
则在上述规则下,只要确定右端点,左端点的信息也唯一确定
树状数组用一个数组来存储序列的信息:
tr[x]
:存储序列在[x - lowbit(x) + 1, x]
之间的数的片段和
则按照前面的区间划分规则
∑ i = 1 x a [ i ] = ∑ i = x − 2 i 1 + 1 x a [ i ] + ∑ i = x − 2 i 1 − 2 i 2 + 1 x − 2 i 1 a [ i ] + ⋯ + ∑ i = 1 2 i k a [ i ] \sum_{i = 1}^x{a[i]} = \sum_{i=x-2^{i_1}+1}^x{a[i]} + \sum_{i=x-2^{i_1}-2^{i_2}+1}^{x-2^{i_1}}{a[i]} + \cdots + \sum_{i=1}^{2^{i_k}}{a[i]} ∑i=1xa[i]=∑i=x−2i1+1xa[i]+∑i=x−2i1−2i2+1x−2i1a[i]+⋯+∑i=12ika[i]
= t r [ x ] + t r [ x − 2 i 1 ] + ⋯ + t r [ 2 i k ] \qquad \qquad = tr[x] + tr[x-2^{i_1}] + \cdots + tr[2^{i_k}] =tr[x]+tr[x−2i1]+⋯+tr[2ik]
= t r [ x ] + t r [ x − lowbit ( x ) ] + t r [ ( x − lowbit ( x ) ) − lowbit ( x − lowbit ( x ) ) ] + ⋯ \qquad \qquad = tr[x] + tr[x-\text{lowbit}(x)] + tr[(x-\text{lowbit}(x))-\text{lowbit}(x-\text{lowbit}(x))] + \cdots =tr[x]+tr[x−lowbit(x)]+tr[(x−lowbit(x))−lowbit(x−lowbit(x))]+⋯
看到公式的第三行,很容易想到可以用递归来实现,只需每层往下不断-lowbit(t)
就行
x最多只有logx位1,所以树状数组求前缀和的操作复杂度是O(logn)
类似的,若要实现在原数组第x
位上添加c
t r [ x ] , t r [ x + l o w b i t ( x ) ] , t r [ ( x + l o w b i t ( x ) ) + l o w b i t ( t r [ x ] + l o w b i t ( x ) ) ] , ⋯ tr[x], tr[x + lowbit(x)], tr[(x + lowbit(x)) + lowbit(tr[x]+lowbit(x))],\cdots tr[x],tr[x+lowbit(x)],tr[(x+lowbit(x))+lowbit(tr[x]+lowbit(x))],⋯
均需添加c
(可能这个结论不是那么明显,读者可自行思考其中的原理,后续笔者将补充上证明)
当然这里不会无穷往后面加,我们只需用到1~n的数据,当加到超过n就可以停了,故整该操作的复杂度仍旧为O(logn)
(分析同求和
一个更容易理解的视频讲解
操作
1. lowbit运算
复杂度:O(1)
代码如下,大家可以自行找几个数验证一下
int lowbit(int x)
{// 取出x的最后一位1return x & -x;
}
2.添加
复杂度:O(logn)
int add(int x, int c)
{// 向第x位添加c,c可正可负// 对所有含第x位的树节点均加上cfor (int i = X; i <= n; i += lowbit(i)) tr[i] += c;
}
3.前缀和
复杂度:O(logn)
int sum(int x)
{// 对第1~x位求和// 计算当前数存的值,然后迭代求剩余节点的值if (!x) return 0;return tr[x] + sum(x - lowbit(x));
}
例题
洛谷 P10589 楼兰图腾
题目描述
在完成了分配任务之后,西部 314 来到了楼兰古城的西部。相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V
),一个部落崇拜铁锹(∧
),他们分别用 V
和 ∧
的形状来代表各自部落的图腾。
西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 N N N 个点,经测量发现这 N N N 个点的水平位置和竖直位置是两两不同的。西部 314 认为这幅壁画所包含的信息与这 N N N 个点的相对位置有关,因此不妨设坐标分别为 ( 1 , y 1 ) , ( 2 , y 2 ) , ⋯ , ( n , y n ) (1,y_1),(2,y_2),\cdots,(n,y_n) (1,y1),(2,y2),⋯,(n,yn),其中 y 1 ∼ y n y_1\sim y_n y1∼yn 是 1 1 1 到 n n n 的一个排列。
如图,图中的 y 1 = 1 y_1=1 y1=1, y 2 = 5 y_2=5 y2=5, y 3 = 3 y_3=3 y3=3, y 4 = 2 y_4=2 y4=2, y 5 = 4 y_5=4 y5=4。
西部 314 打算研究这幅壁画中包含着多少个图腾,其中 V
图腾的定义如下(注意:图腾的形式只和这三个纵坐标的相对大小排列顺序有关) 1 ≤ i < j < k ≤ n 1\le i<j<k\le n 1≤i<j<k≤n 且 y i > y j y_i>y_j yi>yj, y j < y k y_j<y_k yj<yk;
而崇拜 ∧
的部落的图腾被定义为 1 ≤ i < j < k ≤ n 1\le i<j<k\le n 1≤i<j<k≤n 且 y i < y j y_i<y_j yi<yj, y j > y k y_j>y_k yj>yk;
西部 314 想知道,这 n n n 个点中两个部落图腾的数目。因此,你需要编写一个程序来求出 V
的个数和 ∧
的个数。
输入
第一行一个正整数 n n n;
第二行是 n n n 个正整数,分别代表 y 1 , y 2 , ⋯ , y n y_1,y_2,\cdots,y_n y1,y2,⋯,yn。
n ≤ 200000 n\le 200000 n≤200000,答案不超过 2 63 − 1 2^{63} - 1 263−1。
输出
输出两个数,中间用空格隔开,依次为 V
的个数和 ∧
的个数
样例输入 #1
5
1 5 3 2 4
样例输出 #1
3 4
思路
对V字形图腾,只需要知道每个点前后各有多少个比它高的 ; ∧ 字形反之 对 \text{V} 字形图腾,只需要知道每个点前后各有多少个比它高的; ∧ 字形反之 对V字形图腾,只需要知道每个点前后各有多少个比它高的;∧字形反之
朴素思路是枚举到第 i 个点,再一一枚举前后比它高的元素 , 朴素思路是枚举到第i个点,再一一枚举前后比它高的元素, 朴素思路是枚举到第i个点,再一一枚举前后比它高的元素,
复杂度 O ( n 2 ) ∼ 1 0 10 , 需要优化 复杂度O(n^2)\sim 10^{10},需要优化 复杂度O(n2)∼1010,需要优化
想象有这么一个 1 ∼ n 的数轴,从左往右一一读取每个点 , 读完一个点就在数轴上标记 1 想象有这么一个1 \sim n的数轴,从左往右一一读取每个点,读完一个点就在数轴上标记1 想象有这么一个1∼n的数轴,从左往右一一读取每个点,读完一个点就在数轴上标记1
则每个点左边比自己高的点数量其实就是前缀和 s u m ( n ) − s u m ( y ) 则每个点左边比自己高的点数量其实就是前缀和sum(n) - sum(y) 则每个点左边比自己高的点数量其实就是前缀和sum(n)−sum(y)
而右边的只需从右往左一一读取再来一遍就行 而右边的只需从右往左一一读取再来一遍就行 而右边的只需从右往左一一读取再来一遍就行
然而这个前缀数组需要一直修改 然而这个前缀数组需要一直修改 然而这个前缀数组需要一直修改
尽管我们可以用 O ( 1 ) 的时间读出高点 , 但是需要 O ( n ) 的时间去维护它 尽管我们可以用O(1)的时间读出高点,但是需要O(n)的时间去维护它 尽管我们可以用O(1)的时间读出高点,但是需要O(n)的时间去维护它
复杂度仍旧是 O ( n 2 ) , 这就让我们想到了树状数组 → 更新和求和复杂度均为 O ( l o g n ) 复杂度仍旧是O(n^2),这就让我们想到了树状数组 \to 更新和求和复杂度均为O(logn) 复杂度仍旧是O(n2),这就让我们想到了树状数组→更新和求和复杂度均为O(logn)
树状数组优化后复杂度变为 O ( n l o g n ) , 满足要求 树状数组优化后复杂度变为O(nlogn),满足要求 树状数组优化后复杂度变为O(nlogn),满足要求
代码
// 利用树状数组存储某个数左/右 大于/小于它自己的数的数量
#include <iostream>
#include <cstring>
#include <algorithm>using namespace std;typedef long long LL;const int N = 2e5 + 10;int n;
int a[N];
int tr[N];
int gre[N], low[N]; // 存储
LL res1, res2;inline int lowbit(int x)
{return x & (-x);
}inline int sum(int x)
{// 求前x项的和if (!x) return 0;return tr[x] + sum(x - lowbit(x));
}inline void add(int x, int c)
{for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}int main()
{scanf("%d", &n);for (int i = 1; i <= n; i ++ )scanf("%d", &a[i]);// 从左到右来一遍for (int i = 1; i <= n; i ++ ){int y = a[i];gre[i] = sum(n) - sum(y); // 统计在i左边y + 1到n的数的数量low[i] = sum(y - 1); // 统计在i左边1到y - 1的数的数量add(y, 1); // 插入这个数}memset(tr, 0, sizeof tr);// 从右到左再来一遍for (int i = n; i; i --){int y = a[i];res1 += (LL)gre[i] * (sum(n) - sum(y));res2 += (LL)low[i] * (sum(y - 1));add(y, 1);}printf("%lld %lld", res1, res2);return 0;}
:::
POJ 2182 迷路的奶牛 ⇒ 从后往前慢慢确定每头牛高度 , 树状数组前缀和 + 二分 \Rightarrow 从后往前慢慢确定每头牛高度,树状数组前缀和+二分 ⇒从后往前慢慢确定每头牛高度,树状数组前缀和+二分
POJ 3468 A Simple Problem with Integers ⇒ 维护两个前缀和数组的树状数组 \Rightarrow 维护两个前缀和数组的树状数组 ⇒维护两个前缀和数组的树状数组