蓝桥杯国赛真题解析:整数数组最大乘积问题的贪心算法与边界处理
1. 问题引入从一道国赛真题说起最近在整理蓝桥杯国赛的历年真题发现“最大乘积”这道题出现的频率不低而且它非常典型。很多同学第一次看到题目描述可能会觉得这不就是个简单的排序加乘法吗但如果你真的这么想那大概率是要丢分的。这道题的精髓在于它考察的不仅仅是基础的编程能力更是对问题边界、数据特性和算法策略的综合考量。我见过不少同学在本地测试时感觉良好提交后却只能拿到部分分数问题往往就出在没有深入理解“最大乘积”在不同场景下的最优解。简单来说题目通常会给你一个包含正数、负数和零的整数数组要求你从中选出若干个数通常是固定个数比如3个或5个使得它们的乘积最大。这听起来简单但负数、零和正数的组合会让情况变得复杂。比如给你数组[-10, -5, 0, 1, 2, 3]要求选3个数最大乘积是多少是3 * 2 * 1 6吗不对应该是(-10) * (-5) * 3 150。看两个负数相乘得到正数再乘以一个最大的正数结果可能远超全部选正数。这就是这道题的魅力所在也是它成为国赛常客的原因。接下来我将以一个典型的国赛题目为背景手把手带你拆解“最大乘积”问题的Java解法。我们会从最直观的暴力枚举开始逐步优化到高效的贪心策略并深入探讨其中的数学原理和编码细节。无论你是正在备赛的选手还是想巩固算法思维的开发者相信这篇内容都能给你带来实实在在的收获。2. 问题建模与核心难点分析首先我们需要把问题抽象成一个清晰的数学模型。假设我们有一个长度为n的整数数组nums以及一个整数k1 k n代表我们需要选择的数字个数。目标是找到k个数的组合使得其乘积最大。这个问题的核心难点在于数字的符号正、负、零和数量关系。我们不能简单地认为“选最大的k个数”就能得到最大乘积。原因如下负数的存在偶数个负数相乘得到正数奇数个负数相乘得到负数。因此为了获得最大乘积正数我们可能需要刻意选择偶数个绝对值较大的负数。零的存在如果数组中存在零并且我们被迫必须选择零例如k很大那么乘积必然为零。但如果可以避免我们通常不希望选择零除非其他组合的乘积是负数而零至少是非负的但零通常不是最大乘积除非所有组合乘积都为负。正数的角色正数自然是越大越好它们是乘积增长的“引擎”。因此一个有效的算法必须同时考虑以下因素所选数字中负数的个数是奇数还是偶数绝对值大的负数是否值得被选入以“配对”产生正数在什么情况下选择零是唯一或最优的选择一个常见的错误思路是先排序然后总是从数组两端最大正数和绝对值最大的负数取数。这个思路方向是对的但具体策略需要精心设计否则很容易在边界条件上出错。例如对于数组[-100, -99, 1, 2, 3]k3。排序后是[-100, -99, 1, 2, 3]。如果简单地从两端取可能会取3, -100, -99乘积为29700这确实是最大值。但如果数组是[-100, -99, -1, 2, 3]k3。排序后[-100, -99, -1, 2, 3]。从两端取3, -100, -99乘积依然是29700。但如果我们取-100, -99, 3也是29700。然而如果我们考虑取-100, -99, 2乘积是19800略小。看起来没问题让我们看一个反例[-10, -9, 0, 1, 2]k3。排序后[-10, -9, 0, 1, 2]。从两端取2, -10, -9乘积为180。但正确答案是2 * 1 * 0 0吗不应该是(-10) * (-9) * 2 180。这里零不是最优解。但如果数组是[-10, -9, -8, 1, 2]k3呢排序后[-10, -9, -8, 1, 2]。从两端取2, -10, -9乘积为180。但如果我们取-10, -9, -8乘积是-720是负数。显然当可选的负数个数为奇数且没有足够的正数“对冲”时我们需要调整策略。通过以上分析我们可以将问题归纳为几个关键场景这也是我们设计算法的依据场景A数组全是非负数。解决方案很简单直接选取最大的k个数。场景B数组全是非正数负数或零。如果k是奇数则选取绝对值最小的k个数即最大的k个数因为都是非正数最大的就是最接近零的乘积的绝对值最小但负得最少如果k是偶数则选取绝对值最大的k个数即最小的k个数乘积为正且可能最大。场景C数组正负零混杂。这是最复杂的情况也是我们算法需要重点处理的。3. 暴力解法与可行性评估在深入优化算法之前我们先实现一个最直接的解法回溯法枚举所有可能的k个数的组合计算它们的乘积并记录最大值。这种方法思路简单绝对正确能帮助我们验证后续优化算法的正确性。public class MaxProductBruteForce { private long maxProduct Long.MIN_VALUE; public long maxProduct(int[] nums, int k) { backtrack(nums, k, 0, new ArrayList()); return maxProduct Long.MIN_VALUE ? 0 : maxProduct; // 处理空结果 } private void backtrack(int[] nums, int k, int start, ListInteger path) { if (path.size() k) { long product 1; for (int num : path) { product * num; } maxProduct Math.max(maxProduct, product); return; } for (int i start; i nums.length; i) { path.add(nums[i]); backtrack(nums, k, i 1, path); // 从i1开始避免重复选择 path.remove(path.size() - 1); } } public static void main(String[] args) { MaxProductBruteForce solver new MaxProductBruteForce(); int[] nums {-10, -5, 0, 1, 2, 3}; int k 3; System.out.println(暴力解法结果: solver.maxProduct(nums, k)); // 应输出 150 } }为什么先讲暴力解法确立基准任何优化算法都必须和暴力解法的结果一致在数据规模允许的范围内。这是验证算法正确性的“金标准”。理解复杂度回溯法的时间复杂度是O(C(n, k))即组合数。当n20k10时组合数C(20,10)已经超过18万当n更大时完全不可行。这让我们直观感受到优化的必要性。用于测试在开发贪心或动态规划算法时可以用小规模数据n15运行暴力解法与优化算法的结果对比快速定位逻辑错误。暴力解法的局限性指数级时间复杂度完全无法处理n较大的情况比如n1000。整数溢出乘积可能非常大int甚至long都可能溢出。在实际竞赛和工程中我们可能需要使用BigInteger或取模运算如果题目要求。这里为了清晰我们先使用long并假设乘积在long范围内。注意在蓝桥杯等竞赛中通常给出的数据规模会使得暴力解法超时这正是考察点。所以我们必须寻找更优的算法。4. 高效贪心算法设计与实现对于“最大乘积”问题经过数学分析一个高效的贪心策略是可行的。核心思想是排序后从数组的两端最大值和最小值进行选择每次决策都基于当前能获得最大乘积的潜在可能。4.1 算法步骤详解排序将数组nums按升序排序。排序后最小的数可能是绝对值很大的负数在左边最大的数在右边。初始化定义两个指针left和right分别指向排序后数组的最左端和最右端。定义结果product 1。决策循环我们需要选择k个数。如果k是奇数我们至少需要先选择一个数来初始化乘积。为了最大化最终结果我们应该先选择最大的那个数即nums[right]因为它是当前最大的正数或非负数可能性最大。然后right--k--。这样剩下的k就变成了偶数便于我们后续成对选择。接下来我们总是成对地从数组两端选择数字。比较nums[left] * nums[left 1]和nums[right] * nums[right - 1]的大小。为什么比较乘积因为我们的目标是总乘积最大。当k为偶数时我们每次选两个数。如果两个负数的乘积nums[left] * nums[left1]因为left端是较小的数负数的话其绝对值可能很大大于两个正数的乘积nums[right] * nums[right-1]那么选择这两个负数更优即使它们是负数但它们的乘积是正数并且可能更大。选择乘积较大的一对将它们的乘积乘到总结果product上。根据选择移动指针如果选了左边一对则left 2如果选了右边一对则right - 2。k - 2。重复此过程直到k变为0。处理零的边界情况上述策略在大多数情况下有效但有一个潜在问题如果数组中存在零并且我们成对选择时乘积比较可能无法反映出选择零的影响。实际上如果数组中最大的k个数乘积为负而我们可以选择零那么零可能比那个负数乘积更大因为0 负数。但是在我们的贪心策略中如果先选择了最大的数当k为奇数时并且这个数是0那么后续选择将基于0最终乘积为0。这可能是最优解也可能不是如果存在正数乘积。然而一个更严谨的做法是在排序后最大乘积只可能来自最右边的k个数全选最大的或者最左边的两个数和最右边的k-2个数的组合或者最左边的四个数和最右边的k-4个数的组合……因为只有成对地选择负数才能利用负负得正。因此我们可以枚举所有选择偶数个负数的情况。4.2 更健壮的贪心实现基于以上分析一个更通用、更健壮的贪心实现如下import java.util.Arrays; public class MaxProductGreedy { public long maxProduct(int[] nums, int k) { int n nums.length; Arrays.sort(nums); // 升序排序 long product 1; int left 0, right n - 1; // 处理 k 为奇数的情况先选一个最大的数 if (k % 2 1) { product * nums[right]; right--; k--; // 如果刚刚选的那个数是负数且 k 还不为0那么后续我们必须尽可能让乘积变正。 // 但我们的算法通过成对比较已经隐含处理了这一点。 } // 现在 k 是偶数我们成对选择 while (k 0) { // 计算左边一对和右边一对的乘积 // 注意使用long防止溢出 long leftProduct (long) nums[left] * nums[left 1]; long rightProduct (long) nums[right] * nums[right - 1]; // 选择乘积较大的一对 if (leftProduct rightProduct) { product * leftProduct; left 2; } else { product * rightProduct; right - 2; } k - 2; } return product; } public static void main(String[] args) { MaxProductGreedy solver new MaxProductGreedy(); int[][] testCases { {-10, -5, 0, 1, 2, 3}, // k3 - 150 {-100, -99, 1, 2, 3}, // k3 - 29700 {-10, -9, -8, 1, 2}, // k3 - 180 {-1, -2, -3, -4, -5}, // k4 - 120 (-1*-2*-3*-4? 不对是(-5*-4*-3*-2)120) {0, 1, 2, 3, 4}, // k3 - 24 {-10, -9, 0, 1, 2}, // k3 - 180 }; int[] ks {3, 3, 3, 4, 3, 3}; for (int i 0; i testCases.length; i) { long result solver.maxProduct(testCases[i], ks[i]); System.out.println(数组: Arrays.toString(testCases[i]) , k ks[i] - 乘积: result); } } }4.3 算法正确性分析与边界处理让我们深入分析一下这个算法为什么有效以及它如何处理各种边界情况。数学原理 排序后数组被分成负数部分、零如果有和正数部分。最大乘积的k个数必然是从数组的两端选取的。因为任何正数选更大的肯定比选更小的好。对于负数选两个绝对值更大的即更小的数因为排序后越左越小它们的乘积正数可能比两个较小的正数的乘积还大。 因此最优解一定是由最左端绝对值大的负数和最右端大的正数的一些数组合而成。我们的贪心策略每次比较两端“两个数一组”的乘积正是模拟了这个过程。边界情况处理全为负数k为奇数例如nums [-5, -4, -3, -2, -1],k3。排序后就是它自己。算法步骤k3为奇数先选最大的数-1product -1right指向-2k2。现在比较leftProduct (-5)*(-4)20和rightProduct (-2)*(-3)6。选择20product -1 * 20 -20。最终结果是-20。让我们验证所有3个数的组合中乘积最大的是(-1)*(-2)*(-3) -6吗不对(-5)*(-4)*(-1) -20(-5)*(-4)*(-2) -40... 看起来-20是最大的负数中绝对值最小的。等等(-1)*(-2)*(-3) -6-6 -20。我们的算法出错了问题根源当k为奇数且我们先选了一个负数时这个初始的负数乘积为负。为了最终结果最大即负得最少我们后续应该让乘积的绝对值尽可能小而不是尽可能大。但我们的算法在成对选择时仍然选择乘积大的一对这会让负数的绝对值变得更大最终乘积负得更厉害。修正当k为奇数且我们初始选择的数nums[right]是负数时说明整个数组可能都是非正数。在这种情况下为了最大化乘积即让负数乘积的绝对值最小我们应该改变策略后续成对选择时应该选择乘积小的一对即选择绝对值较小的两个数。更简单的做法是当k为奇数且nums[right] 0时我们直接从右向左选k个数即最大的k个数因为对于全负数数组k为奇数时选最大的k个数最接近0的数乘积最大负得最少。包含零的情况我们的算法能正确处理零吗考虑nums [-10, 0, 1, 2],k3。排序后[-10, 0, 1, 2]。k3为奇数先选最大的数2product2,right指向1k2。比较leftProduct (-10)*00和rightProduct 1*0?不对right现在指向1right-1指向0。所以rightProduct 1*00。两者相等算法可能选左边一对或右边一对。如果选左边product2*00如果选右边product2*00。结果是0。但实际最大乘积是2*1*00或2*1*(-10)? -20所以0确实是最大值。但如果数组是[-10, -1, 0, 2],k3最大乘积是(-10)*(-1)*220我们的算法能得出20吗排序后[-10, -1, 0, 2]。k3为奇数先选2product2,right指向0k2。比较leftProduct (-10)*(-1)10和rightProduct 0*(-1)?注意right指向0right-1指向-1。rightProduct 0 * (-1) 0。leftProduct10rightProduct0所以选左边一对product2*1020。正确。由此可见我们的基础贪心算法在大多数情况下有效但在“全负数且k为奇数”的情况下会失败。我们需要完善它。5. 完善的处理方案与代码实现综合以上分析一个鲁棒的解决方案需要分情况讨论。实际上最大乘积的候选答案只有有限的几种情况我们可以枚举这些情况并取最大值。这是竞赛中常见的思路既保证了正确性复杂度也完全可以接受O(n log n) 主要用于排序。完整算法思路将数组升序排序。最大乘积只可能来自以下两种模式之一模式A选右边的k个数即选最大的k个数。这对于全正数、或全负数且k为偶数的情况是最优的。模式B选左边的一些负数和右边的一些正数即选2*i个最左边的数负数和k - 2*i个最右边的数正数其中i从1到k/2因为需要偶数个负数来保证乘积为正。我们枚举所有可能的i。对于每种模式计算乘积取最大值。特别地如果数组中有0并且所有上述模式的乘积都是负数那么0可能比这些负数都大。但如果我们已经枚举了所有可能组合0如果可以被选入即0在选择的k个数中那么它的乘积就是0会在模式A或B中被考虑到如果0是最大的k个数之一。如果0不在这些组合中但其他组合乘积都是负那么选0的组合如果存在乘积为0可能更大。但更简单的方法是在计算完所有模式的最大乘积后如果最大值是负数并且数组中有0那么最终答案应该是0。因为我们可以选择0和其他任意k-1个数乘积为0大于任何负数。为什么枚举模式B时只从左边取偶数个因为从左边取的是负数奇数个负数相乘结果为负会拉低整体乘积。为了最大化乘积我们希望乘积为正数所以从左边取的负数个数必须是偶数。当然如果所有组合的乘积都是负数例如全负数且k为奇数那么我们也只能接受负数结果此时模式A选最大的k个数就是最优。最终实现代码import java.util.Arrays; public class MaxProductRobust { public long maxProduct(int[] nums, int k) { int n nums.length; Arrays.sort(nums); long maxProd Long.MIN_VALUE; // 模式A选最大的k个数最右边的k个 long productA 1; for (int i n - k; i n; i) { productA * nums[i]; } maxProd Math.max(maxProd, productA); // 模式B枚举从左边取 2*i 个负数从右边取 k - 2*i 个正数 // i 的取值范围至少取2个负数i1最多取不超过k且不超过左边负数总数的偶数个 // 左边负数总数 第一个非负数的索引 int firstNonNeg 0; while (firstNonNeg n nums[firstNonNeg] 0) { firstNonNeg; } int maxNegPairs Math.min(k / 2, firstNonNeg / 2); // 最多能取几对负数 for (int i 1; i maxNegPairs; i) { int negCount 2 * i; // 取的负数个数 int posCount k - negCount; // 取的正数个数 if (posCount 0 || posCount (n - firstNonNeg)) { continue; // 正数不够取跳过 } long product 1; // 取左边最小的 negCount 个数即绝对值最大的负数 for (int j 0; j negCount; j) { product * nums[j]; } // 取右边最大的 posCount 个数 for (int j n - posCount; j n; j) { product * nums[j]; } maxProd Math.max(maxProd, product); } // 处理0的情况如果最大值是负数且数组中存在0那么答案至少是0 // 但我们的枚举中如果0被包含在“最大的k个数”或组合中乘积可能为0。 // 如果最大值是负数说明所有枚举的组合乘积都是负的。此时如果数组中有0我们可以主动构造一个含0的组合乘积为0。 // 更简单的判断如果最大值小于0并且k n肯定成立并且数组中有0则返回0。 if (maxProd 0) { for (int num : nums) { if (num 0) { return 0; } } } return maxProd; } public static void main(String[] args) { MaxProductRobust solver new MaxProductRobust(); int[][] testCases { {-10, -5, 0, 1, 2, 3}, // 150 {-100, -99, 1, 2, 3}, // 29700 {-10, -9, -8, 1, 2}, // 180 {-1, -2, -3, -4, -5}, // k4 - 120 {-1, -2, -3, -4, -5}, // k3 - -6 (全负k奇) {0, 1, 2, 3, 4}, // k3 - 24 {-10, -9, 0, 1, 2}, // k3 - 180 {-10, -1, 0, 2}, // k3 - 20 {-10, -5, 1, 2}, // k2 - 20 (选-10和-5) {1, 2, 3, 4, 5}, // k3 - 60 }; int[] ks {3, 3, 3, 4, 3, 3, 3, 3, 2, 3}; for (int i 0; i testCases.length; i) { long result solver.maxProduct(testCases[i], ks[i]); System.out.println(数组: Arrays.toString(testCases[i]) , k ks[i] - 乘积: result); } } }这个实现考虑了所有情况模式A覆盖了全正数、全负数且k为偶数、以及包含零时零在最大k个数中的情况。模式B覆盖了需要负数配对来获得更大正数乘积的情况。最后的零检查确保了当所有可能组合乘积都为负时如果能选零则返回零。时间复杂度是 O(n log n) 排序 O(k) 计算模式A O(k^2) 枚举模式B但i最多k/2每次计算乘积O(k)。在k不太大时比如k10这是完全可以接受的。空间复杂度是 O(1) 或 O(log n)排序所用栈空间。6. 蓝桥杯真题实战与代码优化在蓝桥杯比赛中我们不仅要求算法正确还要求代码简洁、高效能够快速写出。上述完整方案逻辑严密但代码稍长。我们可以根据题目数据范围的特点进行简化。通常蓝桥杯此类题目的k不会太大比如k5因此我们可以采用一种更直观的“分类讨论”贪心代码更短。简化版贪心策略适用于k较小如k3, 4, 5排序数组。最大乘积只可能来自两种选择选最大的k个数。选最小的2个数两个绝对值最大的负数和最大的k-2个数。 为什么只考虑2个负数因为k很小比如k3或4取更多负数4个需要k4且取4个负数意味着从右边取的正数很少通常不会比取2个负数加更多正数更优。但为了绝对正确对于k5我们可能需要考虑取2个或4个负数。不过根据经验对于这类题通常考虑这两种情况就足够了。如果题目k更大则需要像上一节那样枚举。比较这两种选择的乘积取最大值。同样处理零的边界。以k3为例的简化代码import java.util.Arrays; public class MaxProductSimple { public long maxProductForK3(int[] nums) { int n nums.length; Arrays.sort(nums); // 候选方案1最大的三个数 long cand1 (long) nums[n-1] * nums[n-2] * nums[n-3]; // 候选方案2最小的两个数负数和最大的一个数 long cand2 (long) nums[0] * nums[1] * nums[n-1]; long ans Math.max(cand1, cand2); // 如果ans是负数且数组中有0则答案为0 if (ans 0) { for (int num : nums) { if (num 0) { return 0; } } } return ans; } }对于k4候选方案可能包括方案1最大的四个数。方案2最小的两个数和最大的两个数。方案3最小的四个数如果都是负数负负得正。我们可以编写一个通用的方法对于给定的k枚举从左边取0, 2, 4, ...个负数的情况直到k或没有负数为止。通用简化版枚举负数对数public long maxProductGeneral(int[] nums, int k) { int n nums.length; Arrays.sort(nums); long ans Long.MIN_VALUE; // 枚举从左边取 negCount 个负数negCount必须是偶数 for (int negCount 0; negCount k negCount n; negCount 2) { int posCount k - negCount; if (posCount 0 || posCount n) continue; // 取左边最小的 negCount 个数负数 // 取右边最大的 posCount 个数 long product 1; for (int i 0; i negCount; i) { product * nums[i]; } for (int i n - posCount; i n; i) { product * nums[i]; } ans Math.max(ans, product); } // 处理全为负数且k为奇数的情况上面的循环negCount是偶数不会取奇数个负数。 // 但这种情况下答案就是最大的k个数最右边的k个 if (ans 0) { // 检查是否可能通过包含0得到0 for (int num : nums) { if (num 0) { return 0; } } // 否则计算最大的k个数 long productRight 1; for (int i n - k; i n; i) { productRight * nums[i]; } ans Math.max(ans, productRight); // 实际上此时ans就是productRight因为ans0且无0 } return ans; }这个通用版本逻辑清晰且枚举的次数最多 k/2 1 次在k较小10时非常高效代码也比最初的完整枚举更简洁。7. 常见陷阱与调试技巧即使理解了算法在实现时也容易掉进一些陷阱。下面我总结几个常见的坑点和调试方法。陷阱1整数溢出这是最容易忽略的问题。三个10^9的数相乘就接近10^27远超int甚至long的范围long最大值约9.22e18。在蓝桥杯比赛中有时会要求对结果取模有时则保证结果在long范围内。但无论如何在计算过程中使用long是更安全的。操作技巧在乘法前进行类型转换。例如long product (long) nums[i] * nums[j];而不是long product nums[i] * nums[j];因为后者会先以int相乘可能溢出然后再赋值给long。陷阱2排序的副作用我们默认排序是升序。但注意排序改变了原始数组的顺序。如果题目不允许修改原数组你需要先拷贝一份。陷阱3k大于数组长度或k为0题目通常保证1 k n但为了代码健壮性可以添加检查。陷阱4乘积初始值在累乘时初始值设为1。但如果k0虽然题目可能不允许乘积应该是多少通常是1空乘积定义为1。但根据具体题目定义。调试技巧小数据暴力对拍编写一个暴力解法如回溯用随机生成的小规模数据n10测试你的优化算法确保结果一致。打印中间变量在贪心选择过程中打印出每次比较的leftProduct、rightProduct以及做出的选择观察逻辑是否符合预期。构造极端用例全正数数组。全负数数组k为奇数和偶数。包含零的数组。正数、负数、零混合。绝对值很大的数。使用IDE的调试器单步执行观察变量值的变化这是最有效的调试手段。例如测试全负数k为奇数的情况int[] nums {-5, -4, -3, -2, -1}; int k 3; // 正确结果应该是 -6 选 -1, -2, -3 // 如果你的算法得出 -20说明没有处理全负数k为奇数的特殊情况。8. 举一反三相关问题与扩展掌握了“最大乘积”问题你可以尝试解决一些变种问题巩固思维。变种1最大子数组乘积LeetCode 152这是另一个经典问题给定一个整数数组nums找出数组中乘积最大的连续子数组并返回该乘积。这与我们讨论的“选择k个数”不同这里是找连续子数组。解决思路是动态规划同时记录以当前元素结尾的最大乘积和最小乘积因为负数乘以最小可能变最大。变种2乘积小于K的子数组个数LeetCode 713给定一个正整数数组nums和整数k返回子数组内所有元素的乘积严格小于k的连续子数组的个数。这里用到的是滑动窗口技巧。变种3除自身以外数组的乘积LeetCode 238给你一个整数数组nums返回数组answer其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。要求不能使用除法且在 O(n) 时间复杂度内完成。思路是分别计算前缀乘积和后缀乘积。扩展思考 如果题目中的数字不是整数而是实数包括小数我们的贪心策略还适用吗基本适用但需要注意正小数小于1相乘会变小所以“选最大的k个数”策略可能需要调整。对于实数通常也需要排序然后最大乘积可能出现在“最大的几个数”或“最小的两个负数如果存在加上最大的若干个数”的组合中但情况更复杂因为绝对值小于1的正数会减小乘积。在这种情况下可能需要考虑更多的组合或者使用动态规划。回到我们的整数问题我个人的体会是这类“最大乘积”问题在蓝桥杯中出现时数据规模往往允许我们进行排序和有限的枚举。关键是要透彻理解正数、负数和零对乘积的影响不要想当然地认为排序后取两端就万事大吉。多用手动构造的极端例子去测试你的算法特别是全负数和包含零的情况这两者是主要的失分点。在竞赛中如果你时间紧张实现那个“通用简化版”的枚举方法枚举取负数的偶数个数通常是稳妥且编码快速的策略。