首页 > 其他分享 >树状数组入门

树状数组入门

时间:2023-03-14 13:24:45浏览次数:39  
标签:入门 树状 int lowbit sum add 数组

前言

树状数组作为维护序列区间修改与查询的利器

是每一个 “OIer” 都应该要掌握的知识点

今天,我们来详细的整理一下树状数组的知识脉络

目录

一.树状数组简介

二.树状数组的实现

1.单点修改+区间查询

2.区间修改+单点查询

3.区间修改+区间查询

三.树状数组的应用

1.求逆序对

2.树状数组倍增

四.二维树状数组

正文

树状数组简介

树状数组即形如下图的树形结构:  其基本性质为tree[i]=a[i-2^k+1]+a[i-2^k+2]+...+a[i]tree[i]=a[i−2k+1]+a[i−2k+2]+...+a[i]其中kk为ii的二进制中从最低位到高位连续零的长度;treetree表示树状数组;aa表示原数组

我们可以使用 lowbit 来找到最低位的0lowbit(x)=x\&(-x)lowbit(x)=x&(−x)

树状数组的实现

首先我们要明白,树状数组的本质依然是在维护数列中的某一个数的加减和对于数列前缀的求和,除此之外不能够进行其他的任何操作

因此我们需要合理的运用其他思想来利用树状数组解决问题

1.这里是实现对数列中的某一个数的加减的实现

void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    tree[i]+=y;
    return;
}

2.这里是对求区间前缀和的实现

int get(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=tree[i];
    }
    return sum;
}

单点修改+区间查询

关于单点修改+区间查询,我们很容易想到运用前缀和求解,这种方法完全贴合树状数组的基本操作,因而就不过多追述了

模板【模板】树状数组 1

代码

#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[500001];
int tree[500001];
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    tree[i]+=y;
    return;
}
int get(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=tree[i];
    }
    return sum;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        add(i,a[i]);
    }
    for(int i=1;i<=m;i++)
    {
        int op,l,r;
        scanf("%d%d%d",&op,&l,&r);
        if(op==2)
        printf("%d\n",get(r)-get(l-1));
        else
        add(l,r);
    }
    return 0;
} 

区间修改+单点查询

实现区间修改+单点查询的方法同样好想,我们很容易由前缀和想到差分求解,因为树状数组的查询是求前缀和,因此刚好我们就可以还原出单个元素的值

模板【模板】树状数组 2

代码

#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[500005];
int tree[500005];
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    tree[i]+=y;
    return;
}
int get(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=tree[i];
    }
    return sum;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        add(i,a[i]-a[i-1]);
    }
    for(int i=1;i<=m;i++)
    {
        int op;
        scanf("%d",&op);
        if(op==1)
        {
            int x,y,k;
            scanf("%d%d%d",&x,&y,&k);
            add(x,k);
            add(y+1,0-k);
        }
        if(op==2)
        {
            int x;
            scanf("%d",&x);
            printf("%d\n",get(x));
        }
    }
    return 0;
} 

区间修改+区间查询

解决这个问题的方法较为复杂,但是我们很容易想到用差分的前缀和(还原原数组)的基础上再取前缀和

其中显然树状数组的询问方式可以帮助我们解决掉一次的求和,那我们怎么完成第二次的求和呢?

我们用a表示原数组,c表示差分数组可以发现a[1]+a[2]+...+a[x]a[1]+a[2]+...+a[x]=(c[1])+(c[1]+c[2])+...+(c[1]+c[2]+...+c[x])=(c[1])+(c[1]+c[2])+...+(c[1]+c[2]+...+c[x])=c[1]\times n+c[2]\times (x-1)+...+c[x]\times 1=c[1]×n+c[2]×(x−1)+...+c[x]×1即对于每一个c[i]c[i]在求和中的贡献应该为c[i]\times (x-i+1)c[i]×(x−i+1)故我们维护treea[i]treea[i]表示原差分数组,treeb[i]treeb[i]表示treea[i]\times (i-1)treea[i]×(i−1)

问题就能够迎刃而解!!!

1.这里是对于修改的实现

对于修改,我们修改treeatreea时只需要直接修改即可

至于修改treebtreeb我们也只需要变成

