树状数组

树状数组总结

前言

树状数组是数据结构中的一股清流,代码简洁,思路清晰,又好理解 qwq。

前置芝士

lowbit:https://www.cnblogs.com/zhouruoheng/p/18003331

简介

树状数组是一种基于 lowbit 的用于维护 n 个数前缀和信息的数据结构。

支持:

  • 快速求前缀和,复杂度为 O(logn)
  • 修改某一个数,复杂度为 O(logn)

可以看下表:

数组 前缀和数组 树状数组 线段树
单点修改 O(1) O(n) O(logn) O(logn)
查询 O(n) O(1) O(logn) O(logn)

树状数组将各操作优化成 O(logn),且比线段树好写得多,能处理许多线段树能写的问题。

具体实现

存储

树状数组具体实现和 st 表差不多,尤其是区间划分。

首先,一个数可以用二进制表示:

n=2i1+2i2++2ik

i1>i2>>ik,那么区间 [1,n] 就可以被分成 lognk 个小区间:

[1,2i1]

[2i1+1,2i1+2i2]

[2i1+2i2+1,2i1+2i2+2i3]

[2i1+2i2++2ik1+1,2i1+2i2++2ik]

长度分别为 2i12i12ik

它们有个共同特点:若区间右端点为 x,则区间长度就是 lowbit(x)

那么设存有 n 个数的数组 a,用树状数组使用 c 来储存。

ci=j=ilowbit(i)+1iaj

ci 表示 [ilowbit(i)+1,i] 区间内的和,即将 i 作为右端点,长度为 lowbit(i) 的区间。数组 c 可以看作一个树形结构。
如下图:

img

查询

进行查询前缀和那么就要将原区间分解成若干小区间,就和分解正整数那样,然后相加。

在上面的处理出 c 数组的基础上,将 logn 个区间和相加就能得到原数组的前缀和,例如求 a1ai 的和 si,直接将 ci 加入答案,ci 表示 [ilowbit(i)+1,i] 区间内的和,所以答案还需加上 a1ailowbit(i) 的和,也就是 silowbit(i),再加上 cilowbit(i) 一直操作,最后到 s0,就得到答案了。例如查询 s7s7=c7+c6+c4

img

code:

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

修改

既然要进行单点修改,那么儿子一定会影响其父亲,因此我们必须修改所以有影响的点。由图可知,每个点都只有一个父亲,只需要一直往父亲那走就好了。那么父节点和子节点有什么关系呢?

显而易见,节点 x 的父节点是 x+lowbit(x),若是有闲功夫证明的话也可以证明下。

所以只要一直往父节点去,然后进行修改就可以了。

img

// 将a[x]加上y
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}

初始化

有了修改操作后,那么就可以非常简单的进行初始化,将 a 数组中的值一次加入到树状数组中即可,复杂度为 O(nlogn)

void init()
{
    for(int i=1;i<=n;i++) add(i,a[i]);
}

初始化还有一种更快的方法,时间复杂度为线性。先处理出前缀和,再直接给 c 数组赋值。不过一般情况下没有必要,用上面的就好。

int b[N];//a 的前缀和

void init()
{
    for(int i=1;i<=n;i++)
    {
        b[i]=b[i-1]+a[i];
        c[i]=b[i]-b[i-lowbit(i)];//[i-lowbit(i)-1,i]
    }
}

例1 P3374 【模板】树状数组 1

https://www.luogu.com.cn/problem/P3374

分析

将上面所讲的内容结合一下即可。

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+5;

int n,m,a[N],c[N];

int lowbit(int x)
{
    return x&-x;
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}
int sum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=c[i];
    return ans;
}
int main ()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        add(i,a[i]);
    }
    while(m--)
    {
        int op,x,y;
        cin>>op>>x>>y;
        if(op==1) add(x,y);
        else cout<<sum(y)-sum(x-1)<<"\n";
    }
    return 0;
}

例2 P3368 【模板】树状数组 2

https://www.luogu.com.cn/problem/P3368

分析

