树状数组 算法详解

这几天扫描知识点,扫描到了树状数组就解决了。
首先什么是树状数组?它其实就是支持单点修改和区间查询的数据结构,我会一步一步讲解这个树状数组到底是个什么东西,所以请跟上我的步骤,拿出纸笔跟我一起计算。
首先我们要知道树状数组是怎么实现的,原理就是前缀和,如果我们可以快速地计算出前缀和,然后一减就得出了区间和了,可问题是如果用前缀和的话修改是要O(n)的,太大了,好了不扯了回归正题。
然后我们要知道它是怎么构成的。
树状数组 算法详解_第1张图片
这是树状数组的常规定义图,这样看是不是看不出什么,没错我也看不出来,可我们换个角度看一下,我们把所有十进制数全换成二进制。
树状数组 算法详解_第2张图片
这样应该可以看出他们之间有关,可要说具体的话就难了点,我在这里就直接点破了吧。我们先定义一个数lowbit(i)这个函数是什么意思呢?它返回的值就是i的二进制下从右到左的第一个1保留其他全部变为0所对应的数值,举个栗子:14的二进制1110,我们把从右到左的第一个1保留,其他全变为0就是0010,值也就是2,我们发现如果i+lowbit(i)得到的数就是它父亲的位置,奇妙吧。有什么用呢?这样我们就可以从父亲到儿子,也可以从儿子到兄弟(为什么可以找到建议自己推一下)。然后我们讲一下lowbit函数,我们可以直接根据主观上的理解去求,也可以用一个比较好的方法,主观的方法就自己算,我们主要讲快速地方法,我们根据补码的定义来求,如果有看过补码的话就比较好理解没看过的话就直接背公式吧,不过个人建议理解。我们算负数补码就是从右往左找到第一个1(有没有很眼熟),把1左边的全部取反(不包括这个1)。所以我们可以直接i&(-i),因为左边全部取反,我们可以保证&后左边一定全部是0。

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

构树(定义上的):对于树状数组从十进制角度看比较难,我们从二进制角度看,我们设a为题目给的数列,c是我们的树状数组。对于c中的每一个我们可以看成一个树的节点,而对应二进制的拆分以及a中对应的数(例如c[16(10000)]就可以拆成c[8(1000)]、c[12(1100)]、c[14(1110)]、c[15(1111)]以及a[16(10000)])拆分是怎么分的?其实很简单对于一个数,我们从右往左找到第一个1,把这个1变成0,然后这个位置右边的0不断地依次变成1,这样就可以构成树了,自己动手画一下。这样就巧妙地构成了树,而且空间是O(n)。
查询:我们可以发现要查询[1,i]([i,j]可以看成[1,j]-[1,i-1])有两种情况,一,i刚好是2的次方倍,直接输出;二不是2的次方倍,那该怎么办呢?我们根据上面的图发现其实就是i以及比i小的兄弟的和,那么我们直接根据lowbit函数找到兄弟。

int sum(int i)//计算[1,i]区间内的和 
{
	int ans=0;
	while(i>0)
	{
		ans+=c[i];
		i-=lowbit(i);
	}
	return ans;
}

修改:我们如果改动一个数的值,根据图我们可以发现影响到的一定只有它的祖先,所以我们只要修改它本身和祖先就好了。

void change(int i,int num)//建树和修改并用 
{
	while(i<=n)
	{
		c[i]+=num;
		i+=lowbit(i);
	}
	return ;
}

建树(代码上):我们可以初始化为0,然后每一次输入都当成修改处理不就行了。
下面上几题模板:模板 1
代码:

#include
#include
using namespace std;
int c[500100],n;
int lowbit(int i)
{
	return i&(-i);
}
void change(int i,int num)//修改 
{
	while(i<=n)
	{
		c[i]+=num;
		i+=lowbit(i);
	}
	return ;
}
int sum(int i)//查询 
{
	int ans=0;
	while(i>0)
	{
		ans+=c[i];
		i-=lowbit(i);
	}
	return ans;
}
int main()
{
	int m;
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)//输入数据 
	{
		int x;
		scanf("%d",&x);
		change(i,x);//直接当成修改处理 
	}
	for(int i=1;i<=m;i++)
	{
		int b,x,k;
		scanf("%d %d %d",&b,&x,&k);//输入查询或修改 
		if(b==1)
			change(x,k);//修改 
		else 
		{
			int ans;
			ans=sum(k)-sum(x-1);//查询 
			printf("%d \n",ans);
		}
	}
	return 0;
 } 

模板 2
这一题有点意思,我们之前说树状数组支持区间查询,单点修改,那区间修改和单点查询呢?其实也不会难,我们可以用树状数组维护差分,我们新增一个数组a表示修改的差分然后直接在(修改范围是[i,j])i上加k(k是修改值),j-1上加-k这样就修改好了,查询就是差分的前缀和加上自己本身的值。

#include
#include
using namespace std;
int b[500100],n,c[500100];
int lowbit(int i)//树状数组尝龟操作 
{
   return i&(-i);
}
void change(int i,int num)
{
   while(i<=n)
   {
   	b[i]+=num;
   	i+=lowbit(i);
   }
   return ;
}
int sum(int i)
{
   int ans=0;
   while(i>0)
   {
   	ans+=b[i];
   	i-=lowbit(i);
   }
   return ans;
}
int main()
{
   int m;
   scanf("%d %d",&n,&m);
   for(int i=1;i<=n;i++)
   {
   	scanf("%d",&c[i]);//输入数据 
   }
   for(int i=1;i<=m;i++)
   {
   	int d,x,y,k;
   	scanf("%d",&d);
   	if(d==1)
   	{
   		scanf("%d %d %d",&x,&y,&k);//修改区间 
   		change(x,k);
   		change(y+1,-k);
   	}
   	else 
   	{
   		scanf("%d",&x);
   		int ans;
   		ans=sum(x);//单点查询 
   		printf("%d \n",ans+c[x]);
   	}
   }
   return 0;
}

在来一题思考题:思考题
线段树?博主是不是疯了,这不是树状数组吗?对博主疯了,用树状数组写线段树,结果代码更少更清晰,常数还更小(应该吧,反正差不到哪去),空间复杂度还是O(2n),是线段树的一半。(详细的点这)
希望这一篇讲解对大家有帮助,如果有没看懂的欢迎留言。

你可能感兴趣的:(树状数组)