文章目录
算法概述
O ( log n ) O(\log n) O(logn) 的单点修改,区间询问(前提是操作可逆)
弱化版的线段树,但代码极其简单,时间常数极低,空间要求极低,而且更加灵活。
写法
1.定义
这个东西比较特殊,用图感觉反而会乱(主要是根节点不是 1 1 1),我就尽量用纯数学的讲吧。
定义 t [ i ] = a [ i ] + a [ i − 1 ] + ⋯ + a [ i − l o w b i t ( i ) + 1 ] t[i]=a[i]+a[i-1]+\dots+a[i-lowbit(i)+1] t[i]=a[i]+a[i−1]+⋯+a[i−lowbit(i)+1] ,反正就是前 l o w b i t ( i ) lowbit(i) lowbit(i) 个数的和。
其中 l o w b i t ( i ) lowbit(i) lowbit(i) 指的是 i i i 在二进制表示下最低位的 1 1 1 所代表的数量级,比如 10 = ( 1010 ) 2 10=(1010)_2 10=(1010)2 ,那么 l o w b i t ( 10 ) = 2 1 = 2 lowbit(10)=2^1=2 lowbit(10)=21=2 。
它听起来很复杂,但其实求法异常简单: l o w b i t ( i ) = i & − i lowbit(i)=i\&-i lowbit(i)=i&−i 。以下简称 l b ( i ) lb(i) lb(i) 。
2.单点修改
发现其实树状数组是有递归性质的。
看这张图
是不是好像有什么规律又找不出来
直观感受一下,一个数字上叠了几层,就是它的 l b lb lb 。
比如 A [ 2 ] A[2] A[2] 上有 2 2 2 层,代表它的 l b lb lb 是 2 2 2 。
一个数加上它的 l b lb lb 就是它右边的第一个层数比它高的数。(即第一个 l b lb lb 大于它的 l b lb lb 的数)
而一个数加上它的 l b lb lb 就是它左边的第一个层数比它高的数。
怎么理解呢,从图形上讲就是说它有递归性质,从数学上讲减 l b lb lb 就是消除最后一个 1 1 1 ,加法就是消除的同时再进位,详细的话应该是可证的。
反正规律记牢就好了
考虑什么东西会影响这个即将修改的点。
发现就是它的父节点,即它右边的第一个层数比它高的数,不断加 l b lb lb 即可达到目的。
void add(int x,int k) {
for(int i=x;i<=n;i+=lb(i))
t[i]+=k;
}
3.区间询问
因为操作可逆,所以其实询问的是单点前缀和。
发现肯定要向前循环,而一个子树的和就是该子树最右边的树状数组的值,即它左边的第一个层数比它高的数,那么就可以直接跳过该子树,又是它左边的第一个层数比它高的数,所以不断减 l b lb lb 并统计和即可达到目的。
int sum(int x) {
int s=0;
for(int i=x;i;i-=lb(i))
s+=t[i];
return s;
}
int query(int x,int y) {
return sum(y)-sum(x-1);
}
4.建树
因为一般的询问次数不会小于数组的容量,所以每个数都修改一遍即可,复杂度 O ( n log n ) O(n\log n) O(nlogn) ,完全可以接受。
但也不排除某些特殊情况,所以也有一种 O ( n ) O(n) O(n) 建树的方法。
树状数组其实是一个二叉树(不过只存了最高层的值),而每个节点的其中一个儿子就是它自己,另一个儿子从父节点的角度不好找,但从儿子的角度就很好找了。(参照单点修改)
那么一定是 O ( n ) O(n) O(n) 的。
void build() {
for(int i=1;i<=n;++i) {
t[i]+=a[i];
if(i+lb(i)<=n)
t[i+lb(i)]+=t[i];
}
}
5.习题
太多了,先随便放
2
2
2 个模板。
第 2 2 2 题的话需要用树状数组存差分数组。
权值树状数组
算法概述:
树状数组这个东西嘛,就是一个超级弱化版的线段树,一般是用来维护动态前缀和。
那么考虑这样一个问题:求 1 i 1~i 1 i 中比 a i a_i ai 大的数的数量。
第一眼好像不能用树状数组维护,但事实上是可以的,因为有这样一个东西: 权值树状数组 。
权值树状数组的核心思路就是把权值作为下标,来维护一些东西。
经典的应用就是求 逆序对 。
例题:P1908 逆序对
考虑构造一个数组 d i d_i di ,表示 i i i 这个值,在目前出现的次数。
对 d d d 构造树状数组 t t t ,以维护前缀和,那么对于一个 a i a_i ai ,所有比 a i a_i ai 小或等于的值的出现次数就是 t t t 的 s u m sum sum ,所以以 i i i 位置为末尾的逆序对数量就是前面所有比 a i a_i ai 大的值的出现次数,即 i − s u m i-sum i−sum 。
那么就可以 O ( n log n ) O(n\log n) O(nlogn) 解决。
啊本题需要离散化,别忘了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read() {
int s=0,m=0;char ch=getchar();
while(!isdigit(ch)) {if(ch=='-')m=1;ch=getchar();}
while( isdigit(ch)) s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
return m?-s:s;
}
int n,a[500005],b[500005],q,ans;
struct BIT {
int t[500005];
int lb(int x) {return x&-x;}
int sum(int x) {//单点前缀和询问
int s=0;
for(int i=x;i;i-=lb(i)) s+=t[i];
return s;
}
void add(int x,int k) {//单点修改
for(int i=x;i<=500000;i+=lb(i))
t[i]+=k;
}
}t;
signed main() {
cin>>n;
for(int i=1;i<=n;i++)
b[i]=a[i]=read();
sort(b+1,b+n+1);
q=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+q+1,a[i])-b;
for(int i=1;i<=n;i++) {
t.add(a[i],1);
ans+=i-t.sum(a[i]);
}
cout<<ans;
return 0;
}