算法笔记:数组累加和三连问题深度解析
本文将从技术专家视角,深入剖析数组累加和问题的三种典型场景及其解决方案,帮助读者掌握这类算法问题的核心思路和实现技巧。
一、系统设计基础:UUID生成方案
在深入数组问题前,我们先探讨一个相关的系统设计问题:如何设计一个高并发的UUID生成系统。这个案例展示了垂直扩展思想在实际工程中的应用。
核心思路:
- 中央服务器负责分配ID范围段(start和range)
- 各国/地区服务器向中央服务器申请ID段
- 当本地ID段耗尽时可再次申请
- 中央服务器维护已分配位置,确保不重复
这种设计避免了传统水平扩展方案(如hashcode、random等)可能产生的碰撞问题,特别适合需要严格唯一性的场景。
二、数组累加和问题三连
2.1 正数数组的累加和问题
问题描述:给定一个正数数组和正整数sum,求累加和等于sum的最长子数组长度。
示例: 数组:[3,2,1,1,1,6,1,1,1,1,1,1] sum=6 最长子数组:[1,1,1,1,1,1],长度6
解法思路: 利用正数数组的单调性特点,采用滑动窗口算法:
- 初始化窗口左右边界L=R=0,窗口和windowSum=arr[0]
- 比较windowSum与sum:
- 小于sum:右边界R右移,扩大窗口
- 大于sum:左边界L右移,缩小窗口
- 等于sum:记录窗口大小,左边界右移
代码实现:
public static int getMaxLength(int[] arr, int K) {
if (arr == null || arr.length == 0 || K <= 0) return 0;
int left = 0, right = 0;
int sum = arr[0];
int len = 0;
while (right < arr.length) {
if (sum == K) {
len = Math.max(len, right - left + 1);
sum -= arr[left++];
} else if (sum < K) {
right++;
if (right == arr.length) break;
sum += arr[right];
} else {
sum -= arr[left++];
}
}
return len;
}
2.2 任意数组的累加和问题
问题描述:数组元素可正可负可零,求累加和等于sum的最长子数组长度。
解法思路: 利用前缀和+哈希表:
- 维护一个哈希表记录前缀和及其最早出现位置
- 计算当前前缀和allSum
- 查找哈希表中是否存在allSum-sum
- 若存在,则对应位置j到当前位置i的子数组和为sum
关键点:
- 预置map.put(0, -1)处理从数组开头开始的子数组
- 只记录前缀和第一次出现的位置以保证最长子数组
代码实现:
public static int maxLength(int[] arr, int k) {
if (arr == null || arr.length == 0) return 0;
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, -1); // 关键初始化
int len = 0, sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
if (map.containsKey(sum - k)) {
len = Math.max(len, i - map.get(sum - k));
}
if (!map.containsKey(sum)) {
map.put(sum, i);
}
}
return len;
}
变体问题:求1和2数量相等的子数组 解法:将1保持为1,2变为-1,其他为0,转化为求和为0的子数组问题
2.3 累加和小于等于k的最长子数组
问题描述:数组元素可正可负可零,求累加和≤k的最长子数组长度。
解法思路:
- 预处理两个辅助数组:
- minSums[i]:从i开始的最小累加和
- minSumEnds[i]:对应最小累加和的结束位置
- 使用滑动窗口思想,利用预处理信息快速判断窗口扩展可能性
代码实现:
public static int maxLengthAwesome(int[] arr, int k) {
if (arr == null || arr.length == 0) return 0;
// 预处理minSums和minSumEnds
int[] minSums = new int[arr.length];
int[] minSumEnds = new int[arr.length];
minSums[arr.length-1] = arr[arr.length-1];
minSumEnds[arr.length-1] = arr.length-1;
for (int i = arr.length-2; i >= 0; i--) {
if (minSums[i+1] < 0) {
minSums[i] = arr[i] + minSums[i+1];
minSumEnds[i] = minSumEnds[i+1];
} else {
minSums[i] = arr[i];
minSumEnds[i] = i;
}
}
// 滑动窗口求解
int end = 0, sum = 0, res = 0;
for (int i = 0; i < arr.length; i++) {
while (end < arr.length && sum + minSums[end] <= k) {
sum += minSums[end];
end = minSumEnds[end] + 1;
}
res = Math.max(res, end - i);
if (end > i) {
sum -= arr[i];
} else {
end = i + 1;
}
}
return res;
}
三、总结与思考
- 正数数组:利用单调性,滑动窗口是最高效的解法
- 任意数组:前缀和+哈希表是通用解法,时间复杂度O(n)
- ≤k问题:需要巧妙的预处理和滑动窗口结合
这些算法问题展示了如何根据不同数据特性选择最优解法,也体现了预处理思想在优化算法中的重要性。理解这些问题有助于培养对数组类问题的敏感度和解题直觉。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考