树状数组简易教程
前言
树状数组由于其优越的常数,优越的代码量,更小的空间占用,所以常被使用。(实际上\(ZKW\)线段树常数也小,码量也小,功能更强大,但是较难理解所以哪位大佬教我一下)下面就较为详细地介绍树状数组。
然而本博客可能写得较难理解……
树状数组原理
如果我们令一个数组\(A[1]=a[1], A[2]=a[1]+a[2],A[3]=a[3],A[4]=a[1]+a[2]+a[3]+a[4]\)……换言之,如果当前下标二进制中最低位的\(1\)是第\(i\)位,我们令当前的元素存储的就是当前位置及前面\(2^i\)个元素。
你就会发现它有一些好玩的性质:它可以\(O(logn)\)修改单个元素与\(O(logn)\)查询前缀和。这个可在下面程序中配合这段话理解一下。
这里有个注意点,由于树状数组询问求得的是前缀和,所以并不能维护最大最小值。
一维树状数组
单点修改,区间查询
根据上面的定义,我们可以这样修改单个元素:
void Add( int Index, int Value ) {
for( int i = Index; i <= n; i += i & -i ) Tree[ i ] += Value;
return;
}
其中\(i\&-i\)是\(lowbit\)。若它的二进制最低位\(1\)在第\(i\)位,那么它的返回值就是\(2^i\)。
同样的我们可以这样查询前缀和:
int Query( int Index ) {
int Ans = 0;
for( int i = Index; i; i -= i & -i ) Ans += Tree[ i ];
return Ans;
}
那么区间\([x,y]\)和就是\(Query(y)-Query(x-1)\)。
到这里,你已经学会了最基础的线段树。
区间修改,单点查询
其实,如果我们希望将区间\([x,y]\)加上\(v\),我们只需在\(x\)位置加\(v\),再将\(y+1\)位置减\(v\),那么前缀和就是所求答案了。
区间修改,区间查询
如果仅用上面的操作,我们可能无法在理想的复杂度内完成区间修改运算。
我们考虑维护另一个东西,使得它的前缀和是我们当前想维护的数组。那么如果我们当前想维护的数组为\(A\),那么我们令\(B_i=A_i-A_{i-1}\)即可。
考虑修改区间。我们只要将\(B[x]\)加\(v\),\(B[y+1]\)减\(v\),那么对应的\(A[x]\)到\(A[y]\)就都加上了\(v\)。
考虑如何求值。
那么我们就只要维护\(B[i]\)和\((i-1)B[i]\)就好了。
代码:
void Add( LL *a, LL Index, LL V ) {
for( LL i = Index; i <= n + 1; i += i & -i ) a[ i ] += V;
return;
}
LL Ask( LL *a, LL Index ) {
LL Ans = 0;
for( LL i = Index; i; i -= i & -i ) Ans += a[ i ];
return Ans;
}
LL Query( LL x ) {
return x * Ask( Tree1, x ) - Ask( Tree2, x );
}
//区间修改
Add( Tree1, l, x ); Add( Tree1, r + 1, -x );
Add( Tree2, l, ( l - 1 ) * x ); Add( Tree2, r + 1, -r * x );
//区间求和
Query( r ) - Query( l - 1 )
二维树状数组
原理
跟一维的情况相同,每一维的坐标分别维护如一维时的一段长度。所以实际上二维树状数组每个元素是一个矩形的和,每个操作的复杂度也变为了两只\(log\)。
而求和时的前缀和便变成了\(Query(x_2,y_2)-Query(x_1-1,y_2)-Query(x_2,y_1-1)+Query(x_1-1,y_1-1)\)。
单点修改,区间查询
不多解释。
void Add( LL x, LL y, LL c ) {
for( LL i = x; i <= n; i += i & -i )
for( LL j = y; j <= m; j += j & -j )
A[ i ][ j ] += c;
return;
}
LL Query( LL x, LL y ) {
LL Ans = 0;
for( LL i = x; i; i -= i & -i )
for( LL j = y; j; j -= j & -j )
Ans += A[ i ][ j ];
return Ans;
}
LL Ans( LL a, LL b, LL c, LL d ) {
--a; --b;
return Query( c, d ) - Query( a, d ) - Query( c, b ) + Query( a, b );
}
区间修改,区间查询
思路如同上文的一维树状数组。我们令\(B_{i,j}=A_{i,j}-A_{i-1,j}-A_{i,j-1}+A_{i-1,j-1}\)。
修改区间就是\(B_{x_1,y_1}+=v,B_{x_2+1,y_1}-=v,B_{x_1,y_2+1}-=v,B_{x_2+1,y_2+1}+=v\)。
然后区间询问我们就强推一波式子:
于是我们发现只要维护\(4\)个二维树状数组,分别是\(B_{i,j},iB_{i,j},jB_{i,j},ijB_{i,j}\)。(雾)
附上程序:
void add( ll x, ll y, ll v ) {
for( ll i = x; i <= n; i += i & -i )
for( ll j = y; j <= m; j += j & -j ) {
a[ i ][ j ][ 0 ] += v;
a[ i ][ j ][ 1 ] += y * v;
a[ i ][ j ][ 2 ] += x * v;
a[ i ][ j ][ 3 ] += x * y * v;
}
return;
}
ll ask( ll x, ll y ) {
ll ans = 0;
for( ll i = x; i; i -= i & -i )
for( ll j = y; j; j -= j & -j )
ans += ( x + 1 ) * ( y + 1 ) * a[ i ][ j ][ 0 ]
- ( x + 1 ) * a[ i ][ j ][ 1 ]
- ( y + 1 ) * a[ i ][ j ][ 2 ]
+ a[ i ][ j ][ 3 ];
return ans;
}
ll query( ll a, ll b, ll c, ll d ) {
return ask( c, d ) - ask( a - 1, d ) - ask( c, b - 1 ) + ask( a - 1, b - 1 );
}
结语
本垃圾极简教程就到这里了,大家虐题愉快呀。