zoukankan      html  css  js  c++  java
  • 树状数组3种基本操作

    告知

    本博客是由一个蒟蒻编写,内容可能出错,若发现请告诉本蒟蒻,以便大众阅读
    转载请注明原网址: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
    ",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
    ",get(x));
    		}
    	}
    	return 0;	
    }
    

    3.区间修改,区间查询

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

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

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

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

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

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

    修改

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

    查询

    单点(x)的前缀和就是((x+1) imes 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
    ",get(r)-get(l-1));
    		}
    	}
    	return 0;
    }
    

    小结

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

    不定时更新高阶操作

  • 相关阅读:
    C#单例模式的三种写法转载
    silverlight 添加配置项
    oracle 如何实现上一条、下一条、查找不连续的值
    一个IT民工眼中的保障房不能保证公平,赞成取消保障房
    c# where 转载
    进度条 silverlight
    中国软件公司我深表认同:软硬结合
    计算经纬度两点之间的距离(c#)
    如何高效使用SQLITE .NET (C#)
    如何判断系统是否安装了flash插件
  • 原文地址:https://www.cnblogs.com/Livingston/p/14083914.html
Copyright © 2011-2022 走看看