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

树状数组

时间:2024-02-04 11:01:21浏览次数:27  
标签:树状 int lowbit sum add 数组

树状数组总结

前言

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

前置芝士

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

简介

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

支持:

  • 快速求前缀和,复杂度为 \(O(\log{n})\)。
  • 修改某一个数,复杂度为 \(O(\log{n})\)。

可以看下表:

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

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

具体实现

存储

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

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

\[n=2^{i_1}+2^{i_2}+ \dots +2^{i_k} \]

设 \(i_1>i_2> \cdots >i_k\),那么区间 \([1,n]\) 就可以被分成 \(\log{n}\) 即 \(k\) 个小区间:

\[[1,2^{i_1}] \]

\[[2^{i_1}+1,2^{i_1}+2^{i_2}] \]

\[[2^{i_1}+2^{i_2}+1,2^{i_1}+2^{i_2}+2^{i_3}] \]

\[\dots \dots \]

\[[2^{i_1}+2^{i_2}+ \dots +2^{i_{k-1}}+1,2^{i_1}+2^{i_2}+ \dots +2^{i_k}] \]

长度分别为 \(2^{i_1}\),\(2^{i_1}\),\(\dots\) ,\(2^{i_k}\)。

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

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

\[c_i=\sum_{j = i-lowbit(i)+1}^{i} a_j \]

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

img

查询

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

