前缀和算法

1.一维前缀和

【模板】前缀和_牛客题霸_牛客网 (nowcoder.com)https://www.nowcoder.com/practice/acead2f4c28c401889915da98ecdc6bf?tpId=230&tqId=2021480&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D196题中说给了一个长度为n的数组a1,a2......an,这里数组下标从1开始计数。有q次查询,每次查有l和r两个参数,输出al到ar的和。下面来解决,比如:

先想一个暴力解法:模拟就行,让求哪一段区间和,从头到尾加一下就可以了O(n*q)。接下来想一个更优的解法:前缀和,该算法用来解决快速求出数组中某一个连续区间的和。前缀和算法一般分为两步进行:1.先处理出来一个前缀和数组。2.使用前缀和数组。下面细看:首先创建一个数组,这个数组和原始arr数组是同等规模的:

dp[i]表示[1,i]区间内所有元素的和,如dp[3]表示arr[1]+arr[2]+arr[3]的和放在dp[3]的位置:

这样就把dp数组初始化完成了,dp[i]=dp[i-1]+arr[i]。下面来使用:就是处理每次询问,比如:

可以算1~5区间的和,然后减1~2区间的和就剩下3~5区间的和了。1~5区间的和其实是dp[r],1~2区间的和其实是dp[i-1],这样dp中的数据减就可以了:

这样时间复杂度是O(q) + O(n)。这有个疑问,为什么我们的下标要从1开始计数?为了处理边界情况。比如询问[0,2],得出dp[2]-dp[-1],不能到-1位置,要特殊处理。若下标从1开始,最左边最低是1,[1,2]是dp[2] - dp[0],0位置填写0即可,没有越界。下面来实现:

2.二维前缀和

【模板】二维前缀和_牛客题霸_牛客网 (nowcoder.com)https://www.nowcoder.com/practice/99eb8040d116414ea3296467ce81cbbc?tpId=230&tqId=2023819&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D196 先想一个暴力解法,就是模拟,让求哪个区间就把哪个区间的和都求出来。如:

问阴影区间就都依次详相加就可以,这样时间复杂度是O(n*m*q)。接下来想一个更优的解法,前缀和:1.预处理出来一个前缀和矩阵。2.使用前缀和矩阵。如:

重新创一个矩阵dp和原始arr数组规模一样大,dp[i][j]表示从[1,1]位置到[i,j]位置这段区间内所有元素的和。那如何快速求出dp[i][j]值是多少?若从前遍历原数组求和时间复杂度会很高,所以找个规律来快速求dp[i][j]的值。把图抽象出来:

要求dp[i][j],相当于求一个区域的和,可以分为这样一些部分:

最终dp[i][j]=A+B+C+D,A区域的和相当于从[1,1]到[i-1,j-1]所有元素和,A区域相当于是dp[]i-1[j-i] 但B和C不好表示,因此改为dp[i][j] = A+B+A+C+D-A,此时A+B相当于从[1,1]到[i-1,j]所有元素的和,即dp[i-1][j];A+C相当于从[1,1]到[i,j-1],即dp[i][j-1];D相当于arr[i][j]。因此得出dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1],这样在O(1)得出dp[i][j]。那如何使用?如求[x1,y1]~[x2,y2],继续抽象一下:

相当于A+B+C+D-(A+B)-(A+C)+A=D=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1],时间复杂度是O(m*n)+O(q),下面来实现:

3.寻找数组的中心下标

724. 寻找数组的中心下标 - 力扣(LeetCode)https://leetcode.cn/problems/find-pivot-index/description/ 以该下标为分界线,左边所有元素和是等于右边所有元素和的为中心下标:

若选择一个i点想判断它是否为中心下标,先算0~i-1区间的和,再算i+1~n-1区间的和,对比一下和看是否相等。若是暴力解法,每次枚举一个中心下标就左边加一通,右边加一通,这样时间复杂度是O(n^2)。可以通过前缀和思想优化暴力解法,首先预处理,该题需要前面元素的和,又需要后面元素的和,所以需要两个数组:f表示前缀和数组,g表示后缀和数组;f[i]表示区间0~i-1的所有元素和,g[i]表示i+1~n-1区间所有元素的和。下面有了数组想想怎么推数组的值:f[i]=f[i-1]+nums[i-1],g[i]=g[i+1]+nums[i+1]。再来使用数组:枚举所有中心下标i,判断f[i]和g[i]是否相等,若相等返回。还有些细节问题:1.填f[0]是会有f[-1]+nums[-1]越界,因此提前写好f[0]=0;同理g[n-1]=g[n]+nums[n],因此提前写g[n-1]=0。2.填f时从左向右填,填g时从右向左填。下面来实现(vector开始默认是0):

