树状数组学习小结(很久以前的了。。。)

PS:很久以前的,翻东西的时候看到,就一起放上来吧。。。。

树状数组总结

最近学了一个新的数据结构:树状数组,那今天就来写篇总结吧。

树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构,我们通常都叫它BIT

       挑程(《挑战程序设计》)上面说了,BIT是删去了线段树多余的区间从而组成的一棵树,从而“进化”而成树状数组,它相对于线段树来说空间就少了一大截,而却代码长度比线段树短多了,好写,在一定必要的时候是会要它上场的。它主要的优势是求一段的总和,修改一段,单点修改,查询最大值等等。然而它的玄妙之处是在于它能巧妙地把二进制的编码转化成了区间。现在说起来只是空穴来风,我们画个图来看看吧。

 

这样子是一棵再平凡不过的线段树了,但是我们现在可以告诉你,树上全部绿色区间的都不需要。仔细思考一下,如果要求l,r这个区间和,其实不就是r的前缀和减去l-1的前缀和吗?同时如果让这棵数来求前缀和的话,就太浪费空间了吧, 仔细看看,如果这样子删去了绿色区间无论你要求哪一段的前缀都可以轻松得到。

给他们标上号,你就会发现一个非常神奇的规律:对应的编号转化成二进制码后,末尾有n个0那么这个区间的长度也就是2^n这么长,例如1,3,5,7转化成二进制码(0001,0011,0101,0111)末尾0个0所以区间长度是2^0=1再看4的二进制码(0100)末尾两个0所以所以区间长度是2^2=4。

同时你看6的二进制码是0110,末尾有1个0那么区间长度是2所以6就包括了6-2+1到6这个区间的和。那怎么求和呢,例如求前7个的和,7的二进制码0111那么7包括的区间长度为1,所以加上7,然后怎么跳到下一个区间呢?那就不断的减去这个区间的长度就能够跳到下一个区间,7(0111),0111-1(0001)=0110就到了6这个区间,6的长度为2,所以6-2(10)=0110-0010=0100就跳到了4这个区间,然后依次类推。那我们来看看吧,这样子求和下去的话时间最多也只是log(n)假设n的二进制码每位都是1那么最多也只有logn个1所以只用运行logn次。这么来看是不是特别方便,但是如何求最后一个1的位置(也就是这个区间有多长,就是有多少个0)C++中对二进制码有个很神奇的运算x&(-x)就能够找到最后一个1的位置,在C++中它会把一个数的负数表示为他的反码+1所以再&自己就能找到最后一个1。

         刚刚讲完了求和,那如果要单点修改呢?其实修改也是差不多的,单点修改,只用修改和这个数有关的区间就可以了。最多也是不多于logn的区间个数。同上理,还是那个图如果要修改1的话呢,是不是要修改2,4,8 1又怎么得到2,4,8呢,把1转换成二进制码,0001加上它这个区间的长度不就可以了,0001+1=0010(2),0010+2(10)=0100(4),0100+4(0100)=1000(8)是不是也很玄妙,同理他最多也是logn的时间,在这里就不再证明了。

         我觉得发明树状数组这个人真的是个人才,如此玄妙的规律被发掘出来,刚开始还觉得不可思议,越看越觉得玄妙。(伪代码如下)

void add(int u,int ad)

{

         int k=u;

         while(k<=n)

         {

                   sum[k]+=ad;

                   k+=lowbit(k);

         }

         return ;

}

long long query(int u)

{

         int k=u;

         long long num=0;

         while(k>0)

         {

                   num+=sum[k];

                   k-=lowbit(k);

         }

         return num;

}

其实仔细思考一下还是很好理解的,后面江老师还讲到了几道题目。逆序对,最长上升子序列,圆和线的交点,认真想想也是很简单的。逆序对,可以从后往前一个一个数的添加,求当前整数的前缀和,也就是比他小的数提前添加进去的个数有多少个,那这几个就是逆序对(PS:有坑,有些数据要离散化去重)。最长上升子序列,就需要变换一下树的内容了,改成求最大值,和DP的方程有点相似,找到一个最大值,把最大值+1存到这个点里面,然后一直添加下去(PS:也是要去重)圆与线的交点,我认为是这三道题目里最有难度的一道,江老师讲了3种解法杭洋讲了1种解法(虽然是错的,但是他毕竟很大胆,而且善于思考),

记忆最深的解法就是把小边排个序,然后每次在树状数组中查找l到r的右节点的总数,再把这条边的右端点加入到BIT中。还有一种我还没有实现过,就是扫描线,左端点-1,右端点+1,还要容我再三思考。

圆与点与线段:

#include<iostream>

#include<cstdio>

#include<algorithm>

#define lowbit(x) x&(-x)

using namespace std;

int n,in[50005],cur=1,bit[150000];

long long ans=0;

struct EDGE{

         int l;

         int r;

}edge[150005];

void add(int a,int b)

{

         edge[cur].l=a;

         edge[cur].r=b;

         cur++;

         return;

}

bool cmp(EDGE a,EDGE b)

{

         return a.l<b.l;

}

void add(int u)

{

         while(u<=n*2)

         {

                   bit[u]+=1;

                   u+=lowbit(u);

         }

         return ;

}

long long query(int u)

{

         long long sum=0;

         while(u>0)

         {

                   sum+=bit[u];

                   u-=lowbit(u);

         }

         return sum;

}

int main()

{

         freopen("1699.in","r",stdin);

         freopen("1699.out","w",stdout);

         cin>>n;

         for(int i=1;i<=2*n;i++)

         {

                   int x1;

                   cin>>x1;

                   if(!in[x1]) in[x1]=i;

                   else add(in[x1],i);

         }

         sort(edge+1,edge+cur,cmp);

         for(int i=1;i<cur;i++)

         {

                   long long u=query(edge[i].r)-query(edge[i].l-1);

                   ans+=u;

                   add(edge[i].r);

         }

         cout<<ans;

         return 0;

}

就写到这么多吧,后面还要学会把树状数组发挥到极致,例如:差分数组,区间修改,二维树状数组等等。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值