在上面的处理出 \(c\) 数组的基础上,将 \(\log{n}\) 个区间和相加就能得到原数组的前缀和,例如求 \(a_1\) 到 \(a_i\) 的和 \(s_i\),直接将 \(c_i\) 加入答案,\(c_i\) 表示 \([i-lowbit(i)+1,i]\) 区间内的和,所以答案还需加上 \(a_1\) 到 \(a_{i-lowbit(i)}\) 的和,也就是 \(s_{i-lowbit(i)}\),再加上 \(c_{i-lowbit(i)}\) 一直操作,最后到 \(s_0\),就得到答案了。例如查询 \(s_7\),\(s_7=c_7+c_6+c_4\)。

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(n\log{n})\)。

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\),只需要将 \(c_l\) 加上 \(x\),\(c_{r+1}\) 减去 \(x\)。
  • 单点查询:查询 \(a_x\) 就是查 \([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\) 为差分数组,则有:

\[a_1+a_2+a_3+\dots+a_x=\sum_{i = 1}^{x} a_i=\sum_{i = 1}^{x}\sum_{j = 1}^{i} b_j \]

注意看 \(\sum_{i = 1}^{x}\sum_{j = 1}^{i} b_j\),将其展开来:

\[\sum_{i = 1}^{x}\sum_{j = 1}^{i} b_j=b_1+(b_1+b_2)+(b_1+b_2+b_3)+\dots+(b_1+b_2+b_3+\dots+b_x) \]

放到图中来看:

img

黑色数据相加就是所需,将其补全,整个表就是 \((x+1) \times (b_1+b_2+b_3+b_4+ \dots +b_x)\),可以看作前缀和。红色的竖着来看,就是 \(b_1+2 \times b_2+3 \times b_3+ \dots +x \times b_x\),其实也是个前缀和,\(i \times b_i\)。总的来说:

\[\sum_{i = 1}^{x}\sum_{j = 1}^{i} b_j=\sum_{i = 1}^{x} {b_i \times (x+1)}-\sum_{i = 1}^{x} {b_i \times i} \]

也就是说我们要维护两个前缀和,\(b_i\) 和 \(b_i \times 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

分析

首先数据跨度大,可以离散化,离散化后用树状数组维护权值数组。以离散化后的值为下标,存贮出现的数量。\(s_i\) 表示小于等于 \(i\) 的数的个数,离散化后的值为 \(k\),从 \(1\) 到 \(n\) 遍历 \(a\) 数组,将 \(ans\) 加上 \(s_{k-1}\), \(a_k\) 加上 \(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. 树状数组维护的是前缀和数组,差分数组还是权值数组。

标签:树状,int,lowbit,sum,add,数组
From: https://www.cnblogs.com/zhouruoheng/p/18003298

相关文章

  • 寻找两个有序数组的中位数(4)
    4MedianofTwoSortedArrays问题描述:给定两个大小为m和n的有序数组nums1和nums2。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为O(log(m+n))。你可以假设nums1和nums2不会同时为空。示例1:nums1=[1,3]nums2=[2]则中位数是2.0示例2:n......
  • 两数之和-输出有序数组
    167TwoSumII-Inputarrayissorted问题描述:给定一个已按照升序排列的有序数组,找到两个数使得它们相加之和等于目标数。函数应该返回这两个下标值index1和index2,其中index1必须小于index2。说明:返回的下标值(index1和index2)不是从零开始的。你可以假设每个输入......
  • 合并两个有序数组
    88.合并两个有序数组问题描述:给定两个有序整数数组nums1和nums2,将nums2合并到nums1中,使得num1成为一个有序数组。说明:初始化nums1和nums2的元素数量分别为m和n。你可以假设nums1有足够的空间(空间大小大于或等于m+n)来保存nums2中的元素。示例:输......
  • 根据筛法规则对整数分类,建立树状结构
    筛法目前一般用来找整数序列中的素数,不是素数的元素被丢掉了。如果仅把筛法当成一种分类规则,把筛掉的元素和留下的元素算作不同的分类,并用每一类中的最小元素递归地执行筛法,那么能把所有正整数保留下来,并建立一个树状结构。例如,初始集合是正整数集,根据模最小元素p是否为0,可把所有......
  • 获取数组中元素的所有组合方式
    代码/***获取words成员的所有组合方式*@param{(string|number)[]}words*@return{(string|number)[][]}*/functioncombine(words){constlist=[]words.forEach((word,idx)=>{constrestWords=[...words.slice(0,idx),...words.slice(......
  • 「KDOI-03」构造数组
    「KDOI-03」构造数组题目描述你现在有一个长度为\(n\)的数组\(a\)。一开始,所有\(a_i\)均为\(0\)。给出一个同样长度为\(n\)的目标数组\(b\)。求有多少种方案,使得通过若干次以下操作,可以让\(a\)数组变成\(b\)。选出两个不同的下标\(1\leqi<j\leqn\),并将\(a_i\)......
  • 2024-02-03:用go语言,你有 k 个背包。给你一个下标从 0 开始的整数数组 weights, 其中 we
    2024-02-03:用go语言,你有k个背包。给你一个下标从0开始的整数数组weights,其中weights[i]是第i个珠子的重量。同时给你整数k,请你按照如下规则将所有的珠子放进k个背包。没有背包是空的。如果第i个珠子和第j个珠子在同一个背包里,那么下标在i到j之间的所有珠......
  • 数组的中心位置-od-python
    数组的中心位置时间限制:1s空间限制:256MB限定语言:不限题目描述:给你一个整数数组nums,请计算数组的中心位置。数组中心位置是数组的一个下标,其左侧所有元素相乘的积等于右侧所有元素相乘的积。数组第一个元素的左侧积为1,最后一个元素的右侧积为1如果数组有多个中心位置,应该返回......
  • 力扣 34. 在排序数组中查找元素的第一个和最后一个位置
    Problem: 34.在排序数组中查找元素的第一个和最后一个位置思路找到大于等于target的下标,然后遍历之后的数组,找到最后的下标。classSolution{public:intf(vector<int>&nums,inttarget){intl=0,r=nums.size()-1;intmid=floor(l+(r-l)*1.0/2);......
  • flat 拍平数组 手写flat拍平数组
    //flat拍平一维数组letflaoatArr=[1,3,5,6,3,6,[3,46,465,3]]letres=flat(flaoatArr)console.log(res);letres=flaoatArr.flat()console.log(res);//手写float数组Array.protot......