Loading

浅谈树状数组

告知

本博客是由一个蒟蒻编写,内容可能出错,若发现请告诉本蒟蒻,以便大众阅读
转载请注明原网址:https://www.cnblogs.com/H-K-H/p/14083914.html

树状数组和线段树

众所周知, 线段树和树状数组是兄弟来的

它们之间的关系

树状数组可以解的,线段树能解
树状数组不可以解的,线段树还是可以解

既然这样,那我学会线段树不就搞定了吗,干嘛还学树状数组呀

那么,树状数组优在何处呢?

其实呢,就是码量少,思维清晰,常数小
对比一下
单点修改区间查询
线段树100行起步
树状数组呢,50行左右吧
区间修改区间查询
线段树估计要飙到150了吧
树状数组依旧50行
没有对比就没有伤害呀
这时,有些线段树忠实粉或许会思考人生:你看我还有机会吗?
机会是有的,那就是,打树状数组吧(当然有些题还是要打线段树的啦)

树状数组简介

树状数组图解

此章节内容部分引用自bestsort的小站
众所周知,一棵满二叉树长这样:
在这里插入图片描述
挪一下位置后,变成了这样:
在这里插入图片描述
上面这个就是树状数组的画法
准确来说,这是求和数组的画法
把原数组\(a\)也加进来,成了这样(\(c\)是求和数组)
在这里插入图片描述
\(c[i]\)表示子树叶子节点的权值
如上图,有
\(c[1]=a[1]\\ c[2]=a[1]+a[2]\\ c[3]=a[3]\\ c[4]=a[1]+a[2]+a[3]+a[4]\\ c[5]=a[5]\\ c[6]=a[5]+a[6]\\ c[7]=a[7]\\ c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]\)
转换成二进制再来看一眼
\(c[1]=c[(0001)_2]=a[1]\\ c[2]=c[(0010)_2]=a[1]+a[2]\\ c[3]=c[(0011)_2]=a[3]\\ c[4]=c[(0100)_2]=a[1]+a[2]+a[3]+a[4]\\ c[5]=c[(0101)_2]=a[5]\\ c[6]=c[(0110)_2]=a[5]+a[6]\\ c[7]=c[(0111)_2]=a[7]\\ c[8]=c[(1000)_2]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]\)
对照式子可以发现,对于一个\(i\)
\(c[i]=a[i-2^k+1]+a[i-2^k+2]+a[i-2^k+3]……+a[i]\)\(k\)为二进制下\(i\)最低位的1后面的0的个数,例如8对应的\(k\)就等于3,因为\(8_{10}=(1000)_2\),最低位的1后面有3个0)
这时候,问题就来了,\(2^k\)怎么求???

引入\(lowbit\)

\(lowbit\)函数就是用来求\(2^k\)是多少的
具体操作是

int lowbit(int x) {return x&(-x);}

解释
“&”这个符号在C++中指的是按位与运算,具体是说,若在二进制下相同的位置两数都为1,那么&出的答案这一位也为1,否则为0
例如\(12\&6\)
\(12_{10}=(1100)_2\)

\(6_{10}=(0110)_2\)(空位用0补齐)

\(ans=(0100)_2=4_{10}\)
在上面这个数据中,12和6只有第三个位置上才都是1,那么答案也就只有这个位置上是1
( 不过学树状数组的人应该都不会不知道位运算吧)
那么\(x\&(-x)\)是什么意思呢
首先说明\(-x\)在二进制下和\(x\)的关系
在二进制下,\(-x\)就是\(x\)取反后再加1
例如,\(10_{10}=(01010)_2\),那么\(-10_{10}=(10101)_2+1_2=(10110)_2\)(第一位是符号位)
进行按位与运算后,答案就是\((00010)_2=2^1=2_{10}\)(第一位是符号位)
眼睛扫一扫,发现答案就是\(2\)
神奇吧
具体证明呢
我们知道一个数取反后与原来的每个位置都是相反的,那么原本1的位置就是0,原本0的位置就是1,那么加一后会一直进位到第一个0,也就是在原本数上的第一个1,这时候按位与一下就只有第一个1及以前的是一样的,也就可以得到正确结果

基本应用

1.单点修改,区间查询

修改

若要更新当前节点的\(a[i]\)
那么是不是可以直接更新\(a[i]\)的上级,\(a[i]\)上级的上级,以此类推
\(lowbit\)到上级所在下标

void update(int now,int x)
{
	int i;
	for (i=now;i<=n;i+=lowbit(i))
		c[i]+=x;
}

查询

对于区间查询,我们采取前缀和的求法
对于一个区间\([l,r]\),我们求出\(r\)的前缀和,减去\(l-1\)的前缀和即为答案
查询的具体过程呢,也很简单
就是从要查的节点以此往下,搜索下级
依旧是用\(lowbit\)

int get(int x)
{
	int i,ans;
	ans=0;
	for (i=x;i>=1;i-=lowbit(i))
		ans+=c[i];
	return ans;
}

题目

Loj#130 树状数组 1 :单点修改,区间查询

Code

#include<cstdio>
#include<iostream>
using namespace std;
long long n,m,i,x,y,ch,c[1000005];
long long lowbit(long long x)
{
	return x&(-x);
}
void update(long long now,long long x)
{
	long long i;
	for (i=now;i<=n;i+=lowbit(i))
		c[i]+=x;
}
long long get(long long x)
{
	long long i,ans;
	ans=0;
	for (i=x;i>=1;i-=lowbit(i))
		ans+=c[i];
	return ans;
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for (i=1;i<=n;i++)
	{
		scanf("%lld",&x);
		update(i,x);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%lld%lld%lld",&ch,&x,&y);
		if (ch==2) printf("%lld\n",get(y)-get(x-1));
		else update(x,y);
	}
	return 0;
} 

