首页 > 其他分享 >从 动态前缀和 到 树状数组

从 动态前缀和 到 树状数组

时间:2024-12-10 21:32:13浏览次数:9  
标签:元素 前缀 val nums int sum 树状 数组

一.引言——动态前缀和

前缀和求解我会在最后给出,想看的直接翻到最后,这里默认大家都知道前缀和怎么求解。

有这么一个场景,我们需要不断修改数组中的元素,并且问我们数组内某个区间的和。如果使用最原始的前缀和来求解,每次我们都要重新求一遍sum[],更新时间复杂度是O(n)的,查询是O(1)的。

有没有什么办法能够平衡一下让时间复杂度更低,这就引入了树状数组的概念。

二.树状数组

1.介绍

我们最原始的想法是这样的,我们给数组分块,比如一个长度为16的数组,我们给它2个元素分一组,这样我们修改元素的时候可以只修改一个模块就行,其他模块不用修改。这样我们在求某个区间的元素的话,只需要将单个元素和模块相加就行了。

想到这,我们在进一步,我们是不是可以多分几个组,先1个分一组,再2个分一组,再4个分一组,再8个,再16个,将这个数组都存一遍,这样求和的时候就不用一个一个去将小模块加在一起。

从上面举的例子我们可以发现其实有一些模块是用不到的,删除它们

为什么用不到?

比如我们求前两个元素的元素和,直接使用14这个模块即可,不用使用到6。

再比如为什么14模块后面的5用不到,因为求前三个元素的元素和可以使用14+1,求前四个元素的元素和可以使用19。

将上面元素依次排成一个数组,这个数组就是树状数组。

看到这里可能有人会问:这是什么跟什么呀,为什么是这样?接下来我来告诉大家这个分组到底是怎么来的。

其实这个分组是根据下标的二进制来分出来的:

比如5的二进制的101,101就是4+1,所以0-5的区间可以分成一个长度为4的模块和长度为1的模块,对应上面的图是这样的:

看到这里大家不免有一个疑惑,这个树状数组tree[i]到底表示什么?

给大家看另一个图:

这个图对比较清晰的表示tree[]是什么,这里的c[]就是tree[]。

树状数组更新和查询的时间复杂度都是O(log N)的。

2.更新操作

我们想要更新数组内某个元素的值,更新完后肯定是要更新区间内的值的,那我们怎么更新呢?

比如我们要更新11这个下标的数,从图上我们也能看到,我们要跟新的区间是c[11]、c[12]、c[16]对不对。我们可以从这几个下标中找找关系。

这里就直接说结论了,实际证明比较麻烦,感兴趣的可以自己找找。

我们每次求的模块的下标就是当前下标+当前下标的lowbit。

什么是lowbit?lowbit(x)指的是x二进制最低位代表什么数字。

public void update(int index, int val) {
    int delta = val - nums[index];
    nums[index] = val;
    for (int i = index + 1; i < tree.length; i += i & -i) {
        tree[i] += delta;
    }
}

3.查询操作

这里给出查询前i个元素的元素和的方法:

int count(int p){
    //求前q个元素的元素和
    int result=0;
    while(p!=0){
        result+=tree[p];
        p-=lowbit(p);
    }
    return result;
}

4.核心代码

public int lowbit(int x){
    return x&(-x);
}
public void update(int index, int val) {
    //将下标index位置的元素改成val
    int delta = val - nums[index];
    nums[index] = val;
    for (int i = index + 1; i < tree.length; i += i & -i) {
        tree[i] += delta;
    }
}
int count(int p){
    //求前q个元素的元素和
    int result=0;
    while(p!=0){
        result+=tree[p];
        p-=lowbit(p);
    }
    return result;
}

三.补充——前缀和

以往我们要得到某个范围内的全部元素和通过遍历,一个一个加起来,如果只是这么操作一次还好,要是多次操作时间复杂度就很高了。前缀和通过预处理一个数组,可以让我们快速的得到某个范围内的元素和。这个范围既可以是一维的,也可以是二维的。

