树状数组学习笔记

2022/9/2 23:25:10

本文主要是介绍树状数组学习笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

 

一:树状数组定义

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

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

​编辑

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

 1 #include<iostream>
 2 
 3 using namespace std;
 4 const int N=5e5+10;
 5 int tr[N],n,m;
 6 
 7 int lowbit(int x)
 8 {
 9     return x & -x;
10 }
11 
12 void add(int x,int k)
13 {
14     for(;x<=n;x+=lowbit(x)) tr[x]+=k;
15 }
16 
17 int sum(int x)
18 {
19     int sum=0;
20     for(;x;x-=lowbit(x)) sum+=tr[x];
21     return sum;
22 }
23 
24 int main()
25 {
26     ios::sync_with_stdio(false);//树状数组题目中数据范围一般都比较大,最好用scanf或者cin加速读入
27     cin.tie(0);
28     
29     cin>>n>>m;
30     
31     for(int i=1;i<=n;i++) 
32     {
33         int x;
34         cin>>x;
35         add(i,x);//在第i个位置加上x
36     }
37     
38     while(m--)
39     {
40         int op;
41         cin>>op;
42         int x,y;
43         cin>>x>>y;
44         
45         if(op==1) add(x,y);
46         else cout<<sum(y)-sum(x-1)<<endl;//求出1到y和1到x-1的和,相减即为x-y的和
47     }
48 }

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

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

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

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

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

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

 1 #include<iostream>
 2 
 3 using namespace std;
 4 const int N=5e5+10;
 5 int tr[N],n,m;
 6 
 7 int lowbit(int x)
 8 {
 9     return x & -x;
10 }
11 
12 void add(int x,int k)
13 {
14     for(;x<=n;x+=lowbit(x)) tr[x]+=k;
15 }
16 
17 int sum(int x)
18 {
19     int sum=0;
20     for(;x;x-=lowbit(x)) sum+=tr[x];
21     return sum;
22 }
23 
24 int main()
25 {
26     ios::sync_with_stdio(false);
27     cin.tie(0);
28     
29     cin>>n>>m;
30     
31     int last=0;
32     for(int i=1;i<=n;i++)
33     {
34         int x;
35         cin>>x;
36         add(i,x-last);
37         last=x;
38     }
39     
40     while(m--)
41     {
42         int op;
43         cin>>op;
44         
45         if(op==1)
46         {
47             int x,y,k;
48             cin>>x>>y>>k;
49             
50             add(x,k),add(y+1,-k);
51         }
52         
53         else
54         {
55             int x;
56             cin>>x;
57             cout<<sum(x)<<endl;
58         }
59     }
60 }

 十:拓展内容:

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

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

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

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

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

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

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

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

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

 1 #include<iostream>
 2 #include<unordered_map>
 3 #include<algorithm>
 4 
 5 using namespace std;
 6 const int N=5e5+10;
 7 int tr[N],n,a[N];
 8 
 9 inline int lowbit(int x)
10 {
11     return x& -x;
12 }
13 
14 inline void add(int x,int k)
15 {
16     for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=k;
17 }
18 
19 inline int sum(int x)
20 {
21     int sum=0;
22     for(int i=x;i;i-=lowbit(i)) sum+=tr[i];
23     return sum;
24 }
25 
26 unordered_map<int,int> s;
27 int l=0;
28 inline int get(int x)
29 {
30     if(!s.count(x)) s[x]=++l;
31     return s[x];
32 }
33 
34 int b[N];
35 inline void pai()
36 {
37     copy(begin(a),end(a),begin(b));
38     sort(b+1,b+1+n);
39     for(int i=1;i<=n;i++) get(b[i]);
40     
41 }
42 
43 
44 int main()
45 {
46     
47     scanf("%d",&n);
48     
49     long long ans=0;
50     
51     for(int i=1;i<=n;i++) scanf("%d",&a[i]);
52     
53     pai();
54     
55     for(int i=1;i<=n;i++)
56     {
57         int id=get(a[i]);
58         ans=ans+i-1-sum(id);
59         add(id,1);
60     }
61     
62     cout<<ans<<endl;
63     return 0;
64 }

注:由于笔者比较笨,又使用了一次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和网上大佬)

笔者CSDN:https://blog.csdn.net/qq_69908563/article/details/126669563

洛谷blog:https://www.luogu.com.cn/blog/buqiming/#



这篇关于树状数组学习笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程