树状数组总结

树状数组

(一)树状数组基础

一、二进制拆分

我们知道,对于任意的非负整数 n ,可表示为:

n=2i1+2i2++2im

不妨设 i1>i2>>im ,进一步地,区间 [1,x] 可以分成 O(log2x) 个小区间:

  1. 长度为 2i1 的小区间 [1,2i1]

  2. 长度为 2i2 的小区间 [2i1+1,2i1+2i2]

  3. 长度为 2i3 的小区间 [2i1+2i2+1,2i1+2i2+2i3]

m. 长度为 2im 的小区间 [2i1+2i2++2im1+1,2i1+2i2+2i3++2im]

一个不难发现的规律是,若区间设为 [L,R]

那么区间长度等于 R 的 “二进制拆分” 结果的 2i1,即 lowbit(R)

如区间 [1,7] 可分成 [1,4] , [5,6][7,7] 三个小区间,

长度分别为 lowbit(4)=4lowbit(6)=2lowbit(7)=1

二、树状数组

树状数组就是一种基于 “二进制拆分” 思想的数据结构,我们建立一个数组 c

其中 ci 保存要维护的原数组 a 的区间[xlowbit(x)+1,x] 中所有数的和,

j=xlowbit(x)+1xA[i]

事实上,数组 c 可以看做如下图的一个树形结构,

最下面一行为 N 个叶节点,代表数值 a[in]

性质:

  1. 每个内部节点 c[x] 保存以它为根的子树中所有叶节点的和;
  2. 每个内部节点 c[x] 的子节点个数等于 lowbit(x) 的位数;
  3. 除树根外,每个内部节点 c[i] 的父节点是 c[x+lowbit(x)]
  4. 树的深度为 O(logN)

查询前缀和 O(logN)

int ask(int x)
{
    int ans=0;
    while(x)
    {
	ans+=c[x];
	x-=lowbit(x);
    }
    return ans;
}

单点增加 O(logN)

void add(int x,int y)
{
    while(x<=n)
    {
	c[x]+=y;
	x+=lowbit(x);
    }
}

初始化:

1. 简便写法 O(NlogN)
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++)
    add(i,a[i]);
2. 高效写法 O(N)

从小大依次考虑每个节点 x ,借助 lowbit 运算扫描它的子叶子并求和,树形结构中的每条边只会被遍历一次

for(int i=1;i<=n;i++)
    s[i]=s[i-1]+a[i];
for(int i=1;i<=n;i++)
    c[i]=s[i]-s[i-lowbit(i)];

时间复杂度为:

O(k=1logNk×N2k)=O(N)

Example: 单点修改,区间查询

输入一个数列 a1,a2,,an(1n100000)

在数列上进行 m(1m100000) 次操作,操作有以下两种:

  1. 格式为 C I X,其中 C 为字符 "C ",IX(1In,|X|10000) 都是整数,表示把把 a[I] 改为 X

  2. 格式为 Q L R,其中 Q 为字符 "Q ",LR 表示询问区间为 [L,R](1LRn),表示询问 A[L]++A[R] 的值。

for(int i=1;i<=n;i++)
{
    int a;
    scanf("%d",&a);
    add(i,a);
}
for(int i=1;i<=m;i++)
{
    char s;
    int b,d;
    scanf("\n%c %d%d",&s,&b,&d);
    if(s=='C') add(b,d-(ask(b)-ask(b-1)));
    else printf("%d\n",ask(d)-ask(b-1));
}

(二)树状数组拓展

一、树状数组与逆序对

任意给定一个集合 a,如果用 t[val] 存储 val 在集合 a 中出现的次数,

那么数组 t[l,r] 上的区间和(即 i=lrt[i])就表示集合 a[l,r] 中的数有多少个。

因此,我们可以在集合 a数值范围上建立一个树状数组,维护 t 的前缀和。

这样就可以高效地统计在集合 a 中插入或删除一个数。

Example1: 逆序对统计 Luogu P1908

给定一个整数序列 a1,a2,,an,如果存在 i<j 并且 ai>aj ,那么我们称之为逆序对。

求逆序对的数目。

利用上述思路,我们可以利用树状数组求出逆序对个数,解法如下:

  1. 在序列 a数值范围上建立树状数组 t,初值为 0

  2. 倒序扫描序列 a,对于每个数 a[i]

    (1) 在 t 中查询 1a[i]1 的前缀和,累加到 ans 中;

    (2) 把位置为 a[i] 的数加 1(即 t[a[i]]+1),表示数值 a[i] 又出现了一次。

  3. ans 即为所求。

for(int i=n;i>=1;i--)
{
    ans+=ask(a[i]-1);
    add(a[i],1);
}
printf("%lld\n",ans);

Tips:

因为倒序扫描,“已经出现过的数” 就是在 a[i] 后面的数。

这个数 x[i,a[i]1](即比 a[i] 小),又在 a[i] 后面,满足逆序对定义。

Example2: 楼兰图腾 AcWing241

平面上有 N(N105)个点,每个点的横、纵坐标的范围都是 1N,任意两个点的横、纵坐标都不相同。

若三个点 (x1,y1),(x2,y2),(x3,y3) 满足 x1<x2<x3,y1>y2 并且 y3>y2,则称这三个点构成 "V" 字图腾。

若三个点 (x1,y1),(x2,y2),(x3,y3) 满足 x1<x2<x3,y1<y2 并且 y3<y2,则称这三个点构成 "A" 字图腾。