4.除自身以外数组的乘积

238. 除了自身以外数组的乘积 - 力扣(LeetCode)https://leetcode.cn/problems/product-of-array-except-self/description/ 先想一个暴力解法:

想求某一个位置,就把除该位置外的数都乘起来再放到该位置,这样时间复杂度是O(n^2)。下面想个优化方法:

单看某个位置,想求i位置最终结果,先算0~i-1区间的积,再算i+1~n-1区间所有元素的积。所以可以用前缀和思想先预处理一下,再用后缀和思想处理一下,当求i位置时直接从前缀和后缀和拿值就可以了。定义f表示前缀积,g表示后缀积:求f[i]时不要i位置元素,仅需i-1位置及之前元素积,因此f[i]表示0~i-1区间内所有元素的乘积,f[i]=f[i-1]*nums[i-1]。g[i]表示i+1~n区间内所有元素的积,g[i]=g[i+1]*nums[i+1]。有了数组后下面来使用,最后要数组,就创建一个数组ret,然后依次填数组ret[i]=f[i]*g[i]。最后处理一下细节问题:f(0)=1,g(n-1)=1。下面来实现:

5.和为k的子数组

560. 和为 K 的子数组 - 力扣(LeetCode)https://leetcode.cn/problems/subarray-sum-equals-k/description/ 先想一个暴力解法:

固定一个起始位置,直到找到一个位置不能停止(数据既有正又有负,找到一个不能停,可能正负抵消),继续向后找,直到最后位置;下一个位置也是一样的,这里时间复杂度是O(N^2)。下面来优化,引入一个概念,以i位置结尾的所有的子数组,如:

这样也能把所有子数组枚举出来。那以i为结尾的子数组中和为k的如何找:

相当于找一个位置,从该位置到i和为k就行。直接找就是从i开始向前(左)加,直到为k,这样和暴力没有区别。现在这样看:

当枚举到i位置时,已经知道以i位置为结尾的前缀和是sum[i],要找个区间让它和k,仅需找前一段和为sum[i]-k就行。当枚举到以i位置为结尾时,问题转换为在[0,i-1]区间内,有多少个前缀和等于sum[i]-k。若单纯把前缀和数组弄出来:

找i位置之前有多少个前缀和为sum[i]-k,从头到i-1遍历有多少个sum[i]-k,这样还不如暴力解法,还多弄了个前缀和数组。要想如何快速找到前缀和有多少个sum[i]-k,所以借助哈希快速查找,<int, int>,哈希表中存两个int,第一个int是前缀和,第二个int是前缀和出现的次数,这样仅需找前缀和出现了多少次。还有些细节:1.前缀和加入哈希的时机?所有前缀和都是算出来的,再都丢到哈希里?不行,因为可能i之后也有前缀和为sum[i]-k,会重复统计。因此再计算i位置之前,哈希表中只保存[0,i-1]位置前缀和。2.不用真的创建一个前缀和数组,用一个sum来标记前一个位置前缀和即可。3.如果整个前缀和为k呢:

要找一个前缀和数组为0,直接hash[0]=1,默认有一个。下面来实现:

6.和可被k整除的子数组

974. 和可被 K 整除的子数组 - 力扣(LeetCode)https://leetcode.cn/problems/subarray-sums-divisible-by-k/description/ 解法一是暴力枚举,暴力枚举出所有子数组,把和加起来看被k整除的有几个就行了。再看优化解法时先来补充一个知识:1.同余定理:(a -b)/ p = k......0,就是a-b的结果能够被p整除,说明a%p = b%p,它们余数是一样的。2.c++或java中%正数的结果以及修正:负%正=负,若想修正为正数,可通过(a%p + p) % p修改为正数。这道题和上一题很像,依旧是前缀和+哈希表:

