树状数组专题总结

树状数组有许多经典的区间操作值得我们去学习,相当于一个模板,理解起来也是比较容易,树状数组功能很强大,同时代码也比较简单,而线段树代码量很大,容易出错,比较难去深入的理解,所以我先学习了树状数组才准备开线段树,并非线段树不重要,线段树的应用更加广泛,所以这种数据结构的学习肯定也要深入的,那么我先总结一下树状数组吧。

lowbit(x)求的是x的二进制位中,最后有k个0,返回的值是2^k

1.区间求和+单点修改(对单点加或者减)

区间求和:最简单的树状数组操作,利用树状数组的区域管理的操作,能够迅速地求出的[1,x]的前缀和,所以我们求[l,r]的和的时候,只需要求出[1,r],[1,l-1],所以[l,r]的和=[1,r]-[1,l-1].

单点修改:从该点x到最后一位的点(n)所管辖的区间所有都要修改,所以操作为x+=lowbit(x)就能得到所有的x被管辖的位置。

int getsum(int x)
{
    int sum=0;
    while(x)
    {
        sum+=c[i];
        x-=lowbit(x);
    }
    return sum;
}
void update(int x,int val)
{
    while(x<=n)
    {
        c[x]+=val;
        x+=lowbit(x);
    }
}

2.区间修改+单点求值

这个首先介绍差分思想,可能这个比较简单,所以大部分博客都是略过的,这里稍微讲一下。

原数组a[1,5]: 1 3 2 4 6

差分数组c[1,5]:1 2 -1 2 2

差分数组的就是c[i]=a[i]-a[i-1]   (1<=i)

那么我们可以得到a[2]=c[1]+c[2]   a[3]=c[1]+c[2]+c[3]   a[i]=c[1]+c[2]....+c[n]

那么树状数组的求和就有效果了,只是这里的c[i]数组的意义不同了,这里应该是差分的原数组更新的树状数组,所以a[i]=getsum(i).

区间修改:把[l,r]上的数加上一个val,那么根据差分数组的定义先看a[l],因为a[l]+=val,所以a[l]-a[l-1]=val,所以在l点应该更新update(l.val),那么对于a[l+1]...a[r-1]的数呢?可以知道它们都+val所以它们的差值还是原来的,所以不需要更新。而a[r]和a[r+1],a[r]+=val,a[r+1]-a[r]=-val,因此update(r+1,-val);

单点查询:a[i]=getsum(i)

这是一道简单题

Time Limit: 100 MS     Memory Limit: 64 MB

出题人Acnext又要背锅了,他必须出一道大家都会做的简单题,如果有人做不出来他就得背锅,聪明的你能解决这道简单题让他避免背锅吗:

给出一个长n

的数列,以及 n

个操作,操作涉及区间加法,单点查询

Input

第一行输入一个数字n,(1n50000)

第二行输入n

个正整数,第 i个数字为 ai,(1ai109)

,空格隔开

接下来输入n

行询问,每行输入四个数字 opt,l,r,c

opt=0

,表示将 [l,r]的数字都加 c

opt=1

,表示询问 ar的值(忽略 l,c

)

Output

对于每次询问,输出一行代表答案

保证所有数据在int范围内

Sample input and output

Sample Input Sample Output
4
1 2 2 3
0 1 3 1
1 0 1 0
0 1 2 2
1 0 2 0
2
5
#include<iostream>
#include<string>
#include<vector>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<map>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int n;
ll c[100009];
ll lowbit(ll x)
{
    return x&(-x);
}
ll getsum(ll x)
{
    ll sum=0;
    while(x)
    {
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
ll update(ll x,ll val)
{
    while(x<=n)
    {
        c[x]+=val;
        x+=lowbit(x);
    }
}
int main()
{
    ll op,l,r,val,a;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a);
        update(i,a);
        update(i+1,(-1)*a);
    }
    for(int i=1;i<=n;i++)
    {
        scanf("%lld%lld%lld%lld",&op,&l,&r,&val);
        if(op==0)
        {
            update(l,val);
            update(r+1,(-1)*val);
        }
        else
        {
            printf("%lld\n",getsum(r));
        }
    }
}

对于原始值,我们可以假设区间[i,i]来更新

3区间修改+区间求和

这里需要推导一下,基于2的差分数组

a[1]+a[2]+a[3]+....+a[n]

=(c[1])+(c[1]+c[2])+....(c[1]+c[2]+....c[n])

=n*c[1]+(n-1)*c[2]+.....2*c[n-1]+c[n]

=n*(c[1]+c[2]+...+c[n])-(0*c[1]+1*c[2]+....(n-1)*c[n])

推到这里为了方便写等式,我们再+(c[1]+c[2]+...+c[n])-(c[1]+c[2]+...+c[n])

a[1]+a[2]+a[3]+....+a[n]

=(n+1)*(c[1]+c[2]+...+c[n])-(1*c[1]+2*c[2]+....n*c[n])

那么我们就可以看到前面一部分就是我们的差分和,而后面的一部分要另起一个差分数组

所以令原差分数组为c1[],另一个为c2[]

那么区间的时候原差分数组就按原来那样操作即可,而第二个差分数组应该是c2[i]=i*c[i].

也同样是修改l和r+1,这两个地方,只不过它的值变为i*val,所以求和[1,r]的求和=(r+1)getsum(r)-getsum2(r)

我下面的代码是直接写成一个函数,所以getsum2函数其实是合并到一起的,这个去解决一个线段数区间求和问题

一棵普通的线段树

出题人明天就要半期考试了,课程是《火葬场与波》.出题人倒在血泊中,一双有力的手摇晃着出题人的肩膀:“同志,醒醒,你还有题没出完呢”.以下是他的遗言:

给你一个数组 A[1..n]

,初始值全为 0.你需要写一棵裸的区间修改、区间查询的线段树,以支持两个操作.第一个操作是对区间 [L,R] 内的数每个数加上 v.第二个操作是给出区间 [L,R]

内所有数的和.

Input

第一行包含两个整数 n(1n106)

m(1m106)

,分别是数组的大小和操作的个数.

接下来 m

行,每行四个用空格分隔的整数 o l r v (1lrn,|v|103).如果 o=0,则表示对区间 [l,r] 内每个数都加上 v.否则,请给出区间 [l,r] 内所有数的和,此时 v0

.

Output

对于每个 o0

的操作,输出包含一个整数的一行,表示对应区间内所有数的和.

Sample input and output

Sample Input Sample Output
5 4
0 2 4 5
1 3 5 0
0 1 3 -2
1 1 5 0
10
9


#include<iostream>
#include<string>
#include<vector>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<map>
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int n;
ll c1[1000009];
ll c2[1000009];
ll lowbit(ll x)
{
    return x&(-x);
}
ll getsum(ll x)
{
    ll sum1=0,sum2=0,i=x;
    while(x)
    {
        sum1+=c1[x];
        sum2+=c2[x];
        x-=lowbit(x);
    }
    return (i+1)*sum1-sum2;
}
void update(ll x,ll val)
{
    ll i=x;
    while(x<=n)
    {
        c1[x]+=val;
        c2[x]+=i*val;
        x+=lowbit(x);
    }
}
int main()
{
    int m,a;
    scanf("%d",&n);
    scanf("%d",&m);
    while(m--)
    {
        ll op,l,r,val;
        scanf("%lld",&op);
        if(op==0)
        {
            scanf("%lld%lld%lld",&l,&r,&val);
            update(l,val);
            update(r+1,(-1)*val);
        }
        else
        {
            scanf("%lld%lld%lld",&l,&r,&val);
            printf("%lld\n",getsum(r)-getsum(l-1));
        }
    }
}

记住求[1,i]的和不要忘记*(i+1)


4 区间最值+单点更新(直接修改单点)

这个应用其实和上面的求和的都不太一样,c[i]现在表示的是所管辖的区间的最大值了,a[i]代表原数组,所以c[i]要修改的时候,要对它管辖的区间内所有的值都去比较依次,就能得到所管辖的区间里面的最大值,每一次修改的时候都要把原数组当作最大值然后再去遍历管辖的区间。

void init()
{
    int i,j;
    for(i=1; i<=n; i++)
    {
        scanf("%d",&a[i]);//a[]为原数组,c[]为最区间最值数组
        c[i]=a[i];//c[i]表示在i这个位置所管辖区间的最值
        //如i=8,所管辖的区间就是7,6,4,所以得到规律
        for(j=1; j<lowbit(i); j<<=1)
        {
            c[i]=max(c[i],c[i-j]);
        }
    }
}

上面是输入函数,接下来是单点更新函数

void update(int x,int val)
{
    int i,j;
    a[x]=val;
    for(i=x; i<=n; i+=lowbit(i)) //更新了一个位置,要把后面的所有区间都更新一遍
    {
        c[i]=a[i];//和上面输入一样的操作
        //更新每一个c[]时,要对依次遍历所管辖区间前面的最大值并完成更新
        for(j=1; j<lowbit(i); j<<=1)
        {
            c[i]=max(c[i],c[i-j]);
        }
    }
}
而区间求最值的函数要慢慢理解,c[r]不一定是[l,r]的最大值,因为可能包含了[1,l-1]的最大值的情况,所以一开始的最大值为a[r],然后开始遍历,然后先r-=1,如果r-lowbit(r)>=l 证明还处于管辖的区间内,还可以去更新,如果已经<l 了,那么证明下一个管辖的区间已经在l 的左边了,那么就退出循环,并且和当前的a[r]进行比较,注意这里是a[r]。例如说[4,9]的最值,首先r=8,管辖区间r-lowbit(r)==0<l=4 所以要和原数值a[r]去比较,而不能直接和c[r]去比较。
int query(int l,int r)
{
    int ans=a[r];//如果l==r,那么直接就是a[r]
    while(l!=r)
    {
        for(r-=1; r-lowbit(r)>=l; r-=lowbit(r)) //r-lowbit(r)<l时就是此时的区间最大值不一定在[l,r]这个区间内,所以跳过,然后重复r-=1
        {
            ans=max(ans,c[r]);
        }
        ans=max(ans,a[r]);//如果不能延伸区间,即r-lowbit(r)>=l
        //那么就是单独跟a[r]做比较,而不是和c[r]这个区间最大值做比较
    }
    return ans;
}

以上就是hdu1754的代码,而poj3264最值相减也是一样可以去求的,增加一个m[]数组去表示最小值,然后和c[]的操作相同即可。


5 求第k小的值

对于同样的问题,求第k大的值,也就是n+1-k小,同样去处理就好了。其实是去利用一种二分逼近的方法求出,和之前的getsum(x)函数其实有点类似,不过这里直接从二进制最大位开始遍历,其实考虑一下,getsum(x)其实最大的管辖区间也是2^k,

int Find_kth(int k)
{
    int ans=0,cnt=0,i;
    for(i=20;i>=0;i--)
    {
        ans+=(1<<i);
        if(ans>=maxn||cnt+c[ans]>=k)
        {
            ans-=(1<<i);
        }
        else
        {
            cnt+=c[ans];
        }
    }
    return ans+1;
}
所以当ans+=1<<i的时候,如果此时cnt+c[ans]>=k,这证明这个数已经比第k位要大了,所以缩小区间(这里注意一下等于的情况,如果刚好等于,最后求得ans应该是第k小的数减1,这就是这个的核心思想);cnt+c[ans]<k的时候,所以我们要找的区间就在ans右边,所以一直无限靠近就可以找到最终和答案-1相等的数字的。而这里我们去考虑一下重复元素的情况,例如6个1,找第1小的数。所以我们最终的ans=0,所以还是1;6个2,找第二小的数,找到2的时候,cnt+c[2]==6>2,然后找到1 ,cnt+c[1]=0,所以此时ans=1,找到2.注意一下,这里的c[i]函数代表i这个数值有多少个的意思,所以有i这个一个数,就直接update(i,1)就可以了,更新函数和最初版本一样。下面这题是poj2985并查集+树状数组,初始化update(1,n)证明1这个值有n个,合并的时候把a[x],a[y]各减少一个   a[x]+a[y]增加一个,其实a[i]数组就是并查集集合里面元素的个数。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <map>
#include <cmath>
#define INF 99999999
typedef long long ll;
using namespace std;
int c[300009];
int father[300009];
int a[300009];
int maxn=300000;
int n;
int lowbit(int x)
{
    return x&(-x);
}
void update(int x,int val)
{
    while(x<=maxn)
    {
        c[x]+=val;
        x+=lowbit(x);
    }
}
int Find(int x)
{
    if(x!=father[x]) father[x]=Find(father[x]);
    return father[x];
}
int Find_kth(int k)
{
    int ans=0,cnt=0,i;
    for(i=20;i>=0;i--)
    {
        ans+=(1<<i);
        if(ans>=maxn||cnt+c[ans]>=k)
        {
            ans-=(1<<i);
        }
        else
        {
            cnt+=c[ans];
        }
    }
    return ans+1;
}
int main()
{
    //注意分清楚a[i]代表的时候该集合所包含的元素
    //c[i]代表着元素的大小,即树状数组的原数组
    int i,j,n,m;
    scanf("%d%d",&n,&m);
    for(i=1; i<=n; i++)
    {
        father[i]=i;
        a[i]=1;//初始化时每一个序号的集合都有1个
    }
    update(1,n);//初始化原始数组c[i]有n个1即,原来数组为n个1
    while(m--)
    {
        int op,x,y;
        scanf("%d",&op);
        if(op==0)
        {
            scanf("%d%d",&x,&y);
            x=Find(x);
            y=Find(y);
            if(x!=y)
            {
                father[x]=y;
                update(a[x],-1);
                update(a[y],-1);//这一个序号为x,y集合所包含的元素的个数在c[i]中减少
                update(a[x]+a[y],1);//因为合并了,所以新的集合的元素的个数为a[x]+a[y],因此新增了一个a[x]+a[y]
                a[y]+=a[x];//父节点集合元素更新
                a[x]=0;//该集合个数变为0
                n--;
            }
        }
        else
        {
            int k;
            scanf("%d",&k);
            printf("%d\n",Find_kth(n+1-k));
        }
    }
}
以上就是树状数组简单的应用,还有各种二维树状数组什么的,以后再更新吧。

猜你喜欢

转载自blog.csdn.net/keepcoral/article/details/80713110