子数组相关题目:前缀和技巧
程序员文章站
2022-04-02 21:28:01
...
文章目录
- 前缀和介绍
- [LeetCode53-Maximum Subarray](https://leetcode.com/problems/maximum-subarray/submissions/)
- [LintCode44-Minimun Subarray](https://www.lintcode.com/problem/minimum-subarray/description)
- [LintCode138-Subarray Sum](https://www.lintcode.com/problem/subarray-sum/description)
- [LintCode139-Subarray Sum Closest](https://www.lintcode.com/problem/subarray-sum-closest/description)
前缀和介绍
-
假设有数组
A
且A.length = n
-
则可新建一前缀和数组
preSumArray
其长度为n+1
-
前缀和数组定义如下:
preSumArray[0] = 0
preSumArray[1] = A[0]
preSumArray[2] = A[0] +A[1]
............
preSumArray[i] = A[0] + A[1] + ........ + A[i-1]
.........................
preSumArray[n] = A[0] + A[1] + ........ + A[n-1]
-
前缀和数组有如下性质
sum(i~j) = preSumArray[j+1] - preSumArray[i]
preSumArray[j] - preSumArray[i] = Sum(i~(j - 1))
-
这个也很容易证明嘛:
preSumArray[j+1] = A[0] + A[1] + ..+ A[i-1] + ... + A[j]
preSumArray[i] = A[0] + A[1]+ .... + A[i-1]
- 二者之差等于:
A[i] + A[i+1] + .... + A[j] = sum(i~j)
-
显然,构造前缀和数组的时间和空间复杂度均为
O(n)
LeetCode53-Maximum Subarray
题意
- 给定数组,求解此数组中连续子数组之和最大的,返回这个最大值
思路
- 先求解该数组的前缀和数组
- 然后遍历前缀和数组
- 分别维护一个最大值和最小值,因为最大减最小的和最大嘛,这样遍历完成后得到一段连续数组和的最大值
代码
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0){
return 0;
}
//0. calc the preSumArray
int n = nums.length;
int[] preSumArray = new int[n+1];
preSumArray[0] = 0;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += nums[i];
preSumArray[i+1] = sum;
}
//1. find the max sum of the SubArray in the nums
//1.1 最大和的子数组一定等于 最大值减去最小值
int maxSum = Integer.MIN_VALUE;
int minSum = 0;
//1.2 遍历preSumArray
for (int i = 1; i < preSumArray.length; i++) {
maxSum = Math.max(maxSum, preSumArray[i] - minSum);
minSum = Math.min(minSum, preSumArray[i]);
}
return maxSum;
}
LintCode44-Minimun Subarray
题意
- 和上一题完全一样,只不过这一题求最小值
思路
- 可以先把给定数组的元素取反得到新数组
- 然后利用上一题的做法对新数组求最大值
- 然后返回最大值的相反数
代码
public static int minSubArray(List<Integer> nums) {
//0.先对给定list的元素取反
int[] A = new int[nums.size()];
int index = 0;
for (Integer num : nums) {
A[index++] = -num;
}
//1.然后对数组A,求解连续子数组和的最大值
//1.1 依然采用前缀和技巧求解
int[] preSumArray = new int[nums.size() + 1];
preSumArray[0] = 0;
int sum = 0;
for (int i = 1; i < preSumArray.length; i++) {
sum += A[i-1];
preSumArray[i] = sum;
}
//1.2 根据前缀和求解最大值
int maxSum = Integer.MIN_VALUE;
int minSum = 0;
for (int i = 1; i < preSumArray.length; i++) {
maxSum = Math.max(maxSum, preSumArray[i] - minSum);
minSum = Math.min(minSum, preSumArray[i]);
}
//2. 最大对最大值取反,返回
return -maxSum;
}
LintCode138-Subarray Sum
题意
给定数组,求解和为0的子数组,返回子数组的左右端点的下标
思路
- 求出该数组的前缀和数组
- 在前缀和数组中找到两个元素相等的两个数即可
- 然后返回其下标
- 关键在于前缀和数组是无序的,如何才能找到两个相同的数,并同时记录下标信息呢?
- 在数组相关的题目中,经常需要把index和value绑定在一起,有两种方式可以处理
- 利用
map
- 自定义一个类
Pair
,定义两个成员变量,分别是index
和value
- 利用
代码
public List<Integer> subarraySum(int[] nums) {
List<Integer> res = new ArrayList<>();
if (nums == null || nums.length == 0){
return res;
}
//0. 依然计算前缀和,但是需要把前缀和index关联起来
//0.1 这是为了后面排序后依然有前缀和的信息
int n = nums.length;
int sum = 0;
//0.2 sum -> index
Map<Integer,Integer> map = new HashMap<>();
map.put(0, 0);
for (int i = 0; i < n ; i++) {
sum += nums[i];
//1. 在前缀和数组建立的过程中去完成查找,有效的解决了重复的sum问题
if (map.containsKey(sum)){
res.add(map.get(sum));
res.add(i);
return res;
}
map.put(sum, i+1);
}
return res;
}
LintCode139-Subarray Sum Closest
题意
- 给定数组,找出和与0最接近的子数组,返回子数组两端的下标
思路
- 和上题基本一致
- 仍然先求出前缀和数组
- 然后对前缀和数组排序
- 然后依次找前缀和数组中相邻两个数的差,记录两数之差最小的
- 然后记录下这两个数的下标
- 这里又需要把
value和index
绑定在一起,而且还需要对value
排序,所以这里使用Pair类
代码
class LintCode139 {
class Pair{
int index;
int preSum;
public Pair(int index, int preSum){
this.index = index;
this.preSum = preSum;
}
}
public int[] subarraySumClosest(int[] nums) {
int[] res = new int[2];
if (nums == null || nums.length == 0){
return null;
}
//0. 依然计算前缀和,但是需要把前缀和index关联起来
//0.1 这是为了后面排序后依然有前缀和的信息
int n = nums.length;
int sum = 0;
Pair[] pairs = new Pair[n + 1];
pairs[0] = new Pair(0, 0);
//0.2 注意这里按照前缀和数组的下标在进行初始化
for (int i = 1; i < pairs.length; i++) {
sum += nums[i-1];
pairs[i] = new Pair(i, sum);
}
//1. 对pairs按照preSum排序
Arrays.sort(pairs,new Comparator<Pair>(){
@Override
public int compare(Pair o1, Pair o2) {
return o1.preSum - o2.preSum;
}
});
//2. 现在是升序排序,那么差最小的二者一定是相邻的二者
int min = Integer.MAX_VALUE;
for (int i = 0; i < n - 1; i++) {
int curMin = pairs[i+1].preSum - pairs[i].preSum;
if (curMin < min){
min = curMin;
//2.1记录index
if (pairs[i+1].index < pairs[i].index){
res[0] = pairs[i+1].index;
//2.2 -1是因为 preSum[j] - preSum[i] 表示的是 nums中 index = i 到 index = j - 1的和
// 这里的index显然应该是nums中的index,所以需要-1
res[1] = pairs[i].index - 1;
}else {
res[0] = pairs[i].index;
res[1] = pairs[i+1].index - 1;
}
}
}
return res;
}
}
细节
-
preSum[j] - preSum[i]
表示的是nums
中index = i 到 index = j - 1
的和 - 所以最后求解的时候,
index
较大者需要-1
才是nums中元素对应的下标
推荐阅读