基础的树状数组操作

目录

动态求连续区间和

所以用到另一种方法——树状数组

另一种方法——线段树

完整线段树代码

数列区间最大值

分析: 

具体实现:

数星星

小朋友排队

 分析:

代码实现(树状数组): 

一个简单的整数问题2

 改进

代码实现


动态求连续区间和

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b][a,b] 的连续和。

输入格式

第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 1 开始计数。

输出格式

输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围

1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

输出样例:

11
30
35

 1264. 动态求连续区间和 - AcWing题库

对于求区间问题,最先想到的应该是前缀和

  • 建立一个前缀和数组
  • 区间查询:O(1)
  • 元素修改:O(n)

 显然元素修改可能存在过于复杂的情况


所以用到另一种方法——树状数组

  • 区间查询:O(log n)
  • 元素修改:O(log n)

在讲具体算法前,要先介绍一个具体的操作

  • lowbit运算——找出一个数二进制的最低位1(例如9:1001   ->  1    12:1100   ->   100)
  1. 设数为x,x按位取反,设为~x
  2. x 按位与 ~x为0,当x按位与(~x+1)时,得到结果
  3. 计算机内的数字按照补码规则,负数的补码与对应正数的反码加一相等,所以简化为x & -x
  • 以lowbit运算为基础,构建一个树状数组
  1. 元素修改

    void add(int x,int w){
        for(int i = x;i <= n;i += lowbit(i)){
            t[i] += w;
        }
    }
  2. 区间查询

    int query(int u){
        int res = 0;
        for(int i = u;i > 0;i -= lowbit(i)){
            res += t[i];
        }
        return res;
    }

 实现这两个功能之后,就可以写出代码了

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int t[N];
int n,m;

int lowbit(int x)  // 返回末尾的1
{
    return x & -x;                               //正数的反码加一等于补码,即对应负数
}

void add(int x,int w){
    for(int i = x;i <= n;i += lowbit(i)){        //修改x~n之内x的父节点的值
        t[i] += w;
    }
}

int query(int u){
    int res = 0;
    for(int i = u;i > 0;i -= lowbit(i)){         //找到u之前所有的子节点,输出累加和
        res += t[i];
    }
    return res;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ){
        int t;cin >> t;
        add(i,t);                                 //初始化数组
    }
    for(int i = 1;i <= m;i ++){
        int k,a,b;cin >> k >> a >> b;
        if(k == 1){
            add(a,b);                             //元素修改
        }else{
            cout << query(b) - query(a-1) << endl;//区间查询(以前缀和为基础)
        }
    }
    return 0;
}

另一种方法——线段树

  •  线段树的本质是一个二叉树,节点上存储的是两个端点和一些属性,所以像线段一样
    struct node{
        int l,r,sum;//这里存放线段上的元素和
    }nodes[N*4];
    

    像普通二叉树一样,对于节点x

  1. 左子节点  x*2     x << 1

  2. 右子节点  x*2+1     x << 1 | 1

  • 所以可以通过递归的方式,利用给定的一个数组,建立一个二叉树     build(1,1,n) 
    void pushup(int u){
        nodes[u].sum = nodes[u << 1].sum + nodes[u << 1 | 1].sum;
    }
    
    void build(int u,int l,int r){
        if(l == r)nodes[u] = {l,r,a[r]};                          //结束:长度为1的线段作为终点
        else{
            nodes[u] = {l,r};
            int mid = l + r >> 1;
            build(u << 1,l,mid);                                  //左节点递归
            build(u << 1 | 1,mid + 1,r);                          //右节点递归
            pushup(u);                                            //传递值
        }
    }
  • 元素修改——递归查找到目标长度为1的节点,修改该节点的sum值,再回溯修改其母节点

    void change(int u,int x,int w){
        if(nodes[u].l == nodes[u].r)nodes[u].sum += w;//找到目标节点
        else{
            int mid = nodes[u].l + nodes[u].r >> 1;   
            if(mid >= x)add(u << 1,x,w);              //类似于二分查找
            else add(u << 1 | 1,x,w); 
            pushup(u);                                //递归回溯修改母节点
        }
    }
     

  •  区间查询
  1. 设带查找区间为[ L , R ],如果当前节点的两端点包含于[ L , R ],返回当前节点的sum值
  2. 如果当前节点有在[ L , R ]之外的部分,需要判断
    mid = (L + R)/ 2

    如果当前节点的区间有在mid左边的部分,就需要递归查询左节点;如果当前节点的区间有在mid右边的部分,就需要递归查询右节点
    int query(int u,int l,int r){
        if(l <= nodes[u].l && nodes[u].r <= r)return nodes[u].sum;//包含于目标区间之内,返回
        int mid = nodes[u].l + nodes[u].r >> 1;
        int sum = 0;
        if(mid >= l)sum += query(u << 1,l,r);                     //递归左子节点
        if(mid < r)sum += query(u << 1 | 1,l,r);                  //递归右子节点
        return sum;                                               
    }

