前缀和,差分算法理解
前缀和是什么:
前缀和指一个数组的某下标之前的所有数组元素的和(包含其自身)。前缀和分为一维前缀和,以及二维前缀和。前缀和是一种重要的预处理,能够降低算法的时间复杂度
说个人话就是比如有一个数组:
[1,2,3,4],然后我求得sum数组[1,3,6,10];
那我只要有了这个sum数组,我就可以在O(1)得时间内求出区间和,
比如我想求interval[i,j]内得区间和,我怎么办,我直接那sum[j]-sum[i]就可以了。
ok了解了这个思想,我们也很容易想到:前缀和得作用或者说前缀和适合解决什么样的题目呢:就是子数组,或者字字符串类似的题目:
下面上例题:先从简单的开始:
leetcode1732:找到最高海拔:
有一个自行车手打算进行一场公路骑行,这条路线总共由 n + 1
个不同海拔的点组成。自行车手从海拔为 0
的点 0
开始骑行。
给你一个长度为 n
的整数数组 gain
,其中 gain[i]
是点 i
和点 i + 1
的 净海拔高度差(0 <= i < n
)。请你返回 最高点的海拔 。
有一个自行车手打算进行一场公路骑行,这条路线总共由 n + 1
个不同海拔的点组成。自行车手从海拔为 0
的点 0
开始骑行。
给你一个长度为 n
的整数数组 gain
,其中 gain[i]
是点 i
和点 i + 1
的 净海拔高度差(0 <= i < n
)。请你返回 最高点的海拔 。
class Solution {public int largestAltitude(int[] gain) {int len = gain.length;int[] sum = new int[len+1];for(int i=1;i<=len;++i){sum[i] = gain[i-1]+sum[i-1];}int flag = Integer.MIN_VALUE;for(int i=0;i<=len;++i){if(sum[i]>flag){flag = sum[i];}}return flag;}
}
这道题也算是前缀和的一个入门,我们求出前缀和数组sum之后,遍历sum找到最大值即可。
leetcode 1413 逐步求和得到正数的最小值:
给你一个整数数组 nums
。你可以选定任意的 正数 startValue 作为初始值。
你需要从左到右遍历 nums
数组,并将 startValue 依次累加上 nums
数组中的值。
请你在确保累加和始终大于等于 1 的前提下,选出一个最小的 正数 作为 startValue 。
输入:nums = [-3,2,-3,4,2] 输出:5 解释:如果你选择 startValue = 4,在第三次累加时,和小于 1 。 累加求和startValue = 4 | startValue = 5 | nums (4 -3 ) = 1 | (5 -3 ) = 2 | -3(1 +2 ) = 3 | (2 +2 ) = 4 | 2(3 -3 ) = 0 | (4 -3 ) = 1 | -3(0 +4 ) = 4 | (1 +4 ) = 5 | 4(4 +2 ) = 6 | (5 +2 ) = 7 | 2
class Solution {public int minStartValue(int[] nums) {int len = nums.length;int[] sum = new int[len+1];for (int i = 1; i <=len; i++) {sum[i] = sum[i-1]+nums[i-1];}int flag = Integer.MAX_VALUE;for (int i = 1; i <=len ; i++) {if(sum[i]<flag){flag = sum[i];}}return flag<0?(flag*-1)+1:1;}
}
这道题就比上一道需要多分析一些东西,
首先题目要求一个最小的正整数,所以我们肯定要根据这个数组的和来求。
leetcode 724:找数组的中心下标
给你一个整数数组 nums ,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
那这道题目不是典型的前缀和嘛,我们要求这个所谓的中心下标,我们不就是这个中心下标的左边的区间和==右边的区间和嘛
public int pivotIndex(int[] nums) {int len = nums.length;int i;int[] sum = new int[len + 1];sum[0] = 0;for (i = 0; i < len; ++i) {sum[i + 1] = sum[i] + nums[i];}for (i = 1; i < len+1; ++i) {if (sum[len] == (2 * sum[i - 1]) + nums[i-1]) {return i-1;}}return -1;}
当我们算出来这个sum数组之后,我们只需要从左往右遍历一下,然后根据数学公式就可以得出下标的结果,
这里有一个注意点:
就是你求sum数组的时候,你需要在sum[0]的位置垫一个0(这也是前缀和的固定套路),怎么理解呢
比如有一个数组:[1,0,0,0],那这个时候,1这个位置很明显是中心坐标,如果你不垫个0在前面的话,公式就不成立
其实这种情况也可以作为一个特判(我认为),也可以理解为一个前缀和的固定套路。
线性前缀和变形:
leetcode 238 除自身以外数组的乘积:
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n)
时间复杂度内完成此题。
输入: nums =[1,2,3,4]
输出:[24,12,8,6]
先写一种比较容易想到的方法:
利用对应索引的左侧和右侧(也可以叫前缀和后缀)的乘积来得出答案
举个例子:[1,2,3,4]
我们先算出这个数组的前缀和数组和后缀和数组:
[1,2,6,24] [24,24,12,4]
比如我们要求2这个下标的除自身以外数组的乘积
我们就可以用前缀和数组的第一个元素1,和后缀和数组的倒二个元素12相乘,得出12。
class Solution {public int[] productExceptSelf(int[] nums) {int len = nums.length;int[] left = new int[len];left[0] = nums[0];int[] right = new int[len];right[len-1] = nums[len-1];for(int i=1;i<len;++i){left[i] = nums[i]*left[i-1];}for(int i=len-2;i>=0;i--){right[i] = nums[i]*right[i+1];}int[] ans = new int[len];ans[0] = right[1];ans[len-1] = left[len-2];for(int i=1;i<len-1;++i){ans[i] = left[i-1]*right[i+1];}return ans;}
}
再来一种难一点的方法:
思路就是:
先从上往下算对应索引的左变得乘积,然后再倒上去算对应所以右边得乘积。
类似有点像上三角和下三角得感觉。
//假如nums为[1,2,3,4],那么answer的值分别为[(2,3,4),(1,3,4),(1,2,4),(1,2,3)]//如果吧i当前值相乘的时候看做是1那么就有如下样式// 1, 2, 3, 4 // 1, 1, 3, 4// 1, 2, 1, 4// 1, 2, 3, 1// 他的对角线1将他们分割成了两个三角形,对于answer的元素,//我们可以先计算一个三角形每行的乘积,然后再去计算另外一个三角形每行的乘积,//然后各行相乘,就是answer每个对应的元素
class Solution {public int[] productExceptSelf(int[] nums) {int len = nums.length;int[] left = new int[len];left[0] = 1;for(int i=1;i<len;++i){left[i] = nums[i-1]*left[i-1];}int temp = 1;for(int i=len-1;i>=0;i--){left[i] = left[i]*temp;temp *= nums[i];}return left;}
}
leetcode 1310:子数组异或查询:
有一个正整数数组 arr
,现给你一个对应的查询数组 queries
,其中 queries[i] = [Li, Ri]
。
对于每个查询 i
,请你计算从 Li
到 Ri
的 XOR 值(即 arr[Li] xor arr[Li+1] xor ... xor arr[Ri]
)作为本次查询的结果。
并返回一个包含给定查询 queries
所有结果的数组。
输入:arr = [1,3,4,8], queries = [[0,1],[1,2],[0,3],[3,3]] 输出:[2,7,14,8] 解释: 数组中元素的二进制表示形式是: 1 = 0001 3 = 0011 4 = 0100 8 = 1000 查询的 XOR 值为: [0,1] = 1 xor 3 = 2 [1,2] = 3 xor 4 = 7 [0,3] = 1 xor 3 xor 4 xor 8 = 14 [3,3] = 8
这道题其实蛮简单,不知道为什么力扣设置成中等题
这道题有一个小的注意点,这道题的前缀和数组的第一个元素应该是多少?
这就要去考虑异或的性质了。
- 两个数异或,相同的位数等于0 ;0110 ^ 1100 = 1010
- 一个数与0异或,结果等于本身;
- 一个数异或本身,结果为0;
- 综上,可得第四条性质: a ^ b ^ b = a;
所以由异或性质得出,前缀和数组的第一个数应该是0。
class Solution {public int[] xorQueries(int[] arr, int[][] queries) {int[] xor = new int[arr.length+1];for (int i = 1; i <= arr.length; i++) {xor[i] = xor[i-1] ^ arr[i-1];}int[] ans = new int[queries.length];for (int i = 0; i < queries.length; i++) {ans[i] = xor[queries[i][1]+1] ^ xor[queries[i][0]];}return ans;}
}
leetcode 1829:每个查询的最大异或值:
给你一个 有序 数组 nums
,它由 n
个非负整数组成,同时给你一个整数 maximumBit
。你需要执行以下查询 n
次:
- 找到一个非负整数
k <
2的maximumBit次方 ,使得nums[0] XOR nums[1] XOR ... XOR nums[nums.length-1] XOR k
的结果 最大化 。k
是第i
个查询的答案。 - 从当前数组
nums
删除 最后 一个元素。
请你返回一个数组 answer
,其中 answer[i]
是第 i
个查询的结果。
输入:nums = [0,1,1,3], maximumBit = 2 输出:[0,3,2,3] 解释:查询的答案如下: 第一个查询:nums = [0,1,1,3],k = 0,因为 0 XOR 1 XOR 1 XOR 3 XOR 0 = 3 。 第二个查询:nums = [0,1,1],k = 3,因为 0 XOR 1 XOR 1 XOR 3 = 3 。 第三个查询:nums = [0,1],k = 2,因为 0 XOR 1 XOR 2 = 3 。 第四个查询:nums = [0],k = 3,因为 0 XOR 3 = 3 。
这道题和上一道题差不多,都是异或类型的前缀和,
这一题需要多考虑的一个条件就是这个k的大小,题目说k小于2的maximumBit次方,我们由异或的性质可以知道,我们要想最后的结果尽可能大,我们希望我们异或的对象的1尽可能的多。
知道这个条件之后,这题就很简单了,我们只需要把我们累异或的结果都和这个最大值进行异或就行。
class Solution {public int[] getMaximumXor(int[] nums, int maximumBit) {int flag = (1 << maximumBit) - 1;int[] ans = new int[nums.length];int k = 0;for(int i=0;i<nums.length;i++){k ^= nums[i];ans[nums.length-1-i] = flag ^ k;}return ans;}
}
线性前缀和统计:
leetcode 523:连续的子数组和:
给你一个整数数组 nums
和一个整数 k
,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:
- 子数组大小 至少为 2 ,且
- 子数组元素总和为
k
的倍数。
如果存在,返回 true
;否则,返回 false
。
如果存在一个整数 n
,令整数 x
符合 x = n * k
,则称 x
是 k
的一个倍数。0
始终视为 k
的一个倍数
输入:nums = [23,2,4,6,7], k = 6 输出:true 解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。
先贴一个我一开始写的超时解法:
class Solution {public boolean checkSubarraySum(int[] nums, int k) {int[] sum = new int[nums.length+1];int i,j;if(nums.length==1){return false;}if(nums.length==2){int temp = nums[0]+nums[1];return temp%k==0?true:false;}for(i=1;i<=nums.length;++i){sum[i] += nums[i-1] + sum[i-1];}for(i=0;i< nums.length-1;++i){for(j=(i+2);j<= nums.length;++j){if((sum[j]-sum[i])%k==0){return true;}}}return false;}
}
很明显这是一个n方的解法。
下面贴正确解法:
首先在做这个题目之前,我们需要去明白一个数学关系:
预处理前缀和数组 sum,方便快速求得某一段区间的和。然后假定 [i,j] 是我们的目标区间,那么有:
sum[j]−sum[i−1]=n∗ksum[j] - sum[i - 1] = n * k
sum[j]−sum[i−1]=n∗k
经过简单的变形可得:
sum[j]/k - sum[i-1]/k = n
要使得两者除 k 相减为整数,需要满足 sum[j] 和 sum[i−1]对 k取余相同。
(这个就是我看题解看到的数学关系)下面是证明:
知道了这一层数学关系之后,我们的思路就变成了:
先开一个哈希表。
我们从前缀和数组i=2的情况开始遍历,我们把sum[i-2]%k的这个余数存在哈希表中,
我们每次遍历一个元素,就去查这个哈希表中是否有和我们这个余数相同的数。
这其实和经典题目两数之和有点像了。
class Solution {public boolean checkSubarraySum(int[] nums, int k) {int len = nums.length;int[] sum = new int[len+1];Set<Integer> table = new HashSet<>();for(int i=1;i<=len;++i){sum[i] += sum[i-1] + nums[i-1];}int flag = 0;for(int i=2;i<=len;++i){flag = sum[i-2]%k;table.add(flag);if(table.contains(sum[i]%k)){return true;}}return false;}
}
leetcode 1508:子数组和排序后的区间和:
给你一个数组 nums
,它包含 n
个正整数。你需要计算所有非空连续子数组的和,并将它们按升序排序,得到一个新的包含 n * (n + 1) / 2
个数字的数组。
请你返回在新数组中下标为 left
到 right
(下标从 1 开始)的所有数字和(包括左右端点)。由于答案可能很大,请你将它对 10^9 + 7 取模后返回。
输入:nums = [1,2,3,4], n = 4, left = 1, right = 5 输出:13 解释:所有的子数组和为 1, 3, 6, 10, 2, 5, 9, 3, 7, 4 。将它们升序排序后,我们得到新的数组 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 1 到 ri = 5 的和为 1 + 2 + 3 + 3 + 4 = 13 。
这题其实没有什么技巧,就是一道前缀和的模拟题,我们先求一下前缀和数组,然后开一个长度为n * (n + 1) / 2
个数字的数组,对前缀和数组遍历,把每个子数组的和给求出来,然后排序,最后输出即可。
class Solution {int mod = 1000000007;public int rangeSum(int[] nums, int n, int left, int right) {int[] sum = new int[n+1];int i,j;for(i=1;i<=n;++i){sum[i] = sum[i-1] + nums[i-1];}int[] ans = new int[(n*(n+1))/2];int index = 0;for(i=1;i<=n;++i){for(j=i;j<=n;++j){ans[index++] = sum[j] - sum[i-1];}}Arrays.sort(ans);int result = 0;for(i=left-1;i<right;++i){result += ans[i];result = result%mod;}return result;}
}
这里说一个小坑:
就是最后这个result数据的处理:
我一开始是
Arrays.sort(ans);int result = 0;for(i=left-1;i<right;++i){result += ans[i]%mod;}return result;
这样写有什么问题呢,那个ans[i]会先做取余操作,然后再 + ,就会导致后面的结果溢出。
改成 result = (result + ans[i] )%mod 和 result += ans[i]; result = result%mod;都行。
leetcode 1423:可连续的最大点数
几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints
给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k
张卡牌。
你的点数就是你拿到手中的所有卡牌的点数之和。
给你一个整数数组 cardPoints
和整数 k
,请你返回可以获得的最大点数。
输入:cardPoints = [1,2,3,4,5,6,1], k = 3 输出:12 解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12 。
思路:
这题应该怎么想呢?或者说我们怎么往前缀和那个方面想呢?
我们知道前缀和解决的是连续子数组或者子字符串的问题,我们再看这个题目,题目说我们抽牌可以从前面抽,也可以从后面抽,那这个数组就不一定是连续的了,
这个时候我们就需要逆向思维一下。
我们想要抽出来的牌最大,那就是剩下的牌最小,我们抽牌的数组不一定连续,不过剩下来的数组一定是连续的,所以我们的思路就是去求剩下来的牌的最小值。
具体来说:
我们要抽k张牌,就是要剩下(len-k)张牌,所以我们可以维护一个(len-k)张牌的滑动窗口。
我们一开始直接把这个滑动窗口填满,然后再开始遍历这个数组,不断更新这个连续子数组的最小值即可。
这里说一个题外话,为什么要先填满滑动窗口,其实我一开始的做法不是先填满,是遍历完了(len-k)个元素之后再遍历剩下的元素,不过我感觉这样下标处理起来好麻烦。
class Solution {public int maxScore(int[] cardPoints, int k) {int len = cardPoints.length;int[] sum = new int[len+1];int i,cur = 0,min = Integer.MAX_VALUE;int flag = len-k;for(i=1;i<=len;++i){sum[i] = cardPoints[i-1] + sum[i-1];}for(i=0;i<flag;++i){cur += cardPoints[i];}for(i=flag;i<=len;++i){cur = Math.min(cur,sum[i]-sum[i-flag]);}return sum[len]-cur;}
}
leetcode 1685:有序数值中差绝对值之和:
给你一个 非递减 有序整数数组 nums
。
请你建立并返回一个整数数组 result
,它跟 nums
长度相同,且result[i]
等于 nums[i]
与数组中所有其他元素差的绝对值之和。
换句话说, result[i]
等于 sum(|nums[i]-nums[j]|)
,其中 0 <= j < nums.length
且 j != i
(下标从 0 开始)。
输入:nums = [2,3,5] 输出:[4,3,5] 解释:假设数组下标从 0 开始,那么 result[0] = |2-2| + |2-3| + |2-5| = 0 + 1 + 3 = 4, result[1] = |3-2| + |3-3| + |3-5| = 1 + 0 + 2 = 3, result[2] = |5-2| + |5-3| + |5-5| = 3 + 2 + 0 = 5。
思路:
根据这个例子很容易想到我们可以一种解法:
比如我们要找nums[i]==3这个情况下的result,我们可以用nums[i]*len - sum[len-1],
代码:
class Solution {public int[] getSumAbsoluteDifferences(int[] nums) {int sum = 0;int i;for(i=0;i< nums.length;++i){sum += nums[i];}int[] result = new int[nums.length];for(i=0;i< nums.length;++i){result[i] = Math.abs((nums[i]* nums.length)-sum);}return result;}
}
我们很快就会发现是错误的,因为这题算的是绝对值,
按原来的算法,我们用nums[i]*len - sum[len-1] 得出:3*3-10然后取绝对值,
这样算出来的值是1,不过答案是3,问题就是3-2是>0的,而3-5是<,分开取绝对值的和是3,而我们直接把这两个数合在一起算了。
解决办法是:
我们观察到这个数组是有序的,所以我们可以分段进行求,并且利用前缀和数组进行优化,
class Solution {public int[] getSumAbsoluteDifferences(int[] nums) {int[] sum = new int[nums.length];int len = nums.length;int i,j;sum[0] = nums[0];for(i=1;i< nums.length;++i){sum[i] = sum[i-1] + nums[i];}int[] result = new int[len];for(i=0;i<len;++i){result[i] = ((i+1)*nums[i]-sum[i]) + (sum[len-1]-sum[i]-(len-i-1)*nums[i]);}return result;}
}
leetcode 2155:分组得分最高的所有下标:
给你一个下标从 0 开始的二进制数组 nums
,数组长度为 n
。nums
可以按下标 i
( 0 <= i <= n
)拆分成两个数组(可能为空):numsleft
和 numsright
。
numsleft
包含nums
中从下标0
到i - 1
的所有元素(包括0
和i - 1
),而numsright
包含nums
中从下标i
到n - 1
的所有元素(包括i
和n - 1
)。- 如果
i == 0
,numsleft
为 空 ,而numsright
将包含nums
中的所有元素。 - 如果
i == n
,numsleft
将包含nums
中的所有元素,而numsright
为 空 。
下标 i
的 分组得分 为 numsleft
中 0
的个数和 numsright
中 1
的个数之 和 。
返回 分组得分 最高 的 所有不同下标 。你可以按 任意顺序 返回答案。
输入:nums = [0,0,1,0] 输出:[2,4] 解释:按下标分组 - 0 :numsleft 为 [] 。numsright 为 [0,0,1,0] 。得分为 0 + 1 = 1 。 - 1 :numsleft 为 [0] 。numsright 为 [0,1,0] 。得分为 1 + 1 = 2 。 - 2 :numsleft 为 [0,0] 。numsright 为 [1,0] 。得分为 2 + 1 = 3 。 - 3 :numsleft 为 [0,0,1] 。numsright 为 [0] 。得分为 2 + 0 = 2 。 - 4 :numsleft 为 [0,0,1,0] 。numsright 为 [] 。得分为 3 + 0 = 3 。 下标 2 和 4 都可以得到最高的分组得分 3 。 注意,答案 [4,2] 也被视为正确答案。
思路:
我一开始想的思路是:求出前缀和数组,因为这个数组中只有0和1,所以,我们只要有了前缀和数组,我们可以通过下标的关系很快得出0和1的个数,也就是这个时候的分数。
class Solution {public List<Integer> maxScoreIndices(int[] nums) {int len = nums.length;int[] sum = new int[len+1];for(int i=1;i<=len;++i){sum[i] = sum[i-1] + nums[i-1];}List<Integer> ans = new ArrayList<>();int score = -1;ans.add(score);int temp = 0;for(int i=0;i<=len;++i){temp = Math.abs(sum[i]-(i+1)) + sum[len] - sum[i];if(temp>score){ans.clear();ans.add(i);score = temp;} else if (temp==score) {ans.add(i);}}return ans;}
}
更好的思路:
这里其实可以利用到一点贪心的想法,我们要想让这个分数尽可能大,其实就让左半边尽可能有多的0。
class Solution {public List<Integer> maxScoreIndices(int[] nums) {int len = nums.length;int maxscore = 0;int preSum = 0;List<Integer> ans = new ArrayList<>();ans.add(0);for(int i=0;i<len;++i){if(nums[i]==0){preSum++;if(preSum>maxscore){ans.clear();ans.add(i+1);maxscore = preSum;} else if (preSum==maxscore) {ans.add(i+1);}}else preSum--;}return ans;}
}
这里有一个小细节就是我们要在列表之前先垫一个0,我们可以想象:[1,1]这个数组,里面没有一个0,如果我们不垫这个0,答案就什么都没有。
leetcode 2222 选择建筑的方案数:
给你一个下标从 0 开始的二进制字符串 s
,它表示一条街沿途的建筑类型,其中:
s[i] = '0'
表示第i
栋建筑是一栋办公楼,s[i] = '1'
表示第i
栋建筑是一间餐厅。
作为市政厅的官员,你需要随机 选择 3 栋建筑。然而,为了确保多样性,选出来的 3 栋建筑 相邻 的两栋不能是同一类型。
- 比方说,给你
s = "001101"
,我们不能选择第1
,3
和5
栋建筑,因为得到的子序列是"011"
,有相邻两栋建筑是同一类型,所以 不合 题意。
请你返回可以选择 3 栋建筑的 有效方案数 。
输入:s = "001101" 输出:6 解释: 以下下标集合是合法的: - [0,2,4] ,从 "001101" 得到 "010" - [0,3,4] ,从 "001101" 得到 "010" - [1,2,4] ,从 "001101" 得到 "010" - [1,3,4] ,从 "001101" 得到 "010" - [2,4,5] ,从 "001101" 得到 "101" - [3,4,5] ,从 "001101" 得到 "101" 没有别的合法选择,所以总共有 6 种方法。
思路:
这题和之前的前缀和题目有些不同,这道题更多是用了前缀和的思想。
对任意一个位置,以它为中心构建合法相邻建筑的数量,分两种情况讨论:
该位置左侧1的数量*该位置右侧1的数量 (若该位置为0 )。这样可以构成101。
该位置左侧0的数量*该位置右侧0的数量 (若该位置为1 )。这样可以构成010。
一般前缀和得前缀和数组求得是某一个位置元素前面得和。
这道题求的是某一个位置前面1或者0的数量。
具体算法思路:
- 先从右往左遍历一次数组,算出对应位置右侧0或者1的数量(这个位置如果是0,就算1的数量,相反也一样)
- 再从左往右遍历一次数组,算出对应位置的方案数(因为我们这个时候已经有右侧对应的0和1的数量,这个时候的zeroCount和oneCount分别对应左侧的0和1的数量,我们直接相乘即可得出这个位置上的方案数)
class Solution {public long numberOfWays(String s) {int len = s.length();int[] count = new int[len];char[] str = s.toCharArray();long ans = 0;int i;int oneCount = 0;int zeroCount = 0;//从右往左遍历,统计每个位置右侧0或者1的数量for(i=len-1;i>=0;i--){if(str[i]=='0'){count[i] = oneCount;}else count[i] = zeroCount;oneCount += str[i]=='1'?1:0;zeroCount += str[i]=='0'?1:0;}oneCount = 0; zeroCount = 0;//从左往右遍历,根据count数组算出每个位置对应的方案数for(i=0;i<len;++i){if(str[i]=='0'){count[i] *= oneCount;}else count[i] *= zeroCount;oneCount += str[i]=='1'?1:0;zeroCount += str[i]=='0'?1:0;ans += count[i];}return ans;}
}
线性前缀和+哈希表:
在做这个专题的题目之前,我觉得得先复习一下经典的一道题目:两数之和:
class Solution {public int[] twoSum(int[] nums, int target) {Map <Integer,Integer> table = new HashMap<>();table.put(nums[0],0);for(int i=1;i<nums.length;++i){if(table.containsKey(target-nums[i])){int[] ans = new int[2];ans[0] = table.get(target-nums[i]);ans[1] = i;return ans;}table.put(nums[i],i);}return new int[2];}
}
我感觉在做前缀和和哈希表的题目中总是有两数之和这道题目的影子
两数之和这道题用了哈希表求在一个集合中是否有我们需要的目标值。
而在做前缀和的题目中用哈希表更像是在集合中找我们需要的区间段。
因为我们知道,前缀和就是用来解决子数组的问题
下面上例题:
leetcode 930. 和相同的二元子数组:
给你一个二元数组 nums
,和一个整数 goal
,请你统计并返回有多少个和为 goal
的 非空 子数组。
子数组 是数组的一段连续部分。
输入:nums = [1,0,1,0,1], goal = 2 输出:4 解释: 有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]
class Solution {public int numSubarraysWithSum(int[] nums, int t) {int len = nums.length;Map<Integer,Integer> map = new HashMap<>();map.put(0,1);int[] sum = new int[len+1];int i,j;int ans = 0;for(i=1;i<=len;++i){sum[i] = sum[i-1] + nums[i-1];}for(i=0;i<len;++i){int tar = sum[i+1] - t;if(map.containsKey(tar)){ans += map.get(tar);}map.put(sum[i+1],map.getOrDefault(sum[i+1],0)+1);}return ans;}
}
两数之和中的哈希表存储的是:数组的值和索引
而这道题存储的是:前缀和数组的值和出现的次数
leetcode560: 和为K的子数组:
class Solution {public int subarraySum(int[] nums, int k) {if (nums.length == 0) {return 0;}HashMap<Integer,Integer> map = new HashMap<>();//细节,这里需要预存前缀和为 0 的情况,会漏掉前几位就满足的情况//例如输入[1,1,0],k = 2 如果没有这行代码,则会返回0,漏掉了1+1=2,和1+1+0=2的情况//输入:[3,1,1,0] k = 2时则不会漏掉//因为presum[3] - presum[0]表示前面 3 位的和,所以需要map.put(0,1),垫下底map.put(0, 1);int count = 0;int presum = 0;for (int x : nums) {presum += x;//当前前缀和已知,判断是否含有 presum - k的前缀和,那么我们就知道某一区间的和为 k 了。if (map.containsKey(presum - k)) {count += map.get(presum - k);//获取次数}//更新map.put(presum,map.getOrDefault(presum,0) + 1);}return count;}
}
这道题就和上一道几乎一模一样,哈希表的存储的都是前缀和数组的值和出现的次数。
leetcode 1248:统计优美子数组:
给你一个整数数组 nums
和一个整数 k
。如果某个连续子数组中恰好有 k
个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中 「优美子数组」 的数目。
输入:nums = [1,1,2,1,1], k = 3 输出:2 解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
这道题其实没有用到前缀和数组,不过用到了前缀和的思想。
前缀和数组表示数组下标之前的数组元素的和
这道题用了前缀和的思想,我们不是去统计数组元素的和
而是统计数组下标之前的奇数数字的个数。
做完这道题之后,我感觉对前缀和思想的理解深入了一点,之前有一道题,leetcode2222也用了前缀和的思想,这道题统计的是数组下标之前0或者1元素的个数。那个时候理解起来好别扭,可能是对这个前缀和数组的理解不对,认为做这种题目都需要去求这个前缀和数组。
class Solution {public int numberOfSubarrays(int[] nums, int k) {int len = nums.length;HashMap<Integer,Integer> map = new HashMap<>();//哈希表记录的是前缀区间中的奇数个数map.put(0,1);int ans = 0;int oddnum = 0;int i,j;for(i=0;i<len;++i){oddnum += nums[i] & 1;if(map.containsKey(oddnum-k)){ans += map.get(oddnum-k);}map.put(oddnum,map.getOrDefault(oddnum,0)+1);}return ans;}
}
leetcode 974:和可被K整除的子数组:
给定一个整数数组 nums
和一个整数 k
,返回其中元素之和可被 k
整除的(连续、非空) 子数组 的数目。
子数组 是数组的 连续 部分。
输入:nums = [4,5,0,-2,-3,1], k = 5 输出:7 解释: 有 7 个子数组满足其元素之和可被 k = 5 整除: [4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
其实写了三个题目了,已经就是有感觉了,这道题和前几道题有什么不同呢?特别是和为K的子数组,
在和为K的子数组中,我们的判断条件是:是否含有sum[i] - k的前缀和
而在这一题中,我们的判断条件是:是否含有sum[i] %k相同的前缀和。
知道了这一点之后,这题就和之前的题目差不多了。
class Solution {public int subarraysDivByK(int[] nums, int k) {int len = nums.length;HashMap<Integer,Integer> map = new HashMap<>();map.put(0,1);int i,j;int ans = 0;int[] sum = new int[len+1];for(i=1;i<=len;++i){sum[i] = sum[i-1] + nums[i-1];}for(i=0;i<len;++i){int key = (sum[i+1] % k + k) % k;if(map.containsKey(key)){ans += map.get(key);}map.put(key,map.getOrDefault(key,0)+1);}return ans;}
}
注意点:
我们看到上面代码中有一段代码是这样的
int key = (presum % K + K) % K;
这是为什么呢?不能直接用 presum % k 吗?这是因为当我们 presum 为负数时,需要对其纠正。纠正前(-1) %2 = (-1),纠正之后 ( (-1) % 2 + 2) % 2=1 保存在哈希表中的则为 1.则不会漏掉部分情况,例如输入为 [-1,2,9],K = 2如果不对其纠正则会漏掉区间 [2] 此时 2 % 2 = 0,符合条件,但是不会被计数。
我觉得这个取余的方法也可以记下来,用来处理一些碰到负数取余的情况。
差分:
差分可以看成前缀和的逆运算
详细差分算法理解看这个博客:前缀和与差分 图文并茂 超详细整理(全网最通俗易懂)-CSDN博客
我们稍微对比一下前缀和和差分的例子,来理解一下这两个概念:
前缀和的作用很清楚了:
如果我们要求一个数组中某一段区间的数组和,我们用前缀和就可以在O(1)的时间内查找出来。
而差分呢?
我们想象一个场景:我们有一个数组,我们要在规定的区间 [l,r] 这个区间内+c ,我们同样得遍历,如果我们需要做n次这样的操作,那时间复杂度就很高了。
所以差分数组就是用来解决这个问题的。
对 c[l]+=v:由于差分是前缀和的逆向过程,这个操作对于将来的查询而言,带来的影响是对于所有的下标大于等于 l 的位置都增加了值 v;
对 c[r+1]−=v:由于我们期望只对 [l,r] 产生影响,因此需要对下标大于 r 的位置进行减值操作,从而抵消“影响”。
最后我们再用前缀和的运算来输出结果即可。
leetcode 1109 航班预定统计:
这里有 n
个航班,它们分别从 1
到 n
进行编号。
有一份航班预订表 bookings
,表中第 i
条预订记录 bookings[i] = [firsti, lasti, seatsi]
意味着在从 firsti
到 lasti
(包含 firsti
和 lasti
)的 每个航班 上预订了 seatsi
个座位。
请你返回一个长度为 n
的数组 answer
,里面的元素是每个航班预定的座位总数。
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5 输出:[10,55,45,25,25] 解释: 航班编号 1 2 3 4 5 预订记录 1 : 10 10 预订记录 2 : 20 20 预订记录 3 : 25 25 25 25 总座位数: 10 55 45 25 25 因此,answer = [10,55,45,25,25]
这道题就是典型的差分题,
我们遍历bookings数组,对数组中[l,r]区间加上对应的位置。
在(r,len)再减去对应的位置,达到平衡。
最后求出前缀和。
class Solution {public int[] corpFlightBookings(int[][] bs, int n) {int[] diff = new int[n+2];for(int[] b:bs){diff[b[0]] += b[2];diff[b[1]+1] -= b[2];}int[] ans = new int[n];ans[0] = diff[1];for(int i=1;i<n;++i){ans[i] = ans[i-1]+diff[i+1];}return ans;}
}
相关文章:

前缀和,差分算法理解
前缀和是什么: 前缀和指一个数组的某下标之前的所有数组元素的和(包含其自身)。前缀和分为一维前缀和,以及二维前缀和。前缀和是一种重要的预处理,能够降低算法的时间复杂度 说个人话就是比如有一个数组: …...
ubuntu/部分docker容器无法访问https站点
ubuntu/部分docker容器无法访问https站点 解决方案 解决方案 默认的系统内可能没有安装根证书,需要安装一下 apt install ca-certificates如果官方源比较慢,可以换为国内源,但是不要使用https...

【MySQL】库的基础操作
🌎库的操作 文章目录: 库的操作 创建删除数据库 数据库编码集和校验集 数据库的增删查改 数据库查找 数据库修改 备份和恢复 查看数据库连接情况 总结 前言: 数据库操作是软件开发中不可或缺的一部分࿰…...
嵌入式0基础开始学习 ⅠC语言(2)运算符与表达式
1.运算符 什么是运算符? 用来进来某种运算的符号 如: - * / (取余,取模) a,几目运算符 根据其操作数的不同 单目运算符 该运算符…...
汇编语言(一)
寄存器:cpu中可以储存数据的器件(AX,BX) 汇编语言的组成:1.汇编指令 2.伪指令 3.其他符号 存储器:cpu,传入指令和数据,加以运算。(内存) 指令和数据&#…...

2010-2022年各省新质生产力数据(含原始数据+测算代码+计算结果)
2010-2022年各省新质生产力数据(含原始数据测算代码计算结果) 1、时间:2010-2022年 2、范围:31省 3、指标:gdp(亿元)、在岗职工工资:元、第三产业就业比重、人均受教育平均年限、…...

需求分析部分图形工具
描述复杂的事物时,图形远比文字叙述优越得多,它形象直观容易理解。前面已经介绍了用于建立功能模型的数据流图、用于建立数据模型的实体-联系图和用于建立行为模型的状态图,本节再简要地介绍在需求分析阶段可能用到的另外3种图形工具。 1 层次方框图 层次方框图用树形结…...

ML307R OpenCPU GPIO使用
一、GPIO使用流程图 二、函数介绍 三、GPIO 点亮LED 四、代码下载地址 一、GPIO使用流程图 这个图是官网找到的,ML307R GPIO引脚电平默认为1.8V,需注意和外部电路的电平匹配,具体可参考《ML307R_硬件设计手册_OpenCPU版本适用.pdf》中的描…...

python基于深度学习的聊天机器人设计
python基于深度学习的聊天机器人设计 开发语言:Python 数据库:MySQL所用到的知识:Django框架工具:pycharm、Navicat、Maven 系统功能实现 登录注册功能 用户在没有登录自己的用户名之前只能浏览本网站的首页,想要使用其他功能都…...
Golang设计模式(四):观察者模式
观察者模式 什么是观察者 观察者模式(Observer Pattern):定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe…...

huggingface 笔记:查看GPU占用情况
0 准备部分 0.1 创建虚拟数据 import numpy as npfrom datasets import Datasetseq_len, dataset_size 512, 512 dummy_data {"input_ids": np.random.randint(100, 30000, (dataset_size, seq_len)),"labels": np.random.randint(0, 1, (dataset_size…...

JavaSE 学习记录
1. Java 内存 2. this VS super this和super是两个关键字,用于引用当前对象和其父类对象 this 关键字: this 关键字用于引用当前对象,即调用该关键字的方法所属的对象。 主要用途包括: 在类的实例方法中,通过 this …...

HTML与CSS的学习
什么是HTML,CSS? HTML(HyperText Markup Language):超文本标记语言。 超文本:超越了文本的限制,比普通文本更强大。除了文字信息,还可以定义图片、音频、视频等 标记语言:由标签构成的语言 >HTML标签都是预定义好的。例如:使用<a>…...

【单片机】STM32F070F6P6 开发指南(一)STM32建立HAL工程
文章目录 一、基础入门二、工程初步建立三、HSE 和 LSE 时钟源设置四、时钟系统(时钟树)配置五、GPIO 功能引脚配置六、配置 Debug 选项七、生成工程源码八、生成工程源码九、用户程序下载 一、基础入门 f0 pack下载: https://www.keil.arm…...

源码编译安装Rsync数据同步
源码编译安装 RPM软件包:rpm -ivh 或者 yum -y install 需要开发编译工具gcc、gcc-c、make等... 源码包----开发工具gcc与make----》可以执行的程序-----》运行安装 •主要优点 –获得软件的最新版,及时修复bug –软件功能可按需选择/定制ÿ…...

SQL Server2019安装步骤教程(图文)_最新教程
一、下载SQL Server2019 1.到微软官网下载SQL Server Developer版本,官网当前的2019版本下载需要注册账号。 不想注册的朋友,可以选择从网盘下载:点击此处直接下载 2.下载之后先解压,解压后执行exe安装程序。打开之后的界面如下…...

【SpringBoot】SpringBoot中防止接口重复提交(单机环境和分布式环境)
📝个人主页:哈__ 期待您的关注 目录 🌼前言 🔒单机环境下防止接口重复提交 📕导入依赖 📂项目结构 🚀创建自定义注解 ✈创建AOP切面 🚗创建Conotroller 💻分布…...
零基础学Java(全170集)
课程概述 本课程旨在全面深化对 Java 语言的核心技术理解,并提升编程实践能力。课程内容涵盖以下关键领域: 掌握Java核心语法,为后续学习打下扎实的基础。熟练运用Java常用的类库与开发工具,提高开发效率与质量。解决面向对象编…...

摄像头应用测试
作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生在读,研究方向无线联邦学习 擅长领域:驱动开发,嵌入式软件开发,BSP开发 作者主页:一个平凡而乐于分享的小比特的个人主页…...
Golang框架HTTP客户端框架zdpgo_resty发送表单请求
核心代码 这里通过字典传递了一个简单的表单数据。 发送的是POST请求。 resp, err : client.R().SetFormData(map[string]string{"username": "jeeva","password": "mypass",}).Post("http://127.0.0.1:3333/login")fmt.P…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...

(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...