1.一维前缀和

定义一个sum数组,大小是nums数组长度+1,sum[i]表示nums前 i 个数的总和。

预处理方法也是很简单:

int n=nums.length;
int[] sum=new int[n+1];
for (int i = 0; i < n; i++) {
    sum[i+1]=sum[i]+nums[i];
}

为什么sum的大小要比nums大1?其实就是为了方便我们处理。

有了这个sum数组,那我们这么求任意区间内元素的总和呢?

举个例子:

我们要求区间5-9的元素总和

sum[10]=nums[0]+nums[1]+...+nums[9]

sum[5]=nums[0]+nums[1]+...+nums[4]

区间5-9的总和=nums[5]+nums[6]+...+nums[9]=sum[10]-sum[5]

所以说任意区间 l-r 的元素总和等于:

sum[r+1]-sum[l]

2.二维前缀和

定义一个二维sum数组,sum[i][j]表示nums[0][0]-nums[i-1][j-1]这个子矩阵(左上角是(0,0),右下角是(i-1,j-1) )的元素总和。

预处理方法:

int n= nums.length;
int m=nums[0].length;
int[][] sum=new int[n+1][m+1];
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        sum[i+1][j+1]=sum[i+1][j]+sum[i][j+1]-sum[j][j]+nums[i][j];
    }
}

求子矩阵的和:

子矩阵的左上角是(l1,r1),右下角是(l2,r2),这个子矩阵的元素和是:

sum[l2+1][r2+1]-sum[l2+1][r1]-sum[l1][r2+1]+sum[l1][r1]

四.补充——差分

如果我们想给某一个区间上的全部元素全都加上某个数,我们可以遍历一遍这个数组,给区间上的每个数加上。但是如果我们频繁的进行这些操作的话效率就太低了。差分通过预处理一个数组d,可以高效对某个区间内的元素加减某个数。

1.一维差分

定义一个数组d,d[i]=nums[i]-nums[i-1](i>=1),特别的,当i=0时,d[0]=nums[0]。同时d的长度也是nums长度+1。如果将未经过处理的d从头累加起来的话就能还原原来的nums数组。

预处理d:

int n=nums.length;
int[] d=new int[n+1];
d[0]=nums[0];
for (int i = 1; i <= n; i++) {
    d[i]=nums[i]-nums[i-1];
}

我们想对l和r区间的元素同时加上val:

d[l]+=val;
d[r+1]-=val;
//更新nums
int sum=0;
for (int i = 0; i < n; i++) {
    sum+=d[i];
    nums[i]=sum;
}

如果想减掉val的话将加减变一下就行了。

为什么这样写就可以实现对区间内的元素相加减呢?

前面说个将没处理d累加起来就等于原来的nums数组。我们在 l 位置时多加了一个val,那么从 l 开始的往后所有元素都要加上一个val。那我们想让 r 后面的元素不要加上val,那我们就在 r+1 的位置减去一个val,这样从 r+1 开始往后的全部元素都要减去一个val,这样就抵消了。

为什么将没处理d累加起来就等于原来的nums数组?

d[0]=nums[0],d[1]=nums[1]-nums[0],d[2]=nums[2]-nums[1]......

累加起来,nums[i]=d[i]+d[i-1]+...+d[0]。

2.二维差分

预处理:

int[][] d=new int[n+1][m+1];
for (int i = 1; i <=n; i++) {
    for (int j = 1; j <= m; j++) {
        d[i+1][j+1]+=nums[i][j];
        d[i+1][j]-=nums[i][j];
        d[i][j+1]-=nums[i][j];
        d[i][j]+=nums[i][j];
    }
}

对左上角(x1,y1)与右下角(x2,y2)子矩阵内的元素同时加上val:

d[x1][y1] += val;
d[x2 + 1][y1] -= val;
d[x1][y2 + 1] -= val;
d[x2 + 1][y2 + 1] += val;
for (int i = 1; i <= n; i++){
    for (int j = 1; j <= m; j++){
        nums[i][j] = nums[i - 1][j] + nums[i][j - 1] - nums[i - 1][j - 1] + d[i][j];
    }
}

