LeetCode 3637题解:单次线性扫描实现三段式数组分割
1. 问题背景与题目解析今天我们来拆解LeetCode第3637题三段式数组I的第一种解法——单次线性扫描。这是一道典型的数组操作题目在最近的周赛中出现频率较高考察我们对数组遍历和边界条件处理的掌握程度。题目描述是这样的给定一个整数数组nums我们需要判断它是否可以被分成三个连续的非空子数组使得第一个子数组的和等于第三个子数组的和且第二个子数组的和等于它们的和。换句话说寻找索引i和j1 ≤ i j nums.length-1使得sum(nums[0..i]) sum(nums[j1..n-1])sum(nums[i1..j]) sum(nums[0..i])举个例子对于数组[1,2,1,2,1]我们可以选择i1和j3第一部分[1,2]和为3第二部分[1]和为1第三部分[2,1]和为3 这不满足条件。但如果选择i1和j2第一部分[1,2]和为3第二部分[1]和为1第三部分[2,1]和为3 仍然不满足。实际上这个数组没有解。而数组[1,3,1,1,1]在i1,j3时[1,3]4[1]1[1,1]2 也不满足。但i1,j2时[1,3]4[1]1[1,1]2 依然不满足。看来这道题比表面看起来要复杂。2. 暴力解法与初步优化最直观的解法是暴力枚举所有可能的i和j组合然后计算三个子数组的和进行比较。这种方法的时间复杂度是O(n^3)对于较大的数组显然不可行。我们可以先进行一些优化。首先预处理前缀和数组prefix其中prefix[i]表示nums[0..i-1]的和。这样计算任意子数组的和可以在O(1)时间内完成。优化后的暴力解法如下def threeParts(nums): n len(nums) prefix [0] * (n 1) for i in range(n): prefix[i1] prefix[i] nums[i] for i in range(1, n-1): for j in range(i1, n-1): sum1 prefix[i] - prefix[0] sum2 prefix[j] - prefix[i] sum3 prefix[n] - prefix[j] if sum1 sum3 and sum2 sum1: return True return False这个解法将时间复杂度降到了O(n^2)但对于n10^5的情况仍然不够高效。我们需要更聪明的办法。3. 单次线性扫描解法观察题目条件我们可以发现一些关键性质整个数组的总和sum_all sum1 sum2 sum3根据条件sum1 sum3sum2 sum1因此sum_all 3*sum1这意味着整个数组的和必须是3的倍数否则直接返回False。这给了我们一个强有力的剪枝条件。基于这个观察我们可以设计一个线性扫描算法首先计算整个数组的总和sum_all如果sum_all % 3 ! 0直接返回False否则目标sum_target sum_all // 3我们需要找到两个位置i和j使得前缀到i的和为sum_target前缀到j的和为2*sum_target且i和j之间有至少一个元素(j ≥ i2)具体实现时我们可以从左到右扫描数组累加当前和curr_sum当curr_sum sum_target时记录第一个分割点i当curr_sum 2*sum_target时记录第二个分割点j确保i和j都存在且i j以下是Python实现def threeParts(nums): sum_all sum(nums) if sum_all % 3 ! 0: return False target sum_all // 3 n len(nums) curr_sum 0 i, j -1, -1 for idx in range(n): curr_sum nums[idx] if curr_sum target and i -1: i idx elif curr_sum 2 * target and i ! -1 and idx n - 1: j idx return True return False这个算法只需要一次线性扫描时间复杂度O(n)空间复杂度O(1)非常高效。4. 边界条件与测试用例在实现这类数组分割问题时特别需要注意边界条件。让我们考虑几个关键测试用例最小长度数组[1,1,1] → True (i0,j1)[1,2,1] → False全零数组[0,0,0,0,0] → True (任何i,j都满足)无法三等分[1,2,3,4,5] → False (sum15,但无法分割)多个可能分割点[1,0,1,0,1] → True (i1,j3或i2,j3)大数测试[1000000000]*10000 → 需要考虑数值溢出问题在实现时我们还需要注意数组长度至少为3分割点i和j不能重合最后一个子数组不能为空5. 算法优化与变种这个线性扫描算法已经很高效但还可以做一些优化提前终止当找到j后可以立即返回不需要继续扫描反向扫描也可以从右向左扫描寻找sum_target和2*sum_target双指针法维护两个指针分别寻找第一个和第二个分割点对于类似的数组分割问题这个思路可以推广。例如判断数组能否分成两部分等和 → 寻找sum_all/2判断数组能否分成k部分等和 → 寻找sum_all/k的倍数点这类问题的核心都是通过总和判断可能性通过前缀和定位分割点线性扫描实现高效求解6. 实际应用与扩展这种数组分割问题在实际中有许多应用场景负载均衡将任务分成多个部分均匀分配文件分割大文件分块处理数据分析将数据集分成训练集、验证集和测试集对于更复杂的情况比如不均匀分割比例不同多维数组分割带权重的分割我们可以基于类似的思路进行扩展结合动态规划等其他算法技巧。7. 代码实现细节与调试技巧在实际编码时有几个容易出错的地方需要注意索引处理Python是0-based索引要确保分割点在合法范围内整数除法使用//而不是/避免浮点数问题空子数组确保每个子数组至少有一个元素大数处理对于特别大的数考虑使用long类型调试时可以打印中间变量curr_sum, i, j使用小测试用例手动验证检查边界条件空数组、最小数组等一个更健壮的实现可能包括def threeParts(nums): if len(nums) 3: return False sum_all sum(nums) if sum_all % 3 ! 0: return False target sum_all // 3 curr_sum 0 count 0 for num in nums[:-1]: # 确保最后一个子数组非空 curr_sum num if curr_sum target * (count 1): count 1 if count 2: return True return False这个版本通过计数的方式更清晰地表达了我们要找两个分割点的意图。8. 性能分析与比较让我们分析不同解法的时间空间复杂度暴力解法时间O(n^3)空间O(1)前缀和优化暴力时间O(n^2)空间O(n)线性扫描时间O(n)空间O(1)在实际测试中对于n10^5的数组暴力解法完全不可行前缀和优化需要约10^10次操作仍然太慢线性扫描仅需10^5次操作可以在毫秒级完成这说明算法选择对性能的影响是决定性的。在面试或竞赛中能够快速识别问题本质并选择最优算法至关重要。9. 常见错误与避坑指南在解决这个问题时我见过几种常见错误忽略总和检查直接开始找分割点导致无效计算错误的分割点顺序没有确保i j空子数组允许最后一个子数组为空整数溢出对大数求和时使用int导致溢出过早优化尝试复杂方法而忽略了简单线性扫描避坑建议先写清楚数学条件再编码处理边界情况作为第一步使用小测试用例手动验证考虑极端输入全同数、大数、空数组等10. 扩展思考与挑战问题如果你已经掌握这个解法可以尝试以下更难的变种允许空子数组修改条件允许某些子数组为空找所有可能的分割而不仅仅是判断是否存在不等和分割三个子数组的和满足其他关系如等差数列多维数组分割扩展到矩阵或更高维数组带权重分割元素对和的贡献不同例如LeetCode第1013题是将数组分成和相等的三个部分与本题类似但条件略有不同。解决这些问题可以帮助你深入理解数组分割的技巧。