树状数组简易教程

前言

树状数组由于其优越的常数,优越的代码量,更小的空间占用,所以常被使用。(实际上(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}^Nsum_{j=1}^iB[i]=Nsum_{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)

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

[egin{aligned} sum_{i=1}^{X}sum_{j=1}^{Y}A_{i,j}&=sum_{i=1}^{X}sum_{j=1}^{Y}sum_{k=1}^isum_{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 );
}

结语

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

原文地址:https://www.cnblogs.com/chy-2003/p/10152743.html