树状数组简易教程

前言

树状数组由于其优越的常数,优越的代码量,更小的空间占用,所以常被使用。(实际上\(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\)

考虑如何求值。

\[Ans=\sum_{i=1}^NA[i]=\sum_{i=1}^N\sum_{j=1}^iB[i]=N\sum_{i=1}^NB[i]-\sum_{i=1}^N(i-1)B[i] \]

那么我们就只要维护\(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\)

然后区间询问我们就强推一波式子:

\[\begin{aligned} \sum_{i=1}^{X}\sum_{j=1}^{Y}A_{i,j}&=\sum_{i=1}^{X}\sum_{j=1}^{Y}\sum_{k=1}^i\sum_{l=1}^j B_{k,l}\\&=\sum_{i=1}^{X}\sum_{j=1}^{Y}B_{i,j}(X-i+1)(Y-j+1)\\&=(X+1)(Y+1)\sum_{i=1}^{X}\sum_{j=1}^{Y}B_{i,j}-(X+1)\sum_{i=1}^{X}\sum_{j=1}^{Y}jB_{i,j}-(Y+1)\sum_{i=1}^{X}\sum_{j=1}^{Y}iB_{i,j}+\sum_{i=1}^{X}\sum_{j=1}^{Y}ijB_{i,j} \end{aligned} \]

于是我们发现只要维护\(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 );
}

结语

垃圾极简教程就到这里了,大家虐题愉快呀。

posted @ 2018-12-20 22:00  chy_2003  阅读(164)  评论(0编辑  收藏  举报