第18-19天学习笔记——前缀和

本文探讨了前缀和在计算区间和、数组问题和哈希表优化中的应用,涉及LeetCode题目和实际场景,展示了算法效率提升的方法。
摘要由CSDN通过智能技术生成
  • 第18天直播
    • 前缀和
      • 涉及到前几个元素的和的问题
      • 「前缀和」数组的思路是:将原始数组进行预处理,将来需要查询数据的时候,只需要查询预处理数组的某些值即可。
      • 前缀和是非常能体现利用空间换时间的算法思想,实现了快速计算区间和的作用。
      • 第一种定义:
      • 0、nums 数组长度为 N , preSum 数组长度也为 N
      • 1、当 i >= 1 ,preSum[i] 表示从 nums[0] 到 nums[i] 所有元素的和
      • 2、当 i = 0 ,preSum[0] = 0
      • 递推公式:preSum[i] = preSum[i - 1] + nums[i]

      • preSum[0] = nums[0]for( int i = 1 ; i < nums.length ; i++){ preSum[i] = preSum[i - 1] + nums[i];}
      • 第二种定义:
      • nums 数组长度为 N , preSum 数组长度为 N + 1,preSum[0] = 0

      • preSum[0] = 0for( int i = 0 ; i < nums.length ; i++){ preSum[i + 1] = preSum[i] + nums[i];}
      • LeetCode 303、区域和检索-数组不可变
        • 用第二种定义
        • 并且i<= m <=j 的和 sums[j + 1] - sums[i]
      • LeetCode 304、二维区域和检索 - 矩阵不可变(设计类题目)
        • 其实也是类似于前缀和的计算
        • 就是计算包含右下角的整个的和 — 不包含左上角的和 = 我们要计算的矩阵的和

        • 即红色 = 绿色 — 粉色 — 黄色 + 粉黄共有的
        • 可以通过以下的方式进行计算绿色等矩形内的和

        • 最后计算的过程如下:

      • LeetCode 1588、所有奇数长度子数组的和
        • 设置两个循环:
        • for i in range(len(arr)): # 对每个arr中的元素遍历
          • for j in range(1, len(arr)+1, 2): # 隔奇数个
      • LeetCode 560、和为 K 的子数组
        • 用 第一种定义
        • 那么区间 [ i , j ] 之间的子数组之和就是 preSum[j] - preSum[i]。
        • 基于这种思路,可以先遍历一次数组,求出前缀和数组。
        • 题目这个时候就变成了需要寻找出多少个 i 和 j 的组合,使得 [ i , j ] 这个区间的和为 k。
        • 在计算过程中,有两个 for 循环发生了嵌套,时间复杂度来到了 O(n^2) 级别。
        • 需要优化。
        • 事实上,我们不需要去计算出具体是哪两项的前缀和之差等于k,只需要知道等于 k 的前缀和之差出现的次数 count,所以我们可以在遍历数组过程中,先去计算以 nums[i] 结尾的前缀和 pre,然后再去判断之前有没有存储 pre - k 这种前缀和,如果有,那么 pre - k 到 pre 这中间的元素和就是 k 了。
        • 具体操作如下:
        • 1、利用哈希表,以前缀和为键,出现次数为对应的值,记录 pre[i] 出现的次数。注意要在哈希表中存储一个0
        • 2、开始从头到尾遍历 nums 数组,在遍历过程中,会执行两个操作。
        • 3、存储索引为 i 的这个元素时,前缀和的值是多少,并且把这个值出现的频次存储到 mp 中。
        • 4、判断之前有没有存储 pre - k 这种前缀和,如果有,说明 pre - k 到 pre 直接的那些元素值之和就是 k。
        • 5、返回结果。
        • 注意这里涉及到一个高级的函数
        • mp = collections.defaultdict(int)
        • mp[pre - k]
        • 利用 defaultdict 的特性,当 presum - k 不存在时,返回的是 0,即{0: 0}
      • LeetCode 1248、统计「优美子数组」
        • 用前缀和来做,统计前i个连续数字中奇数的个数
        • 和LeetCode 560、和为 K 的子数组的思路是一样的,把原始数组中奇数变为1,偶数变为0即可
        • 知识点:按位与操作:nums[i] & 1,nums[i]为奇数,按位与完为1,偶数按位与完为0
      • LeetCode 238、 除自身以外数组的乘积
        • 和剑指 Offer 66. 构建乘积数组的思路是一样的,构建左边积和右边积数组
          • 比如数组 A 为 [1,2,3,4,5] 。
          • 1、想要计算除了 2 以外的结果时,需要计算 1 * 3 * 4 * 5。
          • 2、想要计算除了 3 以外的结果时,需要计算 1 * 2 * 4 * 5。
          • 注意到,这两个计算过程都计算了 1 和 4 * 5 。
          • 所以,我们优化的方向就是去保存好计算的结果,避免重复计算。
          • 1 出现在 2 和 3 的左侧,4 * 5 出现在 2 和 3 的右侧,它们分别可以使用数组提前计算保存下来。
          • 在公式 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1] 中,实际上可以划分为两个部分,从 0 到 i - 1 和从 i + 1 到 n - 1,因此,想要构建乘积数组后某下标对应元素的值,只需要求出该下标对应原数组中其左边的元素的乘积和其右边的元素的乘积,最后将两个乘积再相乘即可。
          • 具体操作如下:
          • 数组 A 为 [1,2,3,4,5] 。
          • 1、数组 left[i] 表示在数组 A 中下标为 i 的所有左边元素的乘积,如果左边没有元素,默认为 1。
          • 2、数组 right[i] 表示在数组 A 中下标为 i 的所有右边元素的乘积,如果右边没有元素,默认为 1。
          • 3、B[i] = left[i] * right[i] 。
        • 这边更加简化处理了,用answer代替了left和right数组
      • LeetCode 1124、表现良好的最长时间段
        • 前缀和+哈希表
        • 把大于8小时的赋值为1,小于等于8小时的赋值为-1
        • 问题就变为了:寻找出一个最长的子数组,满足子数组所有元素和大于0
        • 也就变为了前缀和的问题,同时还有哈希表
        • 哈希表中记录每个非零前缀和的第一次出现的下标
        • 因为最大/最长,其实就是在找比s[i]差1的那个元素第一次出现的位置
        • 如果 s[i]>0,那么 j=0 就是最远的左端点,因为 s[0]=0,故 s[i]−s[0]=s[i]>0,符合要求。
        • 如果 s[i]≤0,那么 j就是 s[i]−1首次出现的位置。为什么是 s[i]−1 而不是其它更小的数?这是因为前缀和是从0 开始的,由于 nums中只有 1 和 −1,那么相邻前缀和的差都恰好为 1,要想算出比 s[i]−1 更小的数,必然会先算出 s[i]−1,那么这些更小数必然在 s[i]−1 首次出现的位置的右边。
        • 假设

      • 【前缀和】美团2023秋招-小美的蛋糕切割
        • 模拟题,注意有横切和竖切两种方法,需要分别计算。前缀和枚举切的方式计算
          • 注意语法:zip(*grid)将二维数组按照列来进行重新的组合

      • 【前缀和】2023B-数字游戏
        • 本题需要用到前缀和的概念。
          • 对于一个给定的数列A ,它的前缀和数列S中S[i+1]表示从第1个元素到第i个元素的总和。
          • 假设nums是一个int型列表,形如sum(nums[0:i+1])就是从索引0对应的元素开始,累加到索引i对应的元素的前缀和。
          • 譬如nums = [1, 2, 3, 4],那么其前缀和列表即为pre_sum_lst = [0, 1, 3, 6, 10]。
          • 前缀和的作用是可以在O(1)的时间复杂度下快速地计算出某段连续子数组的和。即
          • sum(nums[i:j]) = pre_sum_lst[j] - pre_sum_lst[i]
          • 譬如对于上述nums = [1, 2, 3, 4]而言,如果想快速计算出子数组nums[1:4] = [2, 3, 4]的结果,只需要计算pre_sum_lst[4] - pre_sum_lst[1] = 10 - 1 = 9即为答案。
          • 前缀和的作用也可以解释,为什么我们会把0也视为一个前缀和并且放在前缀和列表的第一个位置。由于设置了pre_sum_lst[0] = 0,那么pre_sum_lst[i] - pre_sum_lst[0] = sum(nums[:i]),才能够得到起始位置为原数组nums中第一个元素的连续子数组的和。
        • 重点、关键,前缀和与取余操作的关系

        • 哈希集合的使用
          • 在本题中,只需要判断能否找到一个满足题意的连续子数组,显然下标的具体值并不重要。故我们可以直接使用一个哈希集合pre_sum_set来储存所有的前缀和对m求余的结果,而不用考虑下标。
          • 我们可以在一个循环中对前缀和进行计算和判断,其具体结果如下:
          • 计算包含了i位置元素的前缀和pre_sum
          • 计算当前前缀和对m的求余结果pre_sum % m
          • 判断求余结果pre_sum % m是否位于哈希集合中,若
            • 存在,则说明在此之前存在某个前缀和对m求余可以得到一样的结果。退出循环,输出1
            • 不存在,继续循环
          • 如果在上一步中没有退出循环,则将pre_sum % m存入哈希集合pre_sum_set中
      • 【前缀和】美团2023秋招-平均数为k的最长连续子数组
        • 题目转化
          • 求连续子数组的平均数是一个比较难处理的过程,可以先做一步转换,把原数组nums中每一个元素都减去k得到一个新数组nums_new,那么题目就变成了求和为0的最长连续子数组的长度了。
        • 对数组nums_new构建前缀和数组pre_sum_lst,由于要求计算和为0的连续子数组,故我们仅需要找到pre_sum_lst中两个距离最远的相等元素。该过程可以通过一边遍历pre_sum_lst中的元素pre_sum,一边构建哈希表dic来进行,若
          • pre_sum不位于哈希表中,说明它是首次出现,将其下标i记录在哈希表中
          • pre_sum已位于哈希表中,说明它之前已经出现过了,第一次出现的下标为dic[pre_sum],那么当前和为0的连续子数组的长度为i-dic[pre_sum],将其与ans比较并更新。
        • 上述过程的核心代码如下

      • 【优先队列】美团2023秋招-小美的游戏
        • 解决本题需要一个前置的数学知识:若两个正数的积为定值,那么这两个数的差越大,它们的和越大。这个性质很容易用导数、基本不等式、数形结合等方式证明,故略去不表。
        • 需要堆化
          • heap = list(-num for num in a_lst)
          • heapq.heapify(heap)
        • 观察出上述规律后,我们为了使得每次操作后数组nums的和尽可能地大,我们每次贪心地选取nums中最大的两个数first和second来进行操作,操作完毕后将1和first*second放回数组中。
        • 为了使得上述弹出first和second,以及插入1和first*second两个操作的时间复杂度尽可能地小,我们可以使用优先队列/堆来辅助上述过程。
        • 要注意在Python和Java中,heapq/PriorityQueue默认是构建小根堆,而此题要求的是构建大根堆,故需要对nums中的元素取反再进行建堆操作,得到一个伪大根堆。
          • 这里要注意:由于python中heapq只能构建小根堆
          • 故建堆的时候,要把nums中的每一个元素取为负数后储存
          • 这样构建出来的堆,是以最小的负数(取反为最大的正数)为堆顶的伪大根堆
          • 每次操作只需要取反即可
      • 【链表】大疆2023秋招-链表合并
        • 注意,本题和LeetCode23. 合并K个升序链表完全一致。
        • 将数组转化为链表,再用优先队列对K个升序链表进行K路归并
        • 注意这道题如何在ACM模式下构建链表
  • 第十九天(作业)
    • 【前缀和】2023B-阿里巴巴找黄金宝箱(1)
      • 考虑0号箱子,初始化两个变量 left_sum = 0 和 right_sum = sum(nums[1:]) 分别表示左、右两边所有箱子和。当考虑第i号箱子时
        • 右边箱子和 right_sum 不再考虑第i号箱子,应该减掉 nums[i]
        • 左边箱子和left_sum要考虑第i-1号箱子,应该加上nums[i-1]
      • 若做完加减后,两者相等,则i为找到的第一个黄金箱子
    • LeetCode 1749、任意子数组和的绝对值的最大值
      • 子数组的元素和 = 两个前缀和的差

      • 取前缀和中的最大值与最小值,它俩的差就是答案
    • LeetCode974、可被K整除的子数组(不好理解)
      • 前缀和+哈希表
      • 哈希表存储当前位置的上一个位置的前缀和的余数加上当前位置的值对K的余数
      • 题目要求是K的倍数,那么对于已经可以被K除掉的部分就不用考虑了,只需要考虑余下的部分就好了
        • pre_mod = (pre_mod + num) % K
      • 如果能在dict中找到相同的pre_mod,说明当前节点前的某个位置的前缀和到当前位置的前缀和间存在若干个K
    • LeetCode707、设计链表
      • 模拟题
      • 可以采用单向链表
    • 简答题
      • 前缀和技巧适用于解决哪一类问题?
        • 涉及到前几个元素的和的问题,或者子数组和的问题
      • 为什么构建前缀和数组时,通常需要在初始位置多添加一个0?
        • 因为设置了pre_sum_lst[0] = 0,那么pre_sum_lst[i] - pre_sum_lst[0] = sum(nums[:i]),才能够得到起始位置为原数组nums中第一个元素的连续子数组的和。
      • 用到前缀和结合哈希数据结构的题目通常有以下几种设问方式,对于这些不同的设问方式,应该如何使用哈希表/哈希集合来解决?
        • 求和为k的连续子数组是否存在
          • 创建一个哈希集合用于存储前缀和。
          • 初始化一个前缀和变量prefixSum为0。
          • 遍历数组元素,累积前缀和,并将当前前缀和preSum添加到哈希集合中。
          • 在每次添加前缀和时,检查是否存在一个前缀和(preSum - k),如果存在,说明存在和为k的连续子数组。
        • 求和为k的最长连续子数组
          • 创建一个哈希表用于存储前缀和和它们第一次出现的索引。
          • 初始化一个前缀和变量prefixSum为0,以及一个最长子数组长度变量maxLength为0。
          • 遍历数组元素,累积前缀和,并将当前前缀和添加到哈希表中,但只在它第一次出现时添加。
          • 在每次添加前缀和时,检查是否存在一个前缀和(prefixSum - k),如果存在,更新maxLength为当前索引与哈希表中存储的索引之差。
        • 求和为k的最短连续子数组
          • 和最长的一样,只不过换为算min()
        • 求和为k的连续子数组的个数
          • 哈希表存储前缀和的值为key,该前缀和值出现的次数为value、
          • 创建一个哈希表用于存储前缀和的频率计数。
          • 初始化一个前缀和变量prefixSum为0,以及一个计数变量count为0。
          • 遍历数组元素,累积前缀和,并将当前前缀和添加到哈希表中,并更新对应前缀和的频率。
          • 在每次添加前缀和时,查询preSum_map[preSum[i]-k],如果存在,将哈希表中该前缀和的频率加到count上,因为这意味着有多个子数组的和为k。
          • 最后,返回count作为答案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值