树状数组
为什么需要树状数组?
很多场景下,需要通过O(n)的复杂度求前缀和,之后通过差分O(1)的复杂度,求指定区间的区间和。
但是,对于已经建立起来的前缀和数组,如果改变某个单点的值,修改的代价是O(n)的。
树状数组的定义
- 可以通过树状数组实现O(logn)的修改,实现O(logn)的查询
- 树状数组的大小于员数列大小相同
- 与前缀和不同的是,树状数组的 i 位置的元素存储的并不是前 i 个元素 而是从 i 开始,包括第 i 个元素的前 ti 个元素的和。
- 其中 ti 是最大的可以整除 i 的2的幂
- 例如:s[ i ] 为树状数组,d[ i ] 是原数组
-
i = 9, ti = 1 s [ 9 ] = d [ 9 ]
-
i = 6 , ti = 2 s [ 6 ] = d [ 5 ] + d [ 6 ]
-
i = 24 , ti = 8 s[ 24 ] = d [17] + d [ 18 ] + d [ 19 ] + d [ 20 ] + d [ 21 ] +...+ d [ 24 ]
- 如果转为二进制,ti 的 归路更加明显
- i = 9 (1001) ti = 1
- i = 6 (0110) ti = 2
- i = 24 (11000) ti = 8
ti 为二进制最低位的1,与其后的0组成的2进制
lowbit(i)
求解ti的函数
原数 i : 101011000
取反 : 010100111
取反+1:010101000
和原数相与:000001000
lowbit(i) = i & ((~1) + 1) = i & ( -i )
#define lb(x) (x & (-x))
树状数组的查询
要查询的数据是指定位置x的前缀和 [1, x ]的前缀和
对于区间 [ 1, 11 ] 可以使用 是 s[ 11 ] + s [ 10 ] + s [ 8 ] 表示
对于任何一个区间,都能被完整的表示出来
因为s[ i ] 可以表示前 ti 个数的和,
ans += s[ i ]
令 i = i - ti
ans += s[ i ]
。。。直到i = 1
ans += s[ 1 ]
返回 ans
#define lb(x) (x & (-x))
long long ask(int x){
long long res = 0;
for(int i = x;i >= 1;i -= lb(i))
res += s[i];
return res;
}
树状数组的性质
性质1 : 若当前结点为x,且令x + tx 为父节点 tx = lowbit(x) ,则梳妆数组将形成一个树形结构
性质2 : 结点x 记录区间 ( x - tx,x ] 的信息,其子节点记录的区间是( x - tx, x ] 的子集,且不会相互覆盖。
性质3 : 结点x 的记录的区间为结点y 记录的区间的子集,当且仅当y是结点x的祖先节点
树状数组的修改
修改的时候,改变原数组x位置的值,只需要修改 s[x] 和所有祖先节点中的值,复杂度是O(logn)的,相比前缀和,修改的复杂度小
怎么找祖先节点?
在 i 位置 加上 lowbit(i),就是祖先的位置
在i 位置加 v,每个祖先节点都要加v
void upd(int x,int v){
for(int i =x; i <= n;i += lb(i))
s[i] += v;
}
例题
求区间[ l, r ]的和, ask(r)- ask(l-1)
#include"bits/stdc++.h"
#define lb(x) (x & (-x))
using namespace std;
long long s[1000001];
int n,q;
long long ask(int x){
long long res = 0;
for(int i = x;i >= 1;i -= lb(i))
res += s[i];
return res;
}
void upd(int x,int v){
for(int i =x; i <= n;i += lb(i))
s[i] += v;
}
int main()
{
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
memset(s,0,sizeof(s));
cin>>n>>q;
for(int i = 1 ;i <= n;i++)
{
int t;
scanf("%d",&t);
upd(i,t);
}
for(int i = 0;i < q;i++)
{
int op,num1,num2;
scanf("%d %d %d",&op,&num1,&num2);
if(op ==1)
{
upd(num1,num2);
}
else
{
long long ansl = ask(num1-1);
long long ansr = ask(num2);
cout << (ansr - ansl) <<endl;
}
}
return 0;
}
用树状数组解决二维偏序问题
- 给定一个序列 a1, a2, a3, …, an,如果存在 i < j 且 ai >
aj,那么我们称之为逆序对,求给定序列中逆序对的数目。其中 1 ≤ ai, n ≤ 10^5。 - 对于任意一个ai,统计在其之前求大于 ai 的数的数量,即为以 ai 结尾的逆序对的数量
- 只要枚举a1 ~ an分别计算,再求和,即为所求的答案
例题2 二维偏序问题
给定一个序列 a1, a2, a3, …, an,如果存在 i < j 且 ai > aj,那么我们称
之为逆序对,求给定序列中逆序对的数目。其中 1 ≤ ai, n ≤ 10^5。
思路:开一个数组s[MAXN],对于a1查询 a[MAXN]到a[a1 + 1]所有的和,也就是目前比a1大的元素有多少个,查询的结果加到ans上,更新 a[a1] +1
例子
两维坐标,按照某一维i例如运行时间排序,求另一维的逆序对数
#include"bits/stdc++.h"
using namespace std;
#define lb(x) (x & (-x))
int s[1000010],n;
//int cnt[1000010];
int ask(int x)
{
int ans = 0;
for(int i = x ;i >= 1; i -= lb(i))
ans += s[i];
return ans;
}
void upd(int x,int v)
{
for(int i = x; i<=1000009;i += lb(i))
s[i] += v;
}
int main()
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
cin>>n;
memset(s,0,sizeof(s));
memset(cnt,0,sizeof(cnt));
vector<pair<int,int>> a;
for(int i= 0;i < n;i++)
{
int t,c;
scanf("%d %d",&t,&c);
a.push_back(make_pair(t+1,c+1));
}
sort(a.begin(),a.end());
for(int i = 0;i <n;i++)
{
int num = ask(1000009) - ask(a[i].second);
int score = i - num;
cnt[score]++;
upd(a[i].second,1);
}
for(int i = 0;i < n;i++)
printf("%d\n",cnt[i]);
return 0;
}
例题3 树状数组和动态规划相结合 – 最长上升子序列
状态:定义fi 表示以A为结尾的最长上升序列的长度
初始化 f1= 1
转移过程: fi = max{ fj | j < i && Aj < Ai } + 1
输出答案 max{f[i], i = 1…n}
再找最大的fj j < i && Aj < Ai 的过程中,要从1 到 i 遍历一遍才行,复杂度为O(n^2)
使用树状数组进行优化
树状数组保存的内容,从前ti个数据的和, 变为前ti个数的最大值
如果读到一个数 x ,ask(x - 1),得出结尾小于x的所有子序列的最长的长度,
tmp = ask(x - 1) + 1
upd(x , tmp)