「学习笔记」树状数组

一.原理

1.简介(百度介绍)

树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
这种数据结构(算法)并没有C++和Java的库支持,需要自己手动实现。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。

2.具体原理

先观察下面这张表(设原数组为a[],前缀数组为b[],树状数组中维护子集和的数组sum[],下面出现相应不再解释)

下标12345678910
下标二进制0001001000110100010101100111100010011010
a数组1201002212
b数组13344468911
sum数组1304002813
sum维护子集区间11-231-455-671-899-10

由表我们可以看出,sum[i]维护的区间大小是i二进制最低非零位所表示的数的大小,如i为2时,其最低为零位所表示的数为2,所以sum[2]维护区间【a[1],a[2]】的和,如i为8时,其最低非零位所表示的数为8所以sum[8]维护的是【a[1],a[2]…a[8]】的和。
下图将更为直观

在这里插入图片描述

知道sum要维护的东西之后,我们还要知道怎样利用sum[]求前缀和.我们发现b[8]=sum[8],b[7]=sum[7]+sum[6]+sum[4],也即是b[1000]=sum[1000],b[0111]=sum[0111]+sum[0110]+sum[0100].(里面是下标的二进制)由此我们可以知道前k个数的和 为 k依次减去其对应的最低非零位的数 的sum和。
由此我们就需要求出一个数的最低非零位所对应的数,也即是lowbit.如果学过补码你会知道 lowbit(x)=x&(-x) 当然你也可以这样算 lowbit(x)=x-(x&(x-1))(将x最低位的1以及后面所有的0都变成了0,然后被x减去之后,就得到想要的数了)
例如: x= 1011000 , (x&(x-1)) 1011000 & 1010111 =1010000 ,1011000-1010000=10000,即得到x最低非零位所对应的数

之后就是修改单点的值了,比如我们要修改a[2]的值,那么sum[2],sum[4],sum[8]…都要相应的改变,通过上图观察也可以看出来,位于其上面的区间段都要改变。假设x是我们要改变值的下标,那么受其影响的sum数组下标都是谁呢,显然是x,x+lowbit(x)(值为c),c+lowbit( c)(值为d),d+lowbit(d)…可以思考下为啥,可以类比求前缀和,此处不再解释.

原理清楚了,可以开始试着写了。

二.操作

1.求lowbit

int lowbit(int x){
    return x&(-x);//写起来简单,不懂就直接记吧
    //return x-(x&(x-1));
}

2.求前缀和

int getsum(int x){
    int ans=0;
    while(x>0){
        ans+=sum[x];
        x-=lowbit(x);//减去x最低非零位所表示的数
    }
    return ans;
}

3.单点更新

void update(int x,int value){
    while(x<=n){
        sum[x]+=value;
        x+=lowbit(x);//加上x最低非零位所表示的数即为下一个受影响的sum下表
    }
}

4.求区间和

//求区间[l,r]的和,调用getsum(r)-getsum(l-1)即可
//如果原数组a[]没开,怎样查询单点的a[x]的值呢
//调用getsum(x)-getsum(x-1)太慢,如果学过LCA我们可以这样求a[x]
// a[x]=sum[x]-(getsum(x-1)-getsum(LCA(x,x-1)))
//例如a[12]=sum[12]-(a[11]+a[10]+a[9])
//也即是a[12]=sum[12]-(getsum(11)-getsum(8))
//而8是怎么得来的呢,8其实是11和12的LCA所以这个式子看似要算两遍,但更省时间。
int get_sum(int x){
	int ans=sum[x];
	int lca=x-lowbit(x);
	x--;
	while(x!=lca){
		ans-=sum[x];
		x-=lowbit(x);
	}
	return ans;
}

5.查询某个前缀下标(元素非负)

//二分
int get_index(int v){
	int x=0,len=n;
	while(len!=0){
		int now=x+len;
		if(v>=sum[now]){
			x=now;
			v-=sum[now];
		}
		len/=2;
	}
	return x;
}

6.封装板子