2.区间修改,单点查询

修改

引入差分的思想,记录数组里每个元素与前一个元素的差,那么\(a_i=\sum_{j=1}^i d_j\),如果修改区间\([l,r]\),令其加上\(x\),那么\(l\)\(l-1\)的差增加了\(x\)\(r\)\(r+1\)的差减小了\(x\),根据差分,就可以给\(d_{l}\)加上\(x\),给\(d_{r+1}\)减去\(x\)

查询

直接根据\(a_i=\sum_{j=1}^i d_j\),查前缀和就好

题目

Loj#131 树状数组2:区间修改,单点查询

Code

#include<cstdio>
using namespace std;
int  n,m,i,l,r,x,bj;
long long a[1000005],c[1000005];
int lowbit(int x)
{
	return x&(-x);
}
void update(int now,int x)
{
	int i;
	for (i=now;i<=n;i+=lowbit(i))
		c[i]+=x;
}
long long get(int x)
{
	int i;
	long long ans;
	ans=0;
	for (i=x;i;i-=lowbit(i))
		ans+=c[i];
	return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		update(i,a[i]-a[i-1]);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%d",&bj);
		if (bj==1)
		{
			scanf("%d%d%d",&l,&r,&x);
			update(l,x);
			update(r+1,-x);
		}
		else
		{
			scanf("%d",&x);
			printf("%lld\n",get(x));
		}
	}
	return 0;	
}

3.区间修改,区间查询

这个也是线段树最麻烦的地方,通常100行起步,但树状数组就不用了,实测50行不到,而且我不压行

先看一下如果按照问题2的方法来求区间前缀和,要怎么求

位置\(x\)的前缀和=\(\sum_{i=1}^x\sum_{j=1}^id_j\),发现在这个式子里,\(d_1\)被计算了\(x\)此,\(d_2\)被计算了\(x-1\)次……,\(d_x\)被计算了1次。那么这个式子就可以转化为

\(\sum_{i=1}^xd_i\times(x-i+1)=(x+1)\sum_{i=1}^xd_i-\sum_{i=1}^xd_i\times i\)

其中\(x+1\)是给出的,那么我们记录\(d_i\)\(d_i\times i\)就可以了

维护两个数组\(sum1\)\(sum2\),分别记录\(d_i\)\(d_i\times i\)

修改

\(sum1\)同问题2的\(d\)\(sum2\)也类似,\(l\)加上\(l\times x\)\(r+1\)减去\((r+1)x\)

查询

单点\(x\)的前缀和就是\((x+1)\times sum1\)\(x\)的前缀和-\(sum2\)\(x\)的前缀和,区间\([l,r]\)的值就是\(r\)的前缀和-\(l-1\)的前缀和

题目

Loj#132 树状数组3:区间修改,区间查询

Code

#include<cstdio>
using namespace std;
long long n,m,i,l,r,x,bj,a[1000005],c1[1000005],c2[1000005];
long long lowbit(long long x)
{
	return x&(-x);
}
void update(long long k,long long x)
{
	long long i;
	for (i=k;i<=n;i+=lowbit(i))
	{
		c1[i]+=x;
		c2[i]+=x*k;
	}
}
long long get(long long x)
{
	long long i,ans;
	ans=0;
	for (i=x;i;i-=lowbit(i))
		ans+=((x+1)*c1[i])-c2[i];
	return ans;
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for (i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		update(i,a[i]-a[i-1]);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%lld",&bj);
		if (bj==1)
		{
			scanf("%lld%lld%lld",&l,&r,&x);
			update(l,x);
			update(r+1,-x);
		}
		else
		{
			scanf("%lld%lld",&l,&r);
			printf("%lld\n",get(r)-get(l-1));
		}
	}
	return 0;
}

4.二维偏序

这里不妨以二维偏序的典型:逆序对来讲解。

首先先对第一关键字进行排序(逆序对中就是 \(i\)),随后对于 \(a_i\),要找到 \(j<i,a_j>a_i\)。那我们不妨建立一个以 \(a_i\) 为下标的树状数组,遍历到 \(i\) 时,查询 \(a_i+1\) 之后的位置的总和,随后更新 \(a_i\)

题目

Luogu P1908 逆序对

Code

#include<cstdio>
#include<iostream>
#include<algorithm>
#define N 500005
#define ll long long
using namespace std;
int n,a[N],b[N];
ll ans,tr[N];
int lowbit(int x) {return x&(-x);}
void modify(int x) {for (;x;x-=lowbit(x)) tr[x]++;}
ll query(int x)
{
    ll res=0;
    for (;x<=n;x+=lowbit(x)) res+=tr[x];
    return res;
}
int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;++i)
        scanf("%lld",&a[i]),b[i]=a[i];
    sort(b+1,b+n+1);
    int len=unique(b+1,b+n+1)-b-1;
    for (int i=1;i<=n;++i)
        a[i]=lower_bound(b+1,b+len+1,a[i])-b;
    for (int i=1;i<=n;++i) 
        ans+=query(a[i]+1),modify(a[i]);
    printf("%lld\n",ans);
    return 0;
}

小结

线段树与树状数组有很多相似的地方,但是树状数组很明显的优势就是短和快,但是线段树可以处理很多种情况,而这里面有些是树状数组做不到的,所以说不论是线段树还是树状数组,我们都应该学习一下,然后选择更好的去解决题目。

不定时更新高阶操作。

posted @ 2020-12-04 08:39  Thunder_S  阅读(481)  评论(0编辑  收藏  举报