完整线段树代码

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1e5+10;
int a[N];
int n,m;
struct node{
    int l,r,sum;
}nodes[N*4];

void pushup(int u){
    nodes[u].sum = nodes[u << 1].sum + nodes[u << 1 | 1].sum;
}

void build(int u,int l,int r){
    if(l == r)nodes[u] = {l,r,a[r]};
    else{
        nodes[u] = {l,r};
        int mid = l + r >> 1;
        build(u << 1,l,mid);
        build(u << 1 | 1,mid + 1,r);
        pushup(u);
    }
}

int query(int u,int l,int r){
    if(l <= nodes[u].l && nodes[u].r <= r)return nodes[u].sum;
    int mid = nodes[u].l + nodes[u].r >> 1;
    int sum = 0;
    if(mid >= l)sum += query(u << 1,l,r);
    if(mid < r)sum += query(u << 1 | 1,l,r);
    return sum;
}

void add(int u,int x,int w){
    if(nodes[u].l == nodes[u].r)nodes[u].sum += w;
    else{
        int mid = nodes[u].l + nodes[u].r >> 1;
        if(mid >= x)add(u << 1,x,w);
        else add(u << 1 | 1,x,w);
        pushup(u);
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )cin >> a[i];
    
    build(1,1,n);
    
    int k,a,b;
    for(int i = 0;i < m; i++){
        cin >> k >> a >> b;
        if(k){
            add(1,a,b);
        }else{
            printf("%d\n",query(1,a,b));
        }
    }
}

另一道与线段树相关的题目:1270. 数列区间最大值 - AcWing题库

数列区间最大值

输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y要求你说出 X 到 Y 这段区间内的最大数。

输入格式

第一行两个整数 N,M 表示数字的个数和要询问的次数;

接下来一行为 N 个数;

接下来 M 行,每行都有两个整数 X,Y。

输出格式

输出共 M 行,每行输出一个数。

数据范围

1≤N≤100000,
1≤M≤1000000,
1≤X≤Y≤N,
数列中的数字均不超过2^31-1

输入样例:

10 2
3 2 4 5 6 8 1 2 9 7
1 4
3 8

输出样例:

5
8

分析: 

这个题目和  线段树区间查询、元素修改  基本方法差不多,都是线段树
只需要把节点的 区间和 属性改成 区间最大值 即可

struct node{
    int l,r,maxx;//区间最大值
}nodes[N*4];

具体实现:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <climits>  //使用INT_MIN需要添加这个库函数
                    //这里找出的最大值可能有负数
using namespace std;

const int N = 1e5+10;
int a[N];
int n,m;
struct node{
    int l,r,maxx;
}nodes[N*4];

void pushup(int u){
    nodes[u].maxx = max(nodes[u << 1].maxx , nodes[u << 1 | 1].maxx);//修改了“属性”计算方法
}

void build(int u,int l,int r){//初始化
    if(l == r)nodes[u] = {l,r,a[r]};
    else{
        nodes[u] = {l,r};
        int mid = l + r >> 1;
        build(u << 1,l,mid);
        build(u << 1 | 1,mid + 1,r);
        pushup(u);
    }
}

int query(int u,int l,int r){//查询方法
    if(l <= nodes[u].l && nodes[u].r <= r)return nodes[u].maxx;
    int mid = nodes[u].l + nodes[u].r >> 1;
    int maxx = INT_MIN;
    if(mid >= l)maxx = max(maxx,query(u << 1,l,r));
    if(mid < r)maxx = max(maxx,query(u << 1 | 1,l,r));
    return maxx;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )scanf("%d",a+i);
    
    build(1,1,n);
    
    int x,y;
    for(int i = 0;i < m; i++){
        scanf("%d%d", &x, &y);
        printf("%d\n",query(1,x,y));
    }
}

稍微难理解的一道题:1265. 数星星 - AcWing题库

数星星

天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。

如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。

例如,上图中星星 5 是 3 级的(1,2,4 在它左下),星星 2,4是 1 级的。

例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。

给定星星的位置,输出各级星星的数目。

换句话说,给定 N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。

输入格式

第一行一个整数 N,表示星星的数目;

接下来 N 行给出每颗星星的坐标,坐标用两个整数 x,y 表示;

不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。

输出格式

N 行,每行一个整数,分别是 0 级,1 级,2 级,……,N−1 级的星星的数目。

数据范围

1≤N≤15000,
0≤x,y≤32000

输入样例:

5
1 1
5 1
7 1
3 3
5 5

