问题描述
小R遇到一个数组 nums,他需要从中找到两个非重叠的子数组,它们的长度分别为 firstLen 和 secondLen。这两个子数组可以相互独立,顺序没有限制,但它们不能有任何重叠。你需要帮小R找出这些子数组的最大和。
测试样例
样例1:
输入:
nums = [0,6,5,2,2,5,1,9,4], firstLen = 1, secondLen = 2
输出:20
样例2:
输入:
nums = [3,8,1,3,5,2,1,0], firstLen = 3, secondLen = 2
输出:21
样例3:
输入:
nums = [2,1,4,3,5,9,5,0,3,8], firstLen = 4, secondLen = 3
输出:33
解析:
这个问题的核心在于找到两个不重叠的子数组,使得这两个子数组的和最大。解决这个问题的关键在于如何高效地计算子数组的和,并且能够快速地比较不同子数组的和。
首先,我们定义了一个辅助函数 sumNumWithIndex,这个函数用于计算数组中从 start 到 end(左闭右开区间)的元素和。这个函数通过遍历指定区间的元素并累加它们的值来计算和。
在 solution 函数中,我们首先初始化了两个数组 firstSum 和 secondSum,它们分别用于存储长度为 firstLen 和 secondLen 的子数组的和。接下来,我们通过遍历原数组 nums 来填充这两个数组。对于 firstSum 和 secondSum 的每个元素,我们使用前一个元素的和减去窗口左侧的元素加上窗口右侧的新元素来更新当前的和,这样可以避免重复计算。
在填充完 firstSum 和 secondSum 后,我们开始寻找两个不重叠子数组的最大和。我们使用两个指针,分别代表两个子数组的起始位置,并遍历 firstSum 数组来找到可能的组合。对于 firstSum 中的每个元素,我们检查它左边和右边是否有足够的空间放置长度为 secondLen 的子数组。如果可以,我们就计算这两个子数组的和,并更新最大和 res。
在遍历过程中,我们使用嵌套循环来比较所有可能的子数组组合。对于每个 firstSum[i],我们分别考虑它左边和右边的 secondSum[j],并计算它们的和。如果这个和大于当前的最大和 res,我们就更新 res。
最后,函数返回 res,即两个不重叠子数组的最大和。
import java.util.Arrays;
public class 非重叠子数组最大和 {
/**
* 这个函数计算数组某个区间内所有数的和,是左闭右开区间
* @param start
* @param end
* @param nums
* @return
*/
public static int sumNumWithIndex(int start,int end,int[] nums){
int sum=0;
for (int i = start; i < end; i++) {
sum+=nums[i];
}
return sum;
}
//上面的思路其实也是不太行
public static int solution(int[] nums, int firstLen, int secondLen) {
int length=nums.length;
int[] firstSum=new int[length];
int[] secondSum=new int[length];
firstSum[firstLen-1]=sumNumWithIndex(0,firstLen,nums);
secondSum[secondLen-1]=sumNumWithIndex(0,secondLen,nums);
//前面的数java会自动赋值为0,不用管他了
int i,start=0;
for(i=firstLen;i<length;i++){
firstSum[i]=firstSum[i-1]-nums[start++]+nums[i];
}
start=0;
for(i=secondLen;i<length;i++){
secondSum[i]=secondSum[i-1]-nums[start++]+nums[i];
}
//System.out.println(Arrays.toString(firstSum));
//System.out.println(Arrays.toString(secondSum));
//测试没毛病
int totalLen=firstLen+secondLen;
int secondLeft=totalLen-1,secondrigth=length-secondLen;
int temp,res=0;
for(i=firstLen-1;i<length;i++){
//左边可以容得下second时
if(i>=secondLeft){
//左边滑动窗口结束的位置
int secondend=i-firstLen;
for(int j=secondLen-1;j<=secondend;j++){
temp=firstSum[i]+secondSum[j];
res=Math.max(res,temp);
}
}
//右边右second的情况
if(i<=secondrigth){
int secondstart=i+secondLen;
for(int j=secondstart;j<length;j++){
temp=firstSum[i]+secondSum[j];
res=Math.max(res,temp);
}
}
}
//System.out.println("res:"+res);
return res;
}
public static void main(String[] args) {
System.out.println(solution(new int[]{0, 6, 5, 2, 2, 5, 1, 9, 4}, 1, 2) == 20);
System.out.println(solution(new int[]{3, 8, 1, 3, 5, 2, 1, 0}, 3, 2) == 21);
System.out.println(solution(new int[]{2, 1, 4, 3, 5, 9, 5, 0, 3, 8}, 4, 3) == 33);
System.out.println(solution(new int[]{12,12,0,11,10,13,10,16,2,0,0,13,4},2,5)==84);
/*TODO:经过验证,java的++i和i++跟C++的机制一样!
int i=0;
System.out.println("i=0;++i="+(++i));
i=0;
System.out.println("i=0;i++="+(i++));
System.out.println(i);*/
}
}
总结: 这个问题的解决方案是一个典型的动态规划问题,它涉及到前缀和的应用。通过预先计算子数组的和,我们可以将原本复杂度为 O(n^2) 的问题降低到 O(n) 的复杂度,从而高效地找到最大和。 代码的关键点在于:
- 使用前缀和数组
firstSum和secondSum来存储子数组的和,这样可以在 O(1) 的时间复杂度内获取任意子数组的和。 - 通过遍历
firstSum数组,并结合secondSum数组,我们可以找到所有可能的不重叠子数组组合,并计算出它们的和。 - 使用嵌套循环来比较所有可能的组合,并更新最大和。 这个问题的解决方案虽然有效,但是存在一些可以优化的地方。例如,嵌套循环会导致时间复杂度上升,特别是在大数据集上。一种可能的优化方法是使用双指针技术来减少不必要的比较,或者进一步优化前缀和的计算过程。
主要用到了以下算法和概念:
-
前缀和(Prefix Sum)算法:
- 代码中的
firstSum和secondSum数组就是前缀和数组的实现。前缀和是一种用于快速计算数组中任意子数组元素和的技巧。通过预先计算并存储从数组开始到当前位置的所有元素之和,可以在常数时间内计算任意子数组的和。
- 代码中的
-
动态规划(Dynamic Programming) :
- 虽然代码中没有明显的动态规划表,但是通过前缀和的计算和更新过程,实际上隐含了动态规划的思想。动态规划通常用于解决具有重叠子问题和最优子结构性质的问题,而在这个问题中,通过前缀和数组,我们避免了重复计算子数组的和,这与动态规划中的“记忆化”概念相似。
-
滑动窗口(Sliding Window) :
- 在更新
firstSum和secondSum数组时,代码使用了滑动窗口的技术。滑动窗口是一种常用的数组/列表处理技术,通过移动窗口的起始和结束位置来计算窗口内的元素和或其他属性。
- 在更新
-
双指针(Two Pointers) :
- 在寻找两个不重叠子数组的最大和时,代码通过两个嵌套循环来模拟双指针的操作。外层循环遍历
firstSum数组,内层循环分别向左和向右寻找secondSum数组中的最大值,这可以看作是一种双指针的应用。
- 在寻找两个不重叠子数组的最大和时,代码通过两个嵌套循环来模拟双指针的操作。外层循环遍历