add(x,y):treeb[i]+=y\times(x-1)add(x,y):treeb[i]+=y×(x−1)可以证明其正确性,理由是乘法的分配率treeb[i]+=y\times(x-1)=k+a\times(x-1)+y\times(x-1)=k+(a+y)\times(x-1)treeb[i]+=y×(x−1)=k+a×(x−1)+y×(x−1)=k+(a+y)×(x−1)

其中aa、bb、kk均为常数

代码

void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        treea[i]+=y;
        treeb[i]+=y*(x-1);
    }
    return;
}

2.这里是对于询问的实现

对于询问我们只需要按如下公式计算即可get(x):treea[i]\times x-treeb[i]get(x):treea[i]×x−treeb[i]

即=c[i]\times x-c[i]\times (i-1)=c[i]\times(x-i+1)=c[i]×x−c[i]×(i−1)=c[i]×(x−i+1)

也就是本章最开始推出来的那个公式

代码

int get(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=treea[i]*x-treeb[i];
    }
    return sum;
}

模板【模板】线段树 1

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
int op;
int k;
int l,r;
int n,m;
int a[100001];
int treea[100001];
int treeb[100001];
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        treea[i]+=y;
        treeb[i]+=y*(x-1);
    }
    return;
}
int get(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=treea[i]*x-treeb[i];
    }
    return sum;
}
signed main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        add(i,a[i]);
        add(i+1,-a[i]);
    }
    while(m--)
    {
        cin>>op;
        if(op==1)
        {
            cin>>l>>r>>k;
            add(l,k);
            add(r+1,-k);
        }
        else
        {
            cin>>l>>r;
            cout<<get(r)-get(l-1)<<endl;
        }
    }
    return 0;
}

树状数组的应用

求逆序对

树状数组是求逆序对的利器,虽然归并排序也可以求逆序对,但是树状数组无论是从码量还是理解难度上都全面碾压归并排序

其实现逻辑非常简单,首先对于进行排序,然后用二分离散化(原因是逆序对要求严格单调)原数组在他们中的位置,按原数组的顺序加入即可求解即可。即统计大于它但在他之前出现的元素个数

同样三元的逆序对也可以这样求解,我们枚举中间的元素(不然无法知道另外两个元素的先后顺序与大小关系),正着求一次,倒着求一次,乘法原理求解即可

因为理解较为容易,因此这里直接附上例题和代码来帮助理解

模板1(二元逆序对)P1908 逆序对

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int pre=5e5+10;
int n;
int a[pre];
int b[pre];
int place[pre];
int tree[pre];
int ans;
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        tree[i]+=y;
    }
    return;
}
int get(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=tree[i];
    }
    return sum;
}
signed main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0); 
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        b[i]=a[i];
    }
    sort(b+1,b+1+n);
    for(int i=1;i<=n;i++)
    {
        place[i]=lower_bound(b+1,b+1+n,a[i])-b;
    }
    for(int i=1;i<=n;i++)
    {
        add(place[i],1);
        ans+=get(n)-get(place[i]);
    }
    cout<<ans;
    return 0;
}

模板2(三元逆序对)Enemy is weak

代码

#include<bits/stdc++.h>
using namespace std;
const int MAX=1e6+10;
int n;
int a[MAX];
int p[MAX];
int place[MAX];
int treebefore[MAX];
int treeafter[MAX];
int ansbefore[MAX];
int ansafter[MAX];
long long ans;
int lowbit(int x)
{
    return x&(-x);
}
void addbefore(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        treebefore[i]+=y;
    }
    return;
}
int getbefore(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=treebefore[i];
    }
    return sum;
}
void addafter(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        treeafter[i]+=y;
    }
    return;
}
int getafter(int x)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        sum+=treeafter[i];
    }
    return sum;
}
int main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        p[i]=a[i];
    }
    sort(p+1,p+1+n);
    for(int i=1;i<=n;i++)
    {
        place[i]=lower_bound(p+1,p+1+n,a[i])-p;
    }
    for(int i=1;i<=n;i++)
    {
        addbefore(place[i],1);
        ansbefore[i]=getbefore(n)-getbefore(place[i]);
    }
    for(int i=n;i>=1;i--)
    {
        addafter(place[i],1);
        ansafter[i]=getafter(place[i]-1);;
    }
    for(int i=1;i<=n;i++)
    {
        ans+=1ll*ansbefore[i]*ansafter[i];
    }
    cout<<ans;
    return 0;
} 

树状数组倍增

树状数组倍增的本质是倍增 (废话)

可以用于求出序列排名为kk的数在树状数组映射的原数组(意思是返回的是树状数组的下标,并非是原数组的值)中的位置

我们考虑到一个性质

tree[x|2^k]=a[x+1]+...+a[x|2^k]tree[x∣2k]=a[x+1]+...+a[x∣2k]

聪明的孩子可以想想这是为什么

所以我们可以证明思路可行性:

首先维护一个关于数列升序排序的各元素个数的树状数组,然后进行倍增

我们用和LCALCA一样的方式进行倍增,判一个边界,和一个<k<k

最后返回答案+1+1即可,不存在排名为kk的数当且仅当返回值为n+1n+1时

具体实现如下

int get(int o)
{
    int s=0;
    int now=0;
    for(int i=log2(n);i>=0;i--)
    {
        int to=now|(1<<i);
        if(to>n)
        continue;
        int x=s+tree[to];
        if(x<o)
        {
            now=to;
            s=x;
        }
    } 
    return now+1;
}

模板Multiset

代码

#include<bits/stdc++.h>
using namespace std;
int n,q; 
int k;
int tree[1000001];
int a[1000001];
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        tree[i]+=y;
    }
    return;
}
int get(int o)
{
    int s=0;
    int now=0;
    for(int i=log2(n);i>=0;i--)
    {
        int to=now|(1<<i);
        if(to>n)
        continue;
        int x=s+tree[to];
        if(x<o)
        {
            now=to;
            s=x;
        }
    } 
    return now+1;
}
int main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        add(a[i],1);
    }
    while(q--)
    {
        cin>>k;
        if(k<0)
        {
            int xx=get(abs(k));
            if(xx!=n+1)
            {
                add(xx,-1);
            }
        }
        else
        {
            add(k,1);
        }
    }
    if(get(1)!=n+1)
    cout<<get(1);
    else
    cout<<0;
    return 0;
}

二维树状数组

二维树状数组的前置知识点是二维的前缀和和差分

我们分别来说一下

首先对于前缀和,我们维护一个该元素左上角的矩阵元素之和

对于任意矩阵之和的计算,我们根据容斥原理,得出

矩阵(a,b,c,d)=(1,1,c,d)-(1,1,a-1,d)-(1,1,c,b-1)+(1,1,a-1,b-1)(a,b,c,d)=(1,1,c,d)−(1,1,a−1,d)−(1,1,c,b−1)+(1,1,a−1,b−1)

然后考虑差分,差分作为前缀和的逆运算,其运算已经无需解释

我们重点来看差分的维护

我们很容易发现 (没错我注意力涣散了) 对于下图的矩阵(3,4,3,4)(3,4,3,4)加上一个xx,差分数组的变化如下

@ 1 2 3 4 5

1 0 0 0 0 0

2 0 0 0 0 0

3 0 0 +x 0 -x

4 0 0 0 0 0

5 0 0 -x 0 +x

这样我们就基本了解关于前缀和和差分的维护了

我们很容易想到利用这两个工具维护出二维树状数组的单点修改和区间查询(前缀和) 以及 区间修改和单点查询(差分)

这里附上两道模板题

单点修改,区间查询

区间修改,单点查询

但是对于区间修改和区间查询事情就有些复杂了

因为存在区间修改,所以我们还是考虑基于差分进行实现,不过我们需要对差分取两次前缀和,依旧考虑拆式子

发现在计算矩阵(1,1,x,y)(1,1,x,y)的时候,元素(i,j)(i,j)被计算了(x-i+1)(y-j+1)(x−i+1)(y−j+1)次

继续拆分

(x-i+1)(y-j+1)(x−i+1)(y−j+1)=(xy-xj+x)+(-yi+ij-i)+(y-j+1)=(xy−xj+x)+(−yi+ij−i)+(y−j+1)=(xy+x+y+1)-i(y+1)-j(x+1)+ij=(xy+x+y+1)−i(y+1)−j(x+1)+ij

所以我们考虑维护一个

原差分数组的11倍ii倍jj倍和ijij倍

然后就可以完成了

附上例题和代码

P4514 上帝造题的七分钟

#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
int n,m;
int tree1[2050][2050];//*1
int tree2[2050][2050];//*i
int tree3[2050][2050];//*j
int tree4[2050][2050];//*ij
char op;
int a,b,c,d,k;
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int y,int k)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        for(int j=y;j<=m;j+=lowbit(j))
        {
            tree1[i][j]+=k;
            tree2[i][j]+=k*x;
            tree3[i][j]+=k*y;
            tree4[i][j]+=k*x*y;
        }
    }
    return;
}
int get(int x,int y)
{
    int sum=0;
    for(int i=x;i>=1;i-=lowbit(i))
    {
        for(int j=y;j>=1;j-=lowbit(j))
        {
            sum+=tree1[i][j]*(x*y+x+y+1)-tree2[i][j]*(y+1)-tree3[i][j]*(x+1)+tree4[i][j];
        }
    }
    return sum;
}
signed main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    while(cin>>op)
    {
        if(op=='L')
        {
            cin>>a>>b>>c>>d>>k;
            add(a,b,k);
            add(a,d+1,-k);
            add(c+1,b,-k);
            add(c+1,d+1,k);
        }
        else if(op=='k')
        {
            cin>>a>>b>>c>>d;
            cout<<get(c,d)-get(a-1,d)-get(c,b-1)+get(a-1,b-1)<<endl;
        }
        else
        {
            cin>>n>>m;
        }
    }
    return 0;
}

标签:入门,树状,int,lowbit,sum,add,数组
From: https://www.cnblogs.com/eveningbreath/p/17214611.html

相关文章

  • 32位汇编语言实现求数组的最大值
    INCLUDEIrvine32.inc.dataarrdd99,2,3,1,22,188,7,77,54,10;定义数组 lendd($-arr)/4;用当前地址减去数组首元素地址除以4得到数组的长度.codemainPROC......
  • (原创)【B4A】一步一步入门07:EditText,文本框、密码框、键盘样式、提示文本(控件篇03)
    一、前言本篇教程,我们来讲一下常用的控件:EditText(文本输入框)。本篇教程将会讲解文本框的基本使用,如:提示文本、密码文本、键盘样式等。相信看完的你,一定会有所收获!本文......
  • 差分数组
    题目难度要点拼车●不需要构造原始数组,直接判断即可航班预定统计●构造原始数组区间加法●构造原始数组拼车classSolution{publicboo......
  • 【LeetCode回溯算法#11】解数独,这次是真的用回溯法处理二维数组
    解数独力扣题目链接(opensnewwindow)编写一个程序,通过填充空格来解决数独问题。一个数独的解法需遵循如下规则:数字1-9在每一行只能出现一次。数字1-9在每一列只......
  • Canal入门
    一.Canal入门1.1Canal是什么canal[kə'næl],译意为水道/管道/沟渠,主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费早期阿里巴巴因为杭州和美国双......
  • 剑指 Offer 11.旋转数组的最小数字
    题目描述   解法二分查找思路:设i为左界,j为右界,中点为mid;将number[mid]与number[j]进行比较,会出现一下情况:number[mid]<number[j]时,说明number[mid]是最......
  • 造价入门
    1.通过图集看图 ---通过软件建模 ---建模过程中--软件自动生成工程量计算方式 比如我先进行柱子的建模,再进行柱子的提亮,建设清单,柱子的定额计价 1.建模......
  • Java数组
    Java数组1.数组概述数组的定义数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个数组元......
  • java基础-一维数组
    1、什么是数组:数组是一个变量,存储是相同数据类型的一组数据,声明数组,就是在内存中划分一串连续的空间注意:数组一经定义,大小就确定了,不可以在此基础上再增加......
  • java基础-排序算法&&二维数组
    1、冒泡排序--升序原理:每次比较相邻两数小的交换到前面每轮结束后最大的数交换到最后口诀:冒泡排序速记口诀(升序)n个数字来排队......