输出样例:

1
2
1
1
0

 分析

  1. 从题目里可得,星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出,那么每个星星左下角有多少个星星就与后续输入坐标无关了
  2. 星星坐标从 0 开始,可以调整一下,自加 1 ,然后建立一个树状数组
  3. 设输入星星横坐标 x ,他的左下角星星数量在树状数组里只等于1~x内有多少个星星,等价于区间查询操作
  4. 对星星的 x 位置元素修改,+1


#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int a[N],cnt[N];
int n,m;

int lowbit(int x)
{
    return x & (-x);
}

void add(int x,int w){
    for(int i = x;i < N;i += lowbit(i)){//这里读题要注意:坐标并没有在n以内,而是在整个的最大范围内
        a[i] += w;
    }
}

int query(int u){
    int res = 0;
    for(int i = u;i > 0;i -= lowbit(i)){
        res += a[i];
    }
    return res;
}

int main()
{
    cin >> n;
    for(int i = 1;i <= n;i ++){         //要实时计算星星的等级,因为一旦y是递增排序的,所以后来的x不会对之前产生影响
        int a,b;cin >> a >> b;
        add(a+1,1);                     //跳过0,因为这个树状数组以0为结束终点
        cnt[query(a+1)]++;              //前面有多少个星星,就是多少级
    }
    
    for (int i = 1; i <= n; i ++ ){     //没有0
        cout << cnt[i] << endl;         //输出各各等级的数量
    }
    return 0;
}

第五届蓝桥杯省赛C++B/C组 1215. 小朋友排队 - AcWing题库

小朋友排队

n 个小朋友站成一排。

现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。

每个小朋友都有一个不高兴的程度。

开始的时候,所有小朋友的不高兴程度都是 0。

如果某个小朋友第一次被要求交换,则他的不高兴程度增加 1,如果第二次要求他交换,则他的不高兴程度增加 2(即不高兴程度为 3),依次类推。当要求某个小朋友第 kk 次交换时,他的不高兴程度增加 k。

请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。

如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。

输入格式

输入的第一行包含一个整数 n,表示小朋友的个数。

第二行包含 n 个整数 H1,H2,…,Hn,分别表示每个小朋友的身高。

输出格式

输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。

数据范围

1≤n≤100000,
0≤Hi≤1000000

输入样例:

3
3 2 1

输出样例:

9

样例解释

首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。

 分析:

  1. 问题简化:一个有 n 个元素的数组H,以冒泡排序的方式排成升序,每个元素交换(大小相同不交换)的次数cnt[ i ],最后累加cnt[ i ] * cnt [ i ] / 2
  2. 每个元素要交换的的次数,就是在数组中关于当前元素的逆序对数量
    交换次数cnt[ i ] = H[ 0 , i - 1 ]中大于H[ i ]的数量 + H[ i+1 , n ]中小于H[ i ]的数量
     
  3. 为什么是逆序对呢?可以从反面来想。(递推)
    假设 H数组 已经升序,那么任意移动一个元素,计算出来的逆序对数量是符合上述定义的;
    如果再移动另一个,也可以推理得出逆序对数量是符合的
    如此反复的操作可以得出,逆序对数量的计算是正确的
     
  4. 经过上述的分析,我们可以得到暴力做法(显然会超时)
    for(int i=1;i<=n;i++){
        long long res=0;
        for(int j=1;j<i;j++)
            if(a[j]>a[i]) res++;// i 前面比 a[i]高的数量
    
        for(int j=i+1;j<=n;j++)
            if(a[j]<a[i]) res++;// i 后面比 a[i]低的数量
    
        res=res*res/2;          //不高兴值
        num[i]=res;
    }                           //计算总和
    for(int i=1;i<=n;i++) ans+=num[i];
    

代码实现(树状数组): 

//每个小孩的移动次数=前面比它高的人数+后面比他低的人数
//不高兴值就是累加1到移动次数=(移动次数)*(移动次数+1)/2

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10,M = 1e6 + 10;

typedef long long LL;

int n;
int a[N],h[N];
int cnt[M];

int lowbit(int x)  // 返回末尾的1
{
    return x & -x;
}

void add(int u){
    for(int i = u ;i < M;i += lowbit(i)){
        cnt[i] += 1;
    }
}

int query(int u){
    int ans = 0;
    for(int i = u; i > 0;i -= lowbit(i)){
        ans += cnt[i];
    }
    return ans;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )scanf("%d",a+i),a[i]++;
    
    for(int i = 1;i <= n;i ++){          //注意顺序
        h[i] += query(M-1) - query(a[i]);//找前面放进来的,比他高的人数
        //cout << h[i] << endl;
        add(a[i]);                       //放入
    }
    memset(cnt,0,sizeof cnt);            //清空
    for(int i = n;i >= 1;i --){
        h[i] += query(a[i]-1);           //找后面放进来的,比他矮的人数
        
        add(a[i]);
    }
    
    LL ans = 0;                          //范围最大n*n
    for (int i = 1; i <= n; i ++ )ans += (LL)(h[i] + 1) * h[i]/2;
    cout << ans << endl;
    return 0;
}