求平面上 "V" 和 "A" 字图腾的个数。

  1. n 个点排序,记序列为 a
  2. 倒序扫描 a,求 a[i] 后面有几个数比它大,记为 ans1[i]
  3. 正序扫描 a,求 a[i] 前面有几个数比它大,记为 ans2[i]
  4. i 为中心的 “V” 字图腾的个数为 ans1[i]×ans2[i],“V” 字图腾的总数则为:

sum1=i=1Nans1[i]×ans2[i]

同理,可以统计出 “A” 字图腾的总数。

for(int i=1;i<=n;i++)
{
    ans4[i]=ask(a[i]-1);
    ans2[i]=ask(n)-ask(a[i]);
    add(a[i],1);
}
memset(c,0,sizeof(c));
for(int i=n;i>=1;i--)
{
    ans3[i]=ask(a[i]-1);
    ans1[i]=ask(n)-ask(a[i]);
    add(a[i],1);
}
for(int i=1;i<=n;i++)
{
    res1+=ans1[i]*ans2[i];
    res2+=ans3[i]*ans4[i];
}
printf("%lld %lld\n",res1,res2);

二、树状数组拓展

1. 区间修改和单点查询

  • 区间修改

利用前缀和思想,对区间 [l,r] 中的每一个值增加 k

可以使 a[l]+k,a[r+1]k,就可以达到区间修改的效果了

int l,r,k;
scanf("%d%d%d",&l,&r,&k);
add(l,k);
add(r+1,-k);
  • 单点查询

利用前缀和思想,求出前 x 个数的前缀和

再加上第 x 个数的值 a[x],就可以达到单点查询的效果了

int x;
scanf("%d",&x);
printf("%lld\n",ask(x)+a[x]);
Example: 区间修改单点查询 Luogu P3368

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 x
  2. 求出某一个数的值。
for(int i=1;i<=m;i++)
{
    int a1,x,y,k;
    scanf("%d",&a1);
    if(a1==1) 
    {
        scanf("%d%d%d",&x,&y,&k);
        add(x,k);
        add(y+1,-k);
    }
    else
    {
        scanf("%d",&x);
        printf("%lld\n",ask(x)+a[x]);
    }
}

2. 区间修改和区间查询

我们已知单点查询求出前 x 个数的前缀和

那么对于区间查询,统计数每个数的前缀和即可,我们假设前缀和数组为 b

序列 a 的前缀和 a[1x] 整体增加就是:i=1xj=1ib[j]

上式可以改写为:

i=1xj=1ib[j]=i=1x(xi+1)×b[i]=(x+1)i=1xb[i]i=1xi×b[i]

因此,我们可以建立两个树状数组 c0c1

c0 用来维护 b[i] 的前缀和,c1 用来维护 i×b[i] 的前缀和。

void add(int k,int x,int y)
{
    while(x<=n)
    {
	c[k][x]+=y;
	x+=lowbit(x);
    }
}
LL ask(int k,int x)
{
    LL ans=0;
    while(x>0)
    {
	ans+=c[k][x];
	x-=lowbit(x);
    }
    return ans;
}
  • 区间修改

利用前缀和思想,进行如下操作:

  1. 在树状数组 c0 中,把位置 x 上的数加 k
  2. 在树状数组 c0 中,把位置 y+1 上的数减 k
  3. 在树状数组 c1 中,把位置 x 上的数加 x×k
  4. 在树状数组 c1 中,把位置 y+1 上的数减 (y+1)×k
int x,y,k;
scanf("%d%d%d",&x,&y,&k);
add(0,x,k);
add(0,y+1,-k);
add(1,x,x*k);
add(1,y+1,-(y+1)*k);
  • 区间查询

利用前缀和思想,我们可以将答案拆分成 1x11y 两个部分

两者相减,即为区间和,式子如下:

(sum[y]+(y+1)ask(0,y)ask(1,y))(sum[x1]+xask(0,x1)ask(1,x1))

LL ans1,ans2;
ans1=sum[x-1]+x*ask(0,x-1)-ask(1,x-1);
ans2=sum[y]+(y+1)*ask(0,y)-ask(1,y);
printf("%lld\n",ans2-ans1);
Example 1: A Simple Problem with Integers AcWing243 一个简单的整数问题2

给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:

  1. C l r d,表示把 A[l],A[l+1],,A[r] 都加上 d
  2. Q l r,表示询问数列中第 lr 个数的和。

对于每个询问,输出一个整数表示答案。

for(int i=1;i<=n;i++)
    sum[i]=sum[i-1]+a[i];
for(int i=1;i<=q;i++)
{
    char a1;
    int x,y,k;
    scanf("\n%c %d%d",&a1,&x,&y);
    if(a1=='C')
    {
        scanf("%d",&k);
        add(0,x,k);
        add(0,y+1,-k);
        add(1,x,x*k);
        add(1,y+1,-(y+1)*k);
    }
    else
    {
        LL ans1,ans2;
        ans1=sum[x-1]+x*ask(0,x-1)-ask(1,x-1);
        ans2=sum[y]+(y+1)*ask(0,y)-ask(1,y);
        printf("%lld\n",ans2-ans1);
    }
}
Example 2: Lost Cows AcWing244 谜一样的牛
Exercise: [SHOI2007]园丁的烦恼 Luogu P2163
posted @   Lan_Sky  阅读(220)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示