标签:元素,前缀,val,nums,int,sum,树状,数组
From: https://blog.csdn.net/lllsure/article/details/144332856

相关文章

  • 树状数组进阶
    树状数组是众多数据结构中常数较小,简单好写的,很多ds题都应该优先考虑树状数组。接下来介绍树状数组的几种常见套路。单点加,区间求和区间加,单点查询区间加,区间求和二维树状数组,包括上面\(3\)个操作单点修改,求全局\(k\)小值求前驱,后继,排名等平衡树操作......
  • leetcode 2779. 数组的最大美丽值
    2779.数组的最大美丽值暴力超时解......
  • 通过指针引用数组
    指针变量既可指向变量,又可指向数组元素(把数组某一元素的地址存放在指针变量),它们都有地址。数组元素的指针就是数组元素的地址。可以由两种办法引用数组元素。1.下标法,如a【1】;2.指针法,如p=&a【0】(p为指针变量)等价于p=a。在用指针变量引用数组有几个很重要的知识点。1.假设a为......
  • leetcode 2958. 最多 K 个重复元素的最长子数组
    2958.最多K个重复元素的最长子数组classSolution{public:intmaxSubarrayLength(vector<int>&nums,intk){intsize=nums.size(),resLenth=0;unordered_map<int,int>numAdded;for(intleft=0,right=0;right<siz......
  • C语言基础-数组:一维数组与二维数组
    数组例子如果我们要在程序中表示一个学生的成绩,我们会使用一个int来表示,假如我们要在程序中表示一组成绩,此时我们所学的常规的数据类型就无法再表示,这时就需要使用一种新的表现形式,这种表现形式就是数组什么是数组数组是相同类型,有序数据的集合数组的特征数组中的数据......
  • 数组去重:双指针法的优雅实现
    数组去重:双指针法的优雅实现在日常开发中,数组去重是一个常见的需求。无论是处理用户输入、数据清洗,还是优化算法性能,去重操作都扮演着重要角色。本文将介绍一种高效的去重方法——双指针法,并结合代码示例,帮助你轻松掌握这一技巧。1.问题描述给定一个包含重复元素的数组,要求删......
  • 请说说JS中的索引数组、关联数组和静态数组、动态数组的定义与区别
    在JavaScript中,数组的概念比较灵活,不像一些强类型语言那样区分得那么严格。JS中的数组实际上是一种特殊的对象,既可以像索引数组一样通过数字索引访问元素,也可以像关联数组一样通过字符串键访问元素。所以,严格意义上来说,JS只有动态数组,它兼具了索引数组和关联数组的特性。而静......
  • JS代码片段-Array数组克隆的几种方法
    JavaScript自身提供了几种克隆数组的方法,以下做了汇总,以作参考:1.展开运算符(...) ES6引入了展开运算符(...),这是创建数组浅克隆最常见的方法。leta=[1,2,3,4,5];letb=[...a];2.Array.from()leta=[1,2,3,4,5];letb=Array.from(a);3.Array.prototype.s......
  • 数组中的第K个最大元素:算法实现与性能分析
    问题背景在算法面试和实际编程中,找出数组中第K大的元素是一个常见且经典的问题。本文将深入探讨该问题的两种主要解决方案:快速选择算法和堆排序方法。问题描述给定一个未排序的整数数组nums和一个整数k,要求找出数组中第k个最大的元素。注意,这里的"第k大"意味着排序......
  • 数组中的逆序对:基于归并排序的高效解法
    问题背景在算法和数据结构领域,"逆序对"是一个经典的计算问题。逆序对定义为数组中前面的元素大于后面的元素的数对。例如,在序列[7,5,6,4]中,存在的逆序对包括:(7,5)(7,6)(7,4)(5,4)(6,4)问题分析问题要求给定一个整数数组,要求计算数组中所有逆序对的总数。暴力解法的局......