区间修改和单点查询,就像用普通数组一样思考,显然,差分数组就能轻易做到区间修改。用树状数组维护差分数组,就能轻易做到区间修改和单点查询。

  • 区间修改:例如将 [l,r] 的值加上 x,只需要将 cl 加上 xcr+1 减去 x
  • 单点查询:查询 ax 就是查 [1,x] 的和,直接求就好了

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+5;

int n,m,a[N],c[N];

int lowbit(int x)
{
    return x&-x;
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}
int sum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=c[i];
    return ans;
}
int main ()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i],add(i,a[i]-a[i-1]);
    while(m--)
    {
        int op,x,y,k;
        cin>>op;
        if(op==1) 
        {
            cin>>x>>y>>k;
            add(x,k);
            add(y+1,-k);
        }
        else 
        {
            cin>>x;
            cout<<sum(x)<<"\n";
        }
    }
    return 0;
}

例3

题目描述

既然已经知道了如何单点修改区间查询以及区间修改和单点查询,那么能不能做到区间修改区间查询呢?

分析

首先肯定是可以的,且复杂度能比数组更优,只需要稍微使用一点点技巧。还是维护差分数组,区间修改还是那样,主要思考区间查询。a 为原数组,b 为差分数组,则有:

a1+a2+a3++ax=i=1xai=i=1xj=1ibj

注意看 i=1xj=1ibj,将其展开来:

i=1xj=1ibj=b1+(b1+b2)+(b1+b2+b3)++(b1+b2+b3++bx)

放到图中来看:

img

黑色数据相加就是所需,将其补全,整个表就是 (x+1)×(b1+b2+b3+b4++bx),可以看作前缀和。红色的竖着来看,就是 b1+2×b2+3×b3++x×bx,其实也是个前缀和,i×bi。总的来说:

i=1xj=1ibj=i=1xbi×(x+1)i=1xbi×i

也就是说我们要维护两个前缀和,bibi×i

code

code 不保证都正确。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;

int n,m,a[N],c1[N],c2[N];

int lowbit(int x)
{
    return x&-x;
}
void add(int c[],int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}
int sum(int c[],int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=c[i];
    return ans;
}
int query(int x)
{
    return sum(c1,x)*(x+1)-sum(c2,x);
}
int main ()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        int b=a[i]-a[i-1];
        add(c1,i,b);
        add(c2,i,b*i);
    }
    while(m--)
    {
        int op,l,r,x;
        cin>>op>>l>>r;
        if(op==1) 
        {
            cin>>x;
            add(c1,l,x),add(c1,r+1,-x);
            add(c2,l,l*x),add(c2,r+1,(r+1)*(-x));
        }
        else cout<<query(r)-query(l-1)<<"\n";
    }
    return 0;
}

例4 P1908 逆序对

https://www.luogu.com.cn/problem/P1908

分析

首先数据跨度大,可以离散化,离散化后用树状数组维护权值数组。以离散化后的值为下标,存贮出现的数量。si 表示小于等于 i 的数的个数,离散化后的值为 k,从 1n 遍历 a 数组,将 ans 加上 sk1ak 加上 1

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+5;

int n,m,a[N],b[N],c[N];

int lowbit(int x)
{
    return x&-x;
}
void add(int x,int y)
{
    for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}
int sum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=c[i];
    return ans;
}

int main ()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
    sort(b+1,b+n+1);
    m=unique(b+1,b+n+1)-b-1;
    ll ans=0;
    for(int i=n;i;i--)
    {
        int k=lower_bound(b+1,b+m+1,a[i])-b;
        ans+=sum(k-1);
        add(k,1);
    }
    cout<<ans<<"\n";
    return 0;
}

练习

  1. P5677 [GZOI2017] 配对统计
  2. P1966 [NOIP2013 提高组] 火柴排队
  3. P2161 [SHOI2009] 会场预约

tips

  1. 注意前缀和的范围,要不要开 long long
  2. 树状数组维护的是前缀和数组,差分数组还是权值数组。
posted @   zhouruoheng  阅读(33)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示