前面的问题都是围绕着区间查询元素修改进行的,用树状数组线段树两种方法来解决

元素查询可以通过查询区间长度为1的情况完成

那么对于区间修改该怎么做呢?

243. 一个简单的整数问题2 - AcWing题库下面个问题就是关于区间修改的

一个简单的整数问题2

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

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

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

输入格式

第一行两个整数 N,M。

第二行 N 个整数 A[i]。

接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。

输出格式

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

每个答案占一行。

数据范围

1≤N,M≤100000
|d|≤10000,
|A[i]|≤1e9

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

输出样例:

4
55
9
15

        分析:

        有了前面的经验后,从方便的角度可以想到,运用 差分 来实现数组的区间修改效率最高

建立一个差分数组 a[ i ] = b[ i ] - b[ i - 1 ]

  1. 先是初始化
    for (LL i = 1; i <= n; i ++ ){
        cin >> b[i];
        a[i] = b[i]-b[i-1];
        add(i,a[i]);
    }
  2. 关于区间修改的部分,按照差分的思想
           区间查询照常

    for(LL i = 1;i <= m;i ++){
        char x;cin >> x;
        if(x == 'Q'){
            LL a,b;cin >> a >> b;
            cout << query(b) - query(a-1) << endl;
        }else{
            LL a,b,c;cin >> a >> b >> c;
            add(a, c);
            add(b+1, -c);
        }
    }
  3. 所以对应着query的实现也要发生改变,此时a数组对应的是差分的树状数组
    所以操作为:累加各个元素的差分和
    LL query(LL u){
        LL res = 0;
        for(LL i = u;i > 0;/*i -= lowbit(i)*/i--){   //累加各个元素的差分和
            for(LL j = i;j > 0;j -= lowbit(j)){      
                res += a[j];
            }
        }
        return res;
    }

 但是这样的操作显然效率过低——计算了大量的重复差分
 所以需要改进

a[1]
a[1]+a[2]
...
...
a[1]+a[2]+..+a[n-1]
a[1]+a[2]+...... + a[n]//转化成一个正方体


a[1]+a[2]+...... + a[n]
a[1]+a[2]+...... + a[n]
...
...(总共n+1行,在最上面添加一行)
a[1]+a[2]+...... + a[n]
a[1]+a[2]+...... + a[n]


计算公式=>
(n+1)(a[1]+a[2]+......+a[n]) - (1*a[1]+2*a[2]+......+n*a[n])

所以我们可以维护两个树状数组

a[ i ]

b[ i ] = i * a[ i ]

代码实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;

const LL N = 1e5+10;
LL a[N],b[N];
LL n,m;

LL lowbit(LL x)  // 返回末尾的1
{
    return x & -x;
}

void add(LL x,LL w){
    for(LL i = x;i <= n;i += lowbit(i)){ //同时维护两个树状数组
        a[i] += w;
        b[i] += x*w;
    }
}

LL query(LL u){
    LL res = 0;
    for(LL i = u;i > 0;i -= lowbit(i)){ //优化的区间查询方案
        res += (u+1) * a[i] - b[i];     //计算公式
    }
    return res;
}
//-----------------------------------------------//朴素代码,对于此题超时
/*
void add(LL x,LL w){
    for(LL i = x;i <= n;i += lowbit(i)){
        a[i] += w;
    }
}

LL query(LL u){
    LL res = 0;
    for(LL i = u;i > 0;i--){
        for(LL j = i;j > 0;j -= lowbit(j)){
            res += a[j];
        }
    }
    return res;
}
*///------------------------------------------------
int main()
{
    cin >> n >> m;
    LL t1=0,t2=0;
    for (LL i = 1; i <= n; i ++ ){
        cin >> t2;
        add(i,t2-t1);
        t1=t2;
    }
    for(LL i = 1;i <= m;i ++){
        char x;cin >> x;
        if(x == 'Q'){
            LL a,b;cin >> a >> b;
            cout << query(b) - query(a-1) << endl;
        }else{
            LL a,b,c;cin >> a >> b >> c;
            add(a, c);
            add(b+1, -c);
        }
    }
    return 0;
}

​​​​​​〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作_哔哩哔哩_bilibili
树状数组 数据结构详解与模板(可能是最详细的了)_bestsort的博客-CSDN博客_树状数组

 感谢阅读!

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值