struct BIT{
    int bit[maxn];
    inline void init(){ for(int i=1;i<=n;i++)bit[i]=0; }//初始化
    inline int lowbit(int x){ return x&(-x); }//求lowbit
    inline void update(int x,int val){ for(int i=x;i<=n;i+=lowbit(i)) bit[i]+=val; }//单点更新
    inline int query(int x,int res=0){ for(int i=x;i;i-=lowbit(i))res+=bit[i]; return res; }//前缀和查询
    inline int get_index(int v,int x=0){//查询前缀和下标
        while(len!=0){ int now=x+len; if(v>=sum[now]){ x=now,v-=sum[now]; } len/=2; } return x;
    }
}s;

三.二维树状数组

1.原理

类比一维树状数组,二维树状数组sum定义就很明确了:
s u m [ x ] [ y ] = ∑ i = x − l o w b i t ( x ) + 1 x ∑ j = y − l o w b i t ( y ) + 1 y a [ i ] [ j ] sum[x][y]=\sum_{i=x-lowbit(x)+1}^{x}\sum_{j=y-lowbit(y)+1}^{y}a[i][j] sum[x][y]=i=xlowbit(x)+1xj=ylowbit(y)+1ya[i][j]
时间复杂度: O ( ( l o g n ) 2 ) O((logn)^2) O((logn)2)(查询 修改)

2.代码实现

(1)二维前缀和

int getsum(int x,int y){//类比一维就多套了一个循环
	int ans=0;
	while(x>0){
		int dy=y;
		while(dy>0){
			ans+=sum[x][dy];
			dy-=lowbit(dy);
		}
		x-=lowbit(x);
	}
	return ans;
}

(2)二维修改操作

void update(int x,int y,int v){
	while(x<=n){
		int dy=y;
		while(dy<=n){
			sum[x][dy]+=v;
			dy+=lowbit(dy);
		}
		x+=lowbit(x);
	}
}

四.维护区间加

首先我们需要把区间加转化为单点加,假设对原数组a[] [ l , r ] [l,r] [l,r]整体加上v,利用差分的思想,可以新建一个数组c[] c [ i ] = a [ i ] − a [ i − 1 ] c[i]=a[i]-a[i-1] c[i]=a[i]a[i1]
也就是 c [ l ] + = v , c [ r + 1 ] − = v c[l]+=v,c[r+1]-=v c[l]+=v,c[r+1]=v这是为什么呢,根据差分,我们知道在 a [ l ] − a [ r ] a[l]-a[r] a[l]a[r]之间 在加v和不加v时 c [ l + 1 ] − c [ r ] c[l+1]-c[r] c[l+1]c[r]的值没有发生变化,而 c [ l ] c[l] c[l] c [ r + 1 ] c[r+1] c[r+1]的值发生变化了,其中 c [ l ] = a [ l ] − a [ l − 1 ] c[l]=a[l]-a[l-1] c[l]=a[l]a[l1]比原来多了d,所以要加d, c [ r + 1 ] = a [ r + 1 ] − a [ r ] c[r+1]=a[r+1]-a[r] c[r+1]=a[r+1]a[r]比原来少了d,所以要减d。附上一张表:(下标从1开始设 a [ 0 ] = c [ 0 ] = s u m [ 0 ] = 0 a[0]=c[0]=sum[0]=0 a[0]=c[0]=sum[0]=0)

下标12345678910
a数组1112222211
c数组10010000-10
sum数组12152421312

a [ 4 ] a[4] a[4] a [ 8 ] a[8] a[8]的值加5

下标12345678910
a数组1117777711
c数组1001+50000-1-50
sum数组1215+52+54+102+513+2012

假设现在更改过的a数组为s[],不难得出:
s [ i ] = a [ i ] + c [ 1 ] + c [ 2 ] + . . . + c [ i ] s[i]=a[i]+c[1]+c[2]+...+c[i] s[i]=a[i]+c[1]+c[2]+...+c[i]
所以:
t [ i ] = s [ 1 ] + s [ 2 ] + . . . + s [ i ] = ( a [ 1 ] + c [ 1 ] ) + ( a [ 2 ] + c [ 1 ] + c [ 2 ] ) + ( a [ 3 ] + c [ 1 ] + c [ 2 ] + c [ 3 ] ) + . . . + ( a [ i ] + b [ 1 ] + . . . + b [ i ] ) ( t [ ] 为 当 前 维 护 的 前 缀 数 组 ) t[i]=s[1]+s[2]+...+s[i]=(a[1]+c[1])+(a[2]+c[1]+c[2])+(a[3]+c[1]+c[2]+c[3])+...+(a[i]+b[1]+...+b[i])(t[]为当前维护的前缀数组) t[i]=s[1]+s[2]+...+s[i]=(a[1]+c[1])+(a[2]+c[1]+c[2])+(a[3]+c[1]+c[2]+c[3])+...+(a[i]+b[1]+...+b[i])(t[])
去掉括号整理得:
t [ i ] = ( a [ 1 ] + a [ 2 ] + . . . + a [ i ] ) + i × c [ 1 ] + ( i − 1 ) × c [ 2 ] + . . . + 1 × c [ i ] t[i]=(a[1]+a[2]+...+a[i])+i×c[1]+(i-1)×c[2]+...+1×c[i] t[i]=(a[1]+a[2]+...+a[i])+i×c[1]+(i1)×c[2]+...+1×c[i]
其中 a [ 1 ] + . . + a [ i ] a[1]+..+a[i] a[1]+..+a[i]可以预处理得到(也就是前面提到的b[]前缀数组),但是后面却不好得到,不妨让每个c[]都乘i+1次
则:
t [ i ] = ( a [ 1 ] + a [ 2 ] + . . + a [ i ] ) + ( i + 1 ) × ( c [ 1 ] + c [ 2 ] + . . + c [ i ] ) − ( 1 × c [ 1 ] + 2 × c [ 1 ] + . . . + i × c [ i ] ) t[i]=(a[1]+a[2]+..+a[i])+(i+1)×(c[1]+c[2]+..+c[i])-(1×c[1]+2×c[1]+...+i×c[i]) t[i]=(a[1]+a[2]+..+a[i])+(i+1)×(c[1]+c[2]+..+c[i])(1×c[1]+2×c[1]+...+i×c[i])
不妨设f[],其中f[i]=i×c[i];
现在整理一下:
(1)用b[]预处理原数组a[]的前缀和
(2)c[]的前缀和用树状数组维护,对于每次[L,R]修改,我们只修改 c [ l ] + = v , c [ r + 1 ] − = v c[l]+=v,c[r+1]-=v c[l]+=v,c[r+1]=v;
(3)f[]的前缀和再用一个树状数组来维护,对于每次[L,R]修改,我们只修改 f [ l ] + = l ∗ v , f [ r + 1 ] − = ( r + 1 ) ∗ v f[l]+=l*v,f[r+1]-=(r+1)*v f[l]+=lv,f[r+1]=(r+1)v;
这是一维条件下的,二维类比一维也可以用一个数组c[][]来使区间加变成单点加,单点查询变成矩形区间查询。

五.应用

1.二维数点问题(线段树/树状数组)

二维数点问题一般用线段树或树状数组来解,分两种形式。
(1)离线型: 树状数组/线段树+一维排序(消除一维限制)
(2)在线型: 主席树
传送门(大佬博客):https://www.cnblogs.com/uid001/p/10718937.html
例题: http://poj.org/problem?id=2352
题意: 二维平面n个点,求出每个点左下角的点的个数。
解析:对于点(x[i],y[i])实际让求之前满足x<=x[i]&&y<=y[i]的点,所以可以先把一维限制去掉,怎么去,题上说过给出的点先按y排序,再按x排序,所以只要统计x<=x[i]的点就行了,可以用树状数组来维护这个点集合。
代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <map>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=32011;
int n,x,y;
int sum[maxn],ans[maxn];
int lowbit(int x){
    return x&(-x);
}
void update(int x,int value){
    while(x<=32001){
        sum[x]+=value;
        x+=lowbit(x);
    }
}
int getsum(int x){
    int ans=0;
    while(x>0){
        ans+=sum[x];
        x-=lowbit(x);
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&x,&y);
        x++;//将x范围调至[1,32001]
        update(x,1);
        ans[getsum(x)-1]++;//由于不包括本身,所以水平为getsum(x)-1的数加1
    }
    for(int i=0;i<n;i++){
        printf("%d\n",ans[i]);
    }
    return 0;
}

2.查询第k大

直接上例题:

