本文旨在讲解:node
树状数组或二元索引树(英语:Binary Indexed Tree),又以其发明者命名为 \(\mathrm{Fenwick}\) 树。最先由 \(\mathrm{Peter\; M. Fenwick}\) 于1994年以 《A New Data Structure for Cumulative Frequency Tables[1]》为题发表在 《SOFTWARE PRACTICE AND EXPERIENCE》。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。它能够以 \(\mathcal{O(\log n)}\) 的时间获得任意前缀和(区间和)。ios
不少初学者确定和我同样,只知晓 BIT 代码精炼,语法简明。对于原理好像了解,却又如雾里探花总感受隔着些什么。c++
按照 Peter M. Fenwick 的说法,BIT 的产生源自整数与二进制的类比。算法
Each integer can be represented as sum of powers of two. In the same way, cumulative frequency can be represented as sum of sets of subfrequencies. In our case, each set contains some successive number of non-overlapping frequencies.数组
简单翻一下:每一个整数能够用二进制来进行表示,在某些状况下,序列累和(这里没有翻译为频率)也能够用一组子序列累和来表示。在本例子中,每一个集合都有一些连续不重叠的子序列构成。数据结构
实际上, BIT 也是采用相似的想法,将序列累和类比为整数的二进制拆分,每一个前缀和拆分为多个不重叠序列和,再利用二进制的方法进行表示。这与 Integer 的位运算很是类似。app
之因此命名为: Binary Indexed Tree,在论文中 Fenwick 有以下解释:学习
In recognition of the close relationship between the tree traversal algorithms and the binary representation of an element index,the name "binar indexed tree" is proposed for the new structure.ui
也就是考虑到:树的遍历方法与二值表示之间的紧密联系,所以将其命名为二元索引树。spa
在介绍原理以前先对于一些关键的符号作出定义:
在学习 BIT 时,很容易忽略 BIT 设计的思想,而仅仅停留在对于其代码简洁精炼的赞叹上,因此第一步咱们将体会 BIT 是如何类比;如何设计;如何实现的。
如上图所示:咱们给定一个整数: \(num = 13\)
咱们尝试将 \(num\) 用二进制进行表示: \(1101_2 = 1000_2 + 100_2 + 1_2\) 。能够看到 \(num\) 能够由\(3\)个二进制数组成。且拆分的个数老是 \(\mathcal{O(\log_2n)}\) 级的,所以我猜测Fenwick便开始思考如何将一个子序列,借助二进制的特色快速的表示出来。
首先,依据最简单的拆分方法(即与二进制拆分相同)如图左示。显然这个方法具备缺陷,某些序列会被重复计算,而有些序列则没有被包含在内,所以解决问题的关键,同时也是 BIT 的核心思想即是如何基于编号,构件一个不重叠的子序列集合。
如右图所示,该拆分方案能很好的实现不重叠的子序列集合,咱们尝试将其列出以发现其中的规律:
通过观察:
设某编号的二进制为 \(\mathrm{XXX}bit\mathrm{XXX}_2\) ,设 \(bit\) 为当前须要考虑的位\((bit=1)\),\(\mathrm{X}\) 为\(0 \;or\; 1\) ,则其表示的范围是:
\([XXX0000_2 + 1, XXX0000_2 + bit000_2]\) ,换一句话说:假如序列编号在 \(bit\) 位为1,则其表明的子序列具备以下性质:
假如咱们逆序的看待以前\(num=13=1101_2\)的例子:
首先处理\(bit=1\)这一位,其表明的范围是:\([1100_2 + 0001_2, 1100_2 + 0001_2]\)。而后在\(num\)上减去他:\(num -= (1 << (bit-1)) = 1100_2\)
而后,咱们处理\(bit=3\)这一位:其表明的范围是:\([1000_2 + 0001_2, 1000_2 + 0100_2]\)。一样,咱们在\(num\)上减去它。
最后咱们处理\(bit=4\)这一位:其表明的范围是:\([0000_2 + 0001_2, 0000_2 + 1000_2]\)。至此,处理结束。
咱们回顾整个处理流程,能够惊讶的发现,若是咱们按照逆序处理,咱们每次处理的\(bit\)都是当前编号的最后的为1位。咱们将每次处理的\(bit\)定义为 \(\mathrm{lowbit}\) (note:这是 BIT 中重要的概念)
用通俗的语言:每一个 \(\mathrm{lowbit}\) 都表明其管辖的某一段子序列,又由于 \(\mathrm{lowbit}\) 的值会随着处理不断增大,其控制的范围也会不断增大。其控制范围为:\([cur - lowbit(cur) + 1, cur]\)
如:\(c[13] = tree[13] + tree[12] + tree[8]\)
所以,咱们能够作出以下总结:
BIT 的原理类比自 Integer 的二进制表示。
BIT 对应的数组 \(tree[i] := 子序列 i 的值\) ,每一个 \(tree[i]\) 控制 \([i - \mathrm{lowbit(i)}+1, i]\) 范围内的\(f[i]\)值。
利用BIT计算 \(c[i]\) 时,经过相似整数的二进制拆分,将 \(c[i]\) 拆分为 \(\mathcal{O(\log_2 n)}\) 个 \(tree[j]\) 进行求解。求解的流程为不断累加 \(tree[i]\) 并置 $ i \leftarrow i - \mathrm{lowbit(i)}$
计算流程的伪代码: let ans <- 0 while i > 0: sub_sum <- tree[i] // 获取子序列累和 i <- i - lowbit(i) // 更新 i ans <- ans + sub_sum return ans
上图是树状数组很是经典的展现图,经过此图能够快速的了解:\(tree[i] := \sum \limits_{i - \mathrm{lowbit}(i)+1}^{i}f[i]\) 对应的含义。
到这里仍是不由感叹一句:“文章本天成,妙手偶得之”,BIT 这个数据结构实在是精巧。
定义 bitcnt(x) := x二进制中 1 的个数
,则根据前文的分析,计算 \(c[i]\) 时类比整数的二进制拆分,咱们只须要计算 \(bitcnt(i)\) 个子序列的和。每一个子序列经过不断进行 \(\mathrm{lowbit}\) 运算进行获取。
\(\mathrm{lowbit}\) 运算为取数 \(x\) 的最低位的 1 ,最经常使用的方法为:\(\mathrm{lowbit(x)= (x \& (-x))}\)
上图展现了一个大小为 \(16\) 的 BIT,能够经过图示清楚的理解 BIT query 的原理:即不断询问当前 \(i\) 指示的子序列和(\(tree[i]\)),并经过 \(\mathrm{lowbit}\) 运算指向下一个子序列和。
其 C++
代码以下:
T tree[maxn]; template <typename T> T query(int i){ T res = 0; while (i > 0){ res += tree[i]; i -= lowbit(i); } return res; }
update 实际上能够当作 query 的逆过程,简单来讲便是:若要将 \(f[i] += x\),则从 \(tree[i]\) 开始不断向上更新直到达到 BIT 的上界。
上图展现了 BIT 更新的流程,这里主要说明其中一个须要注意的点:为何咱们首先须要更新 \(tree[i]\) 而不是其余的,如何保证这就是起始点?(能够本身思考一下)
这是我曾在学习 BIT 的过程当中比较困惑的一个点:答案在于 \(tree[i]\) 所管辖的子序列范围,咱们知道 \(tree[i] 管辖 [i - lowbit(i) + 1, i]\) 这个范围,所以 \(tree[i]\) 是第一个管辖 \(f[i]\) 的元素,因此咱们只须要从这个位置不断向上更新便可。
其 C++
代码以下:
int n; // BIT 的大小, BIT index 从 1 开始 T tree[maxn]; template <typename T> void add(int i, T x){ while (i <= n){ tree[i] += x; i += lowbit(i); } }
template<typename T> struct BIT{ #ifndef lowbit #define lowbit(x) (x & (-x)); #endif static const int maxn = 1e3+50; int n; T t[maxn]; BIT<T> () {} BIT<T> (int _n): n(_n) { memset(t, 0, sizeof(t)); } BIT<T> (int _n, T *a): n(_n) { memset(t, 0, sizeof(t)); /* 从 1 开始 */ for (int i = 1; i <= n; ++ i){ t[i] += a[i]; int j = i + lowbit(i); if (j <= n) t[j] += t[i]; } } void add(int i, T x){ while (i <= n){ t[i] += x; i += lowbit(i); } } /* 1-index */ T sum(int i){ T ans = 0; while (i > 0){ ans += t[i]; i -= lowbit(i); } return ans; } /* 1-index [l, r] */ T sum(int i, int j){ return sum(j) - sum(i - 1); } /* href: https://mingshan.fun/2019/11/29/binary-indexed-tree/ note: C[i] --> [i - lowbit(i) + 1, i] father of i --> i + lowbit(i) node number of i --> lowbit(i) */ };
树状数组(BIT)的主要优点在于:
query
与 update
操做时间复杂度都只须要 \(\mathcal{O(\log n)}\) 。lazy tag
也存在影响)。而缺点在于:
树状数组通常用于解决大部分基于区间上的更新以及求和问题。
下面来谈一谈线段树和树状数组在使用上的不一样:
线段树与树状数组的区别 线段树和树状数组的基本功能都是在某一知足结合律的操做(好比加法,乘法,最大值,最小值)下,\(\mathcal{O}(\log n)\)的时间复杂度内修改单个元素而且维护区间信息。
不一样的是,树状数组只能维护前缀“操做和”(前缀和,前缀积,前缀最大最小),而线段树能够维护区间操做和。可是某些操做是存在逆元的(即:能够用一个操做抵消部分影响,减之于加,除之于乘),这样就给人一种树状数组能够维护区间信息的错觉:维护区间和,模质数意义下的区间乘积,区间 \(\mathrm{xor}\) 和。能这样作的本质是取右端点的前缀结果,而后对左端点左边的前缀结果的逆元作一次操做,因此树状数组的区间询问实际上是在两次前缀和询问。
因此咱们能看到树状数组能维护一些操做的区间信息但维护不了另外一些的:最大/最小值,模非质数意义下的乘法,缘由在于这些操做不存在逆元,因此就无法用两个前缀和作。
总结来讲:线段树只须要保证区间操做的可结合性,可加性(即一个大区间的结果能够由较小区间的结果计算获得);而树状数组除了须要知足上述条件,还须要知足可抵消性,也就是能够经过一个操做抵消掉不须要区间的贡献(由于 BIT 只能维护前缀结果)。仅为我的看法
很是简单,只须要套模板便可。
// 上述模板部分省略 using ll = long long; const int maxn = 1e6+50; ll f[maxn]; int main(){ ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; int q; cin >> q; for (int i = 1; i <= n; ++ i) cin >> f[i]; BIT<ll> bit(f, n); for (int i = 0; i < q; ++ i){ int type; cin >> type; if (type == 1){ int i, x; cin >> i >> x; bit.add(i, (ll) x); }else { int l, r; cin >> l >> r; cout << bit.sum(l, r) << '\n'; } } return 0; }
该模板题则难上许多,须要对问题分析建模。
咱们须要考虑如何建模表示 \(tree\) 数组。
首先,设更新操做为:在 \([l, r]\) 上增长 \(x\)。咱们考虑如何建模维护新的区间前缀和 \(c^{\prime}[i]\)。
下面分状况讨论:
这种状况下,不须要任何处理, \(c^{\prime}[i] = c[i]\)
这种状况下,\(c^{\prime}[i] = c[i] + (i - l + 1) \cdot x\)
这种状况下,\(c^{\prime}[i]=c[i] + (r-l+1)\cdot x\)
所以以下图所示,咱们能够设两个 BIT,那么\(c^{\prime}[i] = \mathrm{sum(bit_1,i)+sum(bit_2,i) \cdot i}\),对于区间修改等价于:
#include <bits/stdc++.h> using namespace std; // 模板代码省略 // 这里作的是单点查询,可是实现的为区间查询 using ll = long long; ll get_sum(BIT<ll> &a, BIT<ll> &b, int l, int r){ auto sum1 = a.sum(r) * r + b.sum(r); auto sum2 = a.sum(l - 1) * (l - 1) + b.sum(l - 1); return sum1 - sum2; } int n, q; const int maxn = 1e6 + 50; ll f[maxn]; int main(){ // ios::sync_with_stdio(0); // cin.tie(0); cin >> n >> q; BIT<ll> bit1, bit2; for (int i = 1; i <= n; ++ i) cin >> f[i]; bit1.init(n), bit2.init(f, n); for (int i = 0; i < q; ++ i){ int type; cin >> type; if (type == 1){ int l, r, x; cin >> l >> r >> x; bit2.add(l, (ll) -1 * (l - 1) * x), bit2.add(r + 1, (ll) r * x); bit1.add(l, (ll) x), bit1.add(r + 1, (ll) -1 * x); }else { int i; cin >> i; cout << get_sum(bit1, bit2, i, i) << '\n'; } } return 0; }
BIT 求解逆序对是很是方便的,在初学时我没有想到过 BIT 还能用于求解逆序对。在这里我借逆序对来引出一个小技巧:离散化
BIT 求逆序对的方法很是简单,逆序对指:i < j and a[i] > a[j]
,统计逆序对实际上就是统计在该元素 a[i]
以前有多少元素大于他。
咱们能够初始化一个大小为 \(maxn\) 的空 BIT(全为0)。随后:
a[i]
,计算区间 [1, a[i]]
的和,更新答案 ans = i - sum([1, a[i]])
a[i]
的值,tree[a[i]] <- tree[a[i]] + 1
举个例子:
eg: [2,1,3,4] BIT: 0, 0, 0, 0 >2, sum(2) = 0, ans += 0 - sum(2) -> ans = 0 BIT: 0, 1, 0, 0 >1, sum(1) = 0, ans += 1 - sum(1) -> ans = 1 BIT: 1, 1, 0, 0 >3, sum(3) = 2, ans += 2 - sum(3) -> ans = 1 BIT: 1, 1, 1, 0 >4, sum(4) = 3, ans += 3 - sum(4) -> ans = 1
实际上,即是借助 BIT 高效计算前缀和的性质实现了快速打标记,先统计在我以前有多少个标记(这些都是合法对),再将本身所在位置的标记加 \(1\)。
所以,很容易写出这段代码:
// 仅保留核心代码 int reversePairs(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; int mx = *max_element(nums.begin(), nums.end()); BIT<int> bit(mx); // 由于最大只到最大值的位置 int ans(0); for (int i = 0; i < n; ++ i){ ans += (i - bit.sum(nums[i])); bit.add(nums[i], 1); } return ans; }
可是这个代码有很是严重的问题,首先假如 mx = 1e9
就会出现段错误;或者假如 nums[i] < 0
则会出现访问越界的问题,可是实际上题目中说明了:数组最多只有 50000个元素,也就是咱们须要想办法将坐标离散化,保留其大小顺序便可。
#define lb lower_bound #define all(x) x.begin(), x.end() const int maxn = 5e4 + 50; struct node{ int v, id; }f[maxn]; // 离散化结构体 int arr[maxn]; bool cmp(const node&a, const node &b){ return a.v < b.v; } class Solution { public: int reversePairs(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; BIT<int> bit(n); for (int i = 1; i <= n; ++ i){ f[i].v = nums[i - 1], f[i].id = i; // 赋值用于排序 } sort(f + 1, f + 1 + n, cmp); int cnt = 1, i = 1; while (i <= n){ /* 用于去重,当有相同元素时其对应的 cnt 应该相同 */ if (f[i].v == f[i - 1].v || i == 1) arr[f[i].id] = cnt; else arr[f[i].id] = ++cnt; ++ i; } int ans = 0; for (int i = 0; i < n; ++ i){ int pos = arr[i + 1]; ans += i - bit.sum(pos); bit.add(pos, 1); } return ans; } };
上面的方法是离散化操做的一种方式,有一点复杂,须要注意的细节比较多。
实际上,该方法即是经过保留每一个元素的所在位置,并将其排序,排序后本身在第 \(i\) 个则将其值 arr[id] = i
离散化为 \(i\) 。这样既能够避免负数,过大的数形成的访问或者内存错误,也充分的保留了各元素之间的大小关系。
离散化的复杂度为 \(\mathcal{O(\log n)}\) ,实际上也就是排序的复杂度。
总结:离散化--结构体方法
通用性:★★
- 设置结构体
node
,包含属性val
与id
,初始化结构体数组f
和离散化数组arr
。- 排序
f
,并从1
开始遍历,arr[f[i].id] = i
,将val
值更新为k-th min
也就是其在元素中按大小排列的编号。
能够发现,结构体方法对于空间要求较大,且在去重方面须要下功夫,稍后咱们会讲解另外一种离散化方法,你也能够试试用后文的离散化方法再次解决这题。
能够看到这题与逆序对的区别在于,翻转对的定义是:i < j
且 a[i] > 2*a[j]
。其大小关系发生了变化,再也不是原来单纯的大小关系,而存在值的变化。
咱们能够思考下可否用结构体进行离散化,简单思考后发现:假如第 i
个元素离散化以后的编号为 id1
,则咱们没法肯定编号为 2 * id1
所对应元素的 val
值之间的关系。可能出现以下状况:
id1 = 1, val = 2 2 * id1 = 2, val' = 3
因此,咱们须要思考一个新的方法来进行离散化。须要注意的是,咱们的关键点在于:如何快速的询问一个元素在一个数组中是第几大的元素。好比,在数组中快速询问某个值的两倍是第几大的。
实际上,稍微有基础的话答案便很是清晰:二分查找,咱们能够首先将数组进行排序,利用 \(lower_{bound}\) 快速找到第一个大于等于该元素所对应的位置,用代码来讲的话:pos = lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1
。
eg: nums = [3, 2, 4, 7] farr = sort(nums) -> farr = [2, 3, 4, 7] pos(4) = lower_bound(..., 4) - farr.begin() + 1 = 3 即可以快速找到 4 的编号为 3 (1-index)
可是,有一个问题须要注意:
eg: nums = [3, 2, 5, 7] farr = sort(nums) -> farr = [2, 3, 5, 7] pos(4) = lower_bound(...,4) - farr.beign() + 1 = 3 但实际上,5 > 4,此次询问错误了!!!
为何会出现询问错误的状况呢?(所以咱们须要找到的是最后一个小于等于元素 x
的对应位置,而二分查找是大于等于 x
的第一个元素,当原数组中不存在 x
时,便会出现询问出错的状况。)
有多种方法能够解决这个问题,可是最为方便的仍是直接将须要查询的元素所有加进去,也就是 2 * x
所有添加到数组中,从而保证必定存在该元素,又由于 lower_bound
的性质,咱们无需去重。
using vi = vector<int>; using vl = vector<ll>; #define complete_unique(x) (x.erase(unique(x.begin(), x.end()), x.end())) #define lb lower_bound class Solution { public: int reversePairs(vector<int>& nums) { vl tarr; for (auto &e: nums){ tarr.push_back(e); tarr.push_back(2ll * e); // 直接把须要离散化的对应元素加入 } sort(tarr.begin(), tarr.end()); int n = nums.size(); BIT<int> bit(2 * n); // 注意,由于加入了两倍的元素,因此对应也要开大一点 int res = 0; for (int i = 0; i < n; ++ i){ res += i - bit.sum(lb(tarr.begin(), tarr.end(), 2ll * nums[i]) - tarr.begin() + 1); bit.add(lb(tarr.begin(), tarr.end(), nums[i]) - tarr.begin() + 1, 1); } return res; } };
总结:离散化--二分查找方法
通用性:★★★★★
- 初始化数组
farr
,将元素以及须要寻找的元素都加入其中- 二分查找便可。
二维 BIT 实际上就是套娃,一层层套便可。
其复杂度为 \(\mathcal{O(\log n \times \log m)}\) ,\(n,m\)分别为每一个维度 BIT 的个数,这里再也不赘述。
#include <bits/stdc++.h> using namespace std; // 模板代码省略 using ll = long long; int n, m, q; const int maxn = 5e3 + 50; BIT<ll> f[maxn]; // 二维BIT void add(int i, int j, ll x){ while (i <= n){ f[i].add(j, x); i += lowbit(i); } } ll sum(int i, int j){ ll res(0); while (i > 0){ res += f[i].sum(j); i -= lowbit(i); } return res; } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i = 1; i <= n; ++ i) f[i] = BIT<ll>(m); int type; while (cin >> type){ if (type == 1){ int x, y, k; cin >> x >> y >> k; add(x, y, (ll) k); }else { int a, b, c, d; cin >> a >> b >> c >> d; cout << sum(c, d) - sum(c, b - 1) - sum(a - 1, d) + sum(a - 1, b - 1) << '\n'; } } return 0; }
这是我耗时最长的一篇博客,也是我花费心血最多的一次,也但愿本身能好好掌握 BIT
附上参考连接: