树状数组学习笔记

本文详细介绍了树状数组的定义、构建过程,以及其实现单点修改和区间求和的功能。通过对比线段树,突出了树状数组的高效性和易调试性。通过实例和模板题展示了如何查询、修改和求和,以及在求逆序对和第k大数问题上的应用。
摘要由CSDN通过智能技术生成

一:树状数组定义

望文生义,树状数组就是用树形结构来模拟数组的一种数据结构。

二:图解(纯手绘,难看勿喷)

C表示从1-k的和,

C[1]=a[1]

C[2]=C[1]+a[2]

C[3]=a[3]

C[4]=C[2]+C[3]+a[4]

C[5]=a[5]

C[6]=C[5]+a[6]

C[7]=a[7]

C[8]=C[4]+C[6]+C[7]+a[8]

C[9]=a[9]

C[10]=C[9]+a[10]

C[11]=a[11]

C[12]=C[10]+C[11]+a[12]

C[13]=a[13]

C[14]=C[13]+a[12]

C[15]=a[15]

C[16]=C[8]+C[12]+C[14]+C[15]+a[16]

三:可解决问题

单点修改,区间求和。

四:为何建立树状数组

我们可以发现树状数组中,一个数直接对应的数最多有logk+1个数,因此我们在求解一个区间和时,可以在O(log n) 内求解出。

五:与线段树的区别

树状数组能解决的问题线段树都能解决,线段树能解决的问题树状数组不一定能解决,但是!!!树状数组更快!!!树状数组更好调试!!!毕竟,能简单点谁不想简单呢。

六:如何查询树状数组子节点

根据上述建树过程我们可以发现,当前点所存的范围是(x-lowbit(x)+1,x)。

那么怎么实现lowbit()呢?

这需要用到二进制了。

计算机中有源码,反码和补码。

源码就是数据本来对应的二进制数;

正数的反码等于它本身。

负数的反码等于在其源码的基础上,符号位不变,其余位取反。

正数的补码等于它本身。

负数的补码等于其反码加一。

例如14=1110

反码:1110

-14的补码:1111

那么他的最低一位1可以油 14&-14。

我们也可以这样想,只要x不为0,那么x必有一位是1,取得他的负数后,最后那些是0的位数都会变为1,再加上1,则会将1都变为0,现在出现的最低一位1就是所求的最低的1,即lowbit()。

由于我们在树状数组中需要经常用到lowbit,因此我们可以将其写为一个函数,以便使用。

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

七:如何修改树状数组的值

对于上面的建树过程分析,我们可以发现当前这个数x会涵盖(x-lowbit(x)+1,x)内的元素,因此不难知道,对于每一个x的修改,都会影响到x+lowbit(x)这个数,直到影响到自己设定的最大值。

可以写出add函数

void add(int x,int k)
{
    for(int i=x;i<=maxx;i+=lowbit(x)) tr[i]+=k;
}

八:如何求和

继续分析建树过程,我们会发现x不会涵盖x-lowbit(x)的内容,所以我们求和时只需要加上

x-lowbit(x)所涵盖的元素即可。

void sum(int x)
{
    int sum=0;
    for(int i=x;i;i-=lowbit(i)) sum+=tr[i];
    return sum;
}

九:模板题目:洛谷:P3374 【模板】树状数组 1

#include<iostream>

using namespace std;
const int N=5e5+10;
int tr[N],n,m;

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

void add(int x,int k)
{
    for(;x<=n;x+=lowbit(x)) tr[x]+=k;
}

int sum(int x)
{
    int sum=0;
    for(;x;x-=lowbit(x)) sum+=tr[x];
    return sum;
}

int main()
{
    ios::sync_with_stdio(false);//树状数组题目中数据范围一般都比较大,最好用scanf或者cin加速读入
    cin.tie(0);
    
    cin>>n>>m;
    
    for(int i=1;i<=n;i++) 
    {
        int x;
        cin>>x;
        add(i,x);//在第i个位置加上x
    }
    
    while(m--)
    {
        int op;
        cin>>op;
        int x,y;
        cin>>x>>y;
        
        if(op==1) add(x,y);
        else cout<<sum(y)-sum(x-1)<<endl;//求出1到y和1到x-1的和,相减即为x-y的和
    }
}

模板题,就不需要再做过多解释了吧。

模板题二:洛谷:【模板】树状数组 2

有些小伙伴可能看见这题,就会问了,树状数组不是只能做单点修改吗?

是的,但是我们可以用差分做这题啊!

想一想,1-n的差分加起来不就是n这个数么?有没有恍然大悟的感觉?

我们只需要在第x个位置上加上数据k,再在第y+1个位置上减去k不就能实现求任何一个位置的数了么?

#include<iostream>

using namespace std;
const int N=5e5+10;
int tr[N],n,m;

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

void add(int x,int k)
{
    for(;x<=n;x+=lowbit(x)) tr[x]+=k;
}

int sum(int x)
{
    int sum=0;
    for(;x;x-=lowbit(x)) sum+=tr[x];
    return sum;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin>>n>>m;
    
    int last=0;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        add(i,x-last);
        last=x;
    }
    
    while(m--)
    {
        int op;
        cin>>op;
        
        if(op==1)
        {
            int x,y,k;
            cin>>x>>y>>k;
            
            add(x,k),add(y+1,-k);
        }
        
        else
        {
            int x;
            cin>>x;
            cout<<sum(x)<<endl;
        }
    }
}

 十:拓展内容:

1:求逆序对:洛谷:P1908 逆序对

此题可以用树状数组边插入边求解。

何谓逆序对?就是i<j时,第i个数大于第j个数此类的。

我们可以用树状数组,在这个数的数值的位置加上一代表这个数被加入到了树状数组。(是数值,不是序号!!!)

那么怎么求有多少个逆序对呢?

我们可以用sum(x)求出在x之前有多少个数,可以边add()边求逆序对,因此第i个数逆序对的数量就等于i-sum(a[i]).

总体逆序对的数量只需要对其求和即可。

但是有一个问题,如果数据过大咋办,我们是不能开这么大的数组的。

这时候就需要用到离散化了。(可以选择的离散方式有很多,这里选择哈希离散)

#include<iostream>
#include<unordered_map>
#include<algorithm>

using namespace std;
const int N=5e5+10;
int tr[N],n,a[N];

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

inline void add(int x,int k)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=k;
}

inline int sum(int x)
{
    int sum=0;
    for(int i=x;i;i-=lowbit(i)) sum+=tr[i];
    return sum;
}

unordered_map<int,int> s;
int l=0;
inline int get(int x)
{
    if(!s.count(x)) s[x]=++l;
    return s[x];
}

int b[N];
inline void pai()
{
    copy(begin(a),end(a),begin(b));
    sort(b+1,b+1+n);
    for(int i=1;i<=n;i++) get(b[i]);
    
}


int main()
{
    
    scanf("%d",&n);
    
    long long ans=0;
    
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    
    pai();
    
    for(int i=1;i<=n;i++)
    {
        int id=get(a[i]);
        ans=ans+i-1-sum(id);
        add(id,1);
    }
    
    cout<<ans<<endl;
    return 0;
}

注:由于笔者比较笨,又使用了一次copy函数,导致此题慢了很多。

2 .求树状数组中第k大数

1.二分法:

int find(int x)
{
    int l=0,r=maxx+1;
    while(l<r)
    {
        int mid=l+r>>1;
        if(sum(mid)<x) l=mid-1;
        else r=mid+1;
    }
    
    return r;
}

2.二进制法:

int find(int x)
{
    int ans=0,cnt=0;
    for(int i=20 ;i>=0;i--)//可以随意设置需要的大小,表示从二进制第i为开始
    {
        ans+=(1<<i);
        if(ans>=maxx||cnt+tr[ans]>=x) ans-=(1<<i);//目前的数大于最大值或者已经求出来的个数+当前值对应的数的个数>=自己要求的数则返回
        else cnt+=tr[ans];
        
    }
    return ans+1;//求出的是第x-1大的数,则ans+1为第x大的数。
}

高中蒟蒻的学习笔记。

(学习自acwing和网上大佬)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值