http://acm.hdu.edu.cn/showproblem.php?pid=2852
题意:q次操作
如果op为0,向数组中增加一个数x;
op为1,向数组中删除一个数x;
op为2,查询比x大的第k个数
解析:
可以开一个a数组存第x个数的个数,op为0则a[x]++;op位为1,先判断a[x]是否大于0,大于则a[x]–,否则输出No Elment!
对于查询操作,我们可以二分i,找到满足:
a[x+1]+a[x+2]+…+a[i-1]<k
a[x+1]+a[x+2]+…+a[i]>=k
i即为答案 两边同时加上a[1]+a[2]+…+a[x]
转化一下,也即是找第一个满足:a[1]+a[2]+…+a[i]>=a[1]+a[2]+…+a[x]+k的i

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <map>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=1e5+10;
int q;
int a[maxn],sum[maxn];
int lowbit(int x){
    return x&(-x);
}
void update(int x,int value){
    while(x<=maxn-10){
        sum[x]+=value;
        x+=lowbit(x);
    }
}
int getsum(int x){
    int ans=0;
    while(x>0){
        ans+=sum[x];
        x-=lowbit(x);
    }
    return ans;
}
int get_index(int v){
	int l=0,r=100000,ans=inf;
	while(l<=r){
        int m=(l+r)>>1;
        int mid=getsum(m);
        if(mid>=v){
            ans=m;
            r=m-1;
        }
        else l=m+1;
	}
	return ans;
}
int main()
{
    while(~scanf("%d",&q)){
        memset(sum,0,sizeof(sum));
        memset(a,0,sizeof(a));
        for(int i=1;i<=q;i++){
            int op,x,k;
            scanf("%d",&op);
            if(op==0){
                scanf("%d",&x);
                a[x]++;
                update(x,1);
            }
            else if(op==1){
                scanf("%d",&x);
                if(a[x]>0)a[x]--,update(x,-1);
                else puts("No Elment!");
            }
            else {
                scanf("%d%d",&x,&k);
                int p=getsum(x)+k;
                int ans=get_index(p);
                if(ans==inf)puts("Not Find!");
                else printf("%d\n",ans);
            }
        }
    }
    return 0;
}

3.优化dp

假设要我们维护一个序列 a [ ] a[] a[]的最长上升子序列;
我们知道 d p [ i ] dp[i] dp[i]为以a[i]为结尾的最长上升子序列
则状态转移方程: d p [ i ] = m a x ( d p [ j ] ) + 1 ( j < i 且 a [ j ] < a [ i ] ) dp[i]=max(dp[j])+1(j<i且a[j]<a[i]) dp[i]=max(dp[j])+1(j<ia[j]<a[i])
很容易看出每次求 d p [ i ] dp[i] dp[i]都要去前面遍历去找满足的最大 d p [ j ] dp[j] dp[j],所以普通写法复杂度为 O ( n 2 ) O(n^2) O(n2),如果我们用数据结构去维护 d p [ i ] dp[i] dp[i],每次去前面查询时间复杂度会大大减少。
根据状态转移方程可以看出,我们需要一种支持单点更新,区间查询的数据结构,线段树/树状数组都可胜任,用树状数组更简便,总时间复杂度为 O ( n l o g n ) O(nlog_n) O(nlogn)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=1e3+7;

int n,m,t;
int a[maxn],b[maxn];
int dp[maxn];
void discretization(){//对原数组离散化
    sort(b+1,b+1+n);
    int len=unique(b+1,b+1+n)-b-1;
    for(int i=1;i<=n;i++){
        a[i]=lower_bound(b+1,b+1+len,a[i])-b;
    }
}
struct BIT{
    int bit[maxn];
    inline void init(){ for(int i=1;i<=n;i++)bit[i]=0; }
    inline int lowbit(int x){ return x&(-x); }
    inline void update(int x,int val){ for(int i=x;i<=n;i+=lowbit(i)) bit[i]=max(bit[i],val); }
    inline int query(int x,int res=0){ for(int i=x;i;i-=lowbit(i))res=max(bit[i],res); return res; }
}s;
int main()
{
    while(~scanf("%d",&n)){
        for(int i=1;i<=n;i++){
            scanf("%d",a+i);
            b[i]=a[i];
        }
        discretization();
        s.init();
        int ans=0;
        for(int i=1;i<=n;i++){
            dp[i]=s.query(a[i]-1)+1;
            s.update(a[i],dp[i]);
            ans=max(ans,dp[i]);
        }
        cout<<ans<<'\n';
    }
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法: 算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法与数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值