依旧考虑i前面的子数组,该题找这个区间(黑线)能否被k整除,此时可找两个前缀和sum和x,若找的黑区间能被k整除,应该是(sum-x) / k = 0,然后推出sum%k=x%k,所以问题变成了在0~i-1区间找一找前缀和中有多少个条数等于sum%k。但是前缀和有可能是负数,所以改为余数等于(sum%k+k)%k。最后注意细节:1.没必要真创建一个前缀和数组,仅需把前缀和条数存哈希表中。定义哈希表<int,int>,第一个int表示前缀和条数,第二个是次数,剩下的细节和前一道题一样。下面来实现:

7.连续数组

525. 连续数组 - 力扣(LeetCode)https://leetcode.cn/problems/contiguous-array/description/ 如[1,0,1,1,0,0,0,1],这道题本质是让我们找一个连续的区域,使这段区间中的0和1数目相等,并且要找一个最长的连续区域。如果把0都变成-1:[1,-1,1,1,-1,-1,-1,1],相当于在数组中找一段连续区域,使这段区域的和为0,相当于找最长和为0的子数组。那么用前缀和+哈希表来解决,和之前一样。下面来看细节问题:1.哈希表中存什么?hash<int,int>存的是前缀和与下标(统计长度)。2.什么时候存入哈希表?谁置于与对应前缀和绑定存入哈希表中:

3.如果有重复的<sum, i>如何存:

更新到i位置发现前面有个位置前缀和也为sum,只保留前面那一对<sum, j>。4.默认的前缀和为0情况如何存?hash[0]=-1。5.长度怎么算:

i-(j+1)+1等于i-j。下面来实现:

8.矩阵区域和

1314. 矩阵区域和 - 力扣(LeetCode)https://leetcode.cn/problems/matrix-block-sum/description/题目给我们一个原始的矩阵mat和一个整数k,如:

最终让我们返回一个同等规模的矩阵answer,answer任意一个位置的值是向上扩展k个单位,此时围成的长方形周围所有元素的和。比如求answer[0,0]:

超出界的不考虑,这样answer[0,0]是12。下面来解决,这道题本质是快速求出矩阵某一区域的和,解法是一个二维前缀和:1.

dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + mat[i][j]。2.使用:

ret = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]。回归题目:

想求answer[i][j],必须知道在原始矩阵中表示的区域是什么,需要知道右下角和左上角的坐标,如:

求区域时要扩k个单位,这样就很好找,左上角(i-k,j-k),右下角是(i+k,j+k)。但有些左边可能越界,所以可这样处理:

还有个问题是下标映射关系:为了处理公式中边界情况,dp矩阵下标是从1开始计数的,但题目给的矩阵是从0开始计数的:

所以填dp[1][1]时找的是原始矩阵中的mat[0][0]。所以让dp多加一行和一列:

此时要修改一下公式:当填dp[1][1]时,本质mat[0][0]找结果,也就是dp[x][y]时去mat[x-1][y-1]找值:

当使用时ans也是从0计数:

ans[0][0](x,y)对应dp[1][1](x+1,y+1),可给ret统一加1或者通过ans[i][j]得到原始矩阵下标,然后+1:

下面来实现:

相关推荐
客卿1232 小时前
滑动窗口--模板
java·算法
_日拱一卒2 小时前
LeetCode:滑动窗口的最大值
数据结构·算法·leetcode
codeの诱惑2 小时前
推荐算法(一):数学基础回顾——勾股定理与欧氏距离
算法·机器学习·推荐算法
落樱弥城2 小时前
Vulkan Compute 详解
算法·ai·图形学
Book思议-2 小时前
【数据结构】字符串模式匹配:暴力算法与 KMP 算法实现与解析
数据结构·算法·kmp算法·bf算法
客卿1233 小时前
动态规划--模板--完全背包
算法·动态规划
L-影3 小时前
下篇:一棵树能长成多少种样子?——AI中决策树的类型与作用,以及它凭什么活了六十年还没过气
人工智能·算法·决策树·ai
mifengxing3 小时前
力扣HOT100——(1)两数之和
java·数据结构·算法·leetcode·hot100
無限進步D3 小时前
算竞常用STL cpp
开发语言·c++·算法·竞赛