news 2026/11/1 21:06:23

最大乘积子数组问题:从暴力枚举到动态规划的完整实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
最大乘积子数组问题:从暴力枚举到动态规划的完整实战解析

刷OJ的时候碰到东华大学OJ的第39题“最大乘积”,这道题卡了我一会儿,做出来之后回头想了想,觉得里面有很多值得展开聊聊的东西。不光是这道题的解法本身,还包括怎么在OJ上用正确的方式读题、分析数据范围、选算法,以及提交之后怎么根据报错调代码。这篇文章就用这道题作为例子,把从读题到AC的完整链路捋一遍。

先说结论:这道题表面上只是一个“求最大连乘子段”的经典问题,但它在数据范围、边界条件、输出格式上埋了好几个雷。你要是能把这道题彻底吃透,OJ系统常见的那几类坑(多组输入、特殊值输出、溢出、全负数场景)基本就都有体感了,对后面刷动态规划、贪心这些专题会有很大帮助。

1. 先把题目彻底读懂,再说怎么写代码

1.1 输入输出到底长什么样

东华OJ的很多题目都是多组测试数据。这道题也不例外。输入一般长这样:

3 1 2 3 2 -1 -1 3 -1 0 -2

第一行是n,代表序列长度,第二行给n个整数。然后可能会继续给下一组,一直到文件结束。很多新手第一次接触多组输入会直接懵:我只处理一组行不行?不行,OJ评测会把所有测试点跑一遍,你的程序必须能读完全部输入。

输出也比想象中要严格。每一组结果单独占一行,大部分OJ还要求“每组输出之后跟一个空行”或者“Case #x: ”之类的格式。东华这题如果题干没额外要求,直接输出结果换行即可,但你要是习惯了LeetCode那种只需要返回结果值的模式,第一次做题往往会踩到输出格式不对导致的Presentation Error(PE)。

1.2 题目里最容易看漏的隐藏条件

我特意去翻了这道题的原题描述,核心要求是:给定一个整数序列,找到乘积最大的连续子序列,输出这个最大乘积。如果最大乘积不是正数,输出 -1。

这里有几个关键信息:

  • “连续子序列”意味着你不能排序,不能重新排列,只能在原始顺序里切一段。
  • 整数序列里有负数,也有可能有0。
  • 最大乘积如果不是正数,就输出 -1。

第三条是很多人会忽略的重点。比如序列是[-1, -2],连续子序列乘积最大其实是(-1)*(-2)=2,它是正数,所以输出2,这个没问题。但如果序列是[0, -2, -3]呢?0是最大的吗?不是,(-2)*(-3)=6。那如果序列是[-1, 0, -2]呢?所有连续子序列乘积分别是 -1、0、-2、0、0、2,最大的是2。但如果序列是[-1, -2, 0]呢?最大子段是(-1)*(-2)=2,不是0。真正会走到输出 -1 的场景,是类似于[0, -1, -2]这样?等等,0包含在[0,-1,-2]整体里是0,但(-1)*(-2)又是2。所以大家看,负数只要凑成偶数个数,乘积就会变正。真正的“最大乘积不是正数”的情况,是序列中任意连续段乘出来都不可能为正,比如[-1, -2, -3]?这里(-2)*(-3)=6,又是正的。

大家发现规律了吗?只要序列中存在两个以上的负数,并且它们之间不隔着0,那么乘积就可以是正数。所以真正会输出-1的情况其实很少,基本是序列全为0,或者只有一个负数且其余为0,或者全是0。但题目要的就是这个边界判断,不做就会在某个测试点上WA(Wrong Answer)到怀疑人生。

1.3 数据范围决定算法方向

东华OJ这道经典题很老,n的数据范围一般是n <= 18,每个数字取值范围是-10到10。为什么要强调数据范围?因为:

  • n只有18,总子段数量是n*(n+1)/2 = 171个,暴力枚举完全可行。
  • 每个数字绝对值最大是10,18个10相乘就是10^18,这个值刚好卡在long long的范围附近(9.22*10^18),所以可以用64位整数存结果。
  • 正因为n小,所以题目其实并没有强求你用动态规划,你暴力枚举也能过。

但作为一篇正经的经验分享,我建议你暴力解法一定要会,同时也要把动态规划的思路搞明白。因为n一旦放大到10^5,暴力就原地爆炸,那时候就必须上DP了。刷题最忌讳的就是只背一种解法,换个数据范围就废了。

2. 暴力枚举法——先把问题想透,再谈优化

2.1 两层循环枚举所有连续子段

最直观的思路是什么?把每个可能的连续子段都乘一遍,记录最大值。连续子段由起点i和终点j决定,只要枚举i从0到n-1,j从i到n-1,然后把这之间的所有数乘起来就行。

很多初学者会在这里犯一个经典错误:用三层循环,先枚举起点,再枚举终点,再用一层循环计算起点到终点的乘积。三层循环算下来是O(n^3),虽然n=18时也能跑,但代码很冗余,而且完全没有必要。

其实可以在枚举终点时逐步累乘。也就是说,固定起点i,然后让j从i开始往右走,同时用一个变量mul累乘a[j],每走一步就更新一次最大值。这样只需要两层循环,复杂度是O(n^2)。

2.2 关键代码与原理解读

#include <bits/stdc++.h> using namespace std; int main() { int n; while (cin >> n) { vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; long long maxProd = 0; // 注意初值,题目要求非正数输出-1 bool hasPositive = false; for (int i = 0; i < n; i++) { long long cur = 1; for (int j = i; j < n; j++) { cur *= a[j]; if (cur > maxProd) { maxProd = cur; } } } if (maxProd <= 0) cout << -1 << endl; else cout << maxProd << endl; } return 0; }

这个代码有个很巧的地方:cur在固定起点的情况下,每乘一个数就更新一次,天然就覆盖了所有“同一个起点、不同终点”的子段。两层循环搞定,不需要额外的“计算乘积”循环。

但这里我要特别提醒一个初值问题:maxProd我设成了0,而不是无穷小。为什么敢设0?因为题目说了“如果最大乘积不是正数,输出-1”。也就是说,如果你最后算出来的maxProd是0或者负数,就直接输出-1。设成0就相当于把“正数”作为初始基准,跑完循环只要maxProd还是0,说明压根没找到正数乘积,直接输出-1就行。而如果你是设成INT_MIN,后面还得再多写一个判断,反而容易漏。

2.3 暴力法的局限

n=18时暴力法当然没问题,但你要把这个思路平移到LeetCode 152题(最大乘积子数组),数据范围是10^5,O(n^2)就是妥妥的超时。所以这里就引出一个问题:能不能用更聪明的方法,一遍遍历就算出最大值?能,这就是动态规划。

3. 动态规划的优化思路——为什么维护最小值很关键

3.1 状态设计:同时维护最大最小

动态规划处理连续子段问题时,最经典的套路是用“以i结尾的子数组”作为状态。假设dpMax[i]表示以第i个数字结尾的所有连续子序列中,乘积最大的那个;dpMin[i]表示乘积最小的那个。

为什么要同时维护最小值?因为在乘法里,一个负数乘以一个负数会变成正数,而且这个正数可能比当前最大值还大。如果你只记录最大值,碰到负数时就会漏掉“负数乘以之前的最小负数”这种情况。

用生活化的类比解释一下:你手里有一笔“最大财富”和一笔“最大负债”。有一天你获得了一个翻转机会(乘以一个负数),最大负债被翻转成了最大财富,而原来的最大财富反而变成了负债。所以你两个都得记着,机会来了才知道用哪个去翻。

3.2 状态转移方程

dpMax[i] 的来源有三种可能:

  1. 从nums[i]重新开始一段子序列,也就是不接前面的。
  2. 让nums[i]乘以dpMax[i-1]。
  3. 让nums[i]乘以dpMin[i-1]。

为什么会有第3种?因为如果nums[i]是负数,而dpMin[i-1]是负数,负负得正。同理,dpMin[i] 也要从这三种里取最小。

写成公式就是:

dpMax[i] = max(nums[i], dpMax[i-1]*nums[i], dpMin[i-1]*nums[i]) dpMin[i] = min(nums[i], dpMax[i-1]*nums[i], dpMin[i-1]*nums[i])

最终答案就是所有dpMax[i]中的最大值。

3.3 滚动数组节省空间

dpMax[i]只用到了dpMax[i-1],所以完全不需要开数组,用两个变量滚动更新就行。每读入一个数,就更新当前的curMax和curMin,然后拿curMax去更新全局最大。

有两点必须注意:

  • 更新顺序很关键。如果你先更新了 curMax,再用更新后的 curMax 去算 curMin,那就错了。因为curMin需要的是“上一轮”的 curMax,而不是更新后的。所以要么用临时变量先存旧值,要么把两个更新放到同一个公式里交错进行。
  • 如果题目改成n范围很大,但数字也有0,那么遇到0时乘积全部归零,dpMax和dpMin都会变成0,这也是符合常识的,因为以0结尾的子段最大乘积就是0。

4. 完整代码与心法——从题目到手撕AC

4.1 动态规划最终代码

#include <bits/stdc++.h> using namespace std; int main() { int n; while (cin >> n) { vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; long long curMax = a[0], curMin = a[0]; long long ans = a[0]; for (int i = 1; i < n; i++) { long long tmpMax = curMax; long long tmpMin = curMin; curMax = max((long long)a[i], max(tmpMax * a[i], tmpMin * a[i])); curMin = min((long long)a[i], min(tmpMax * a[i], tmpMin * a[i])); ans = max(ans, curMax); } if (ans <= 0) cout << -1 << endl; else cout << ans << endl; } return 0; }

这段代码比暴力法多了一点点思考门槛,但扩展性很强。你把n改成一百万也照样跑得动,时间复杂度只有O(n)。

4.2 为什么用long long而不是int

前面说过,n=18,每个数最大能到10,那么18个10相乘是10^18。这个数已经超过int的范围了(int最大约2.1*10^9),所以一旦遇到全10的测试数据,用int存储乘积会直接溢出,WA得莫名其妙。这种题用long long是基本操作,但就是有很多人会忽视。

其实我在实际比赛里也踩过这个坑。记得有次做一道“最大子序列乘积变种题”,我自信满满用int写完,提交之后WA了三次,最后才意识到10个100相乘就爆int了,换成long long立刻AC。从那之后,凡是涉及乘法的题,我都会下意识地用64位整数。

4.3 关于多组输入的处理

东华OJ很多题目都是“输入包含多组测试数据,每组占两行”。代码里用的while (cin >> n)就是典型写法。它的原理是:cin >> n在成功读取到数据时返回非0值,读到EOF(文件结束符)时返回0,循环停止。

新手容易出问题的地方在于:处理完一组之后忘了重置变量。比如暴力法中那个maxProd,如果把它定义在循环外面,第二组测试数据就会带着上一组的残留值,结果全乱套。我自己一般会习惯性把所有变量都定义在while循环内部,这样每组数据都是全新的状态。

另外,如果输出要求每组之间有空行,你得注意最后一个输出后面不能有多余空行,否则会PE。东华这题我印象中没这个要求,但如果有,可以在每组后面直接输出一个空行,题目的special judge一般会忽略最后多余的空格和空行,但保险起见还是按严格格式来。

5. 常见问题与排查技巧实录

5.1 易错点速查表

易错场景原因排查方法
WA但样例能过漏了多组数据中的状态重置检查变量是否定义在循环外
WA且答案很大int溢出换成long long
PE格式错误多了或少了换行/空格看清输出格式要求
RE运行时错误数组越界检查 n=1 时的特殊情况
WA在某些负数和0的数据上没有处理“非正数输出-1”用全负数测试用例自测

5.2 自测用例与陷阱验证

我最推荐的做法是,写完代码后自己构造几组边界数据来测,而不是只依赖样例。下面这组用例非常经典:

4 0 0 0 0

所有连续子序列乘积都是0,最大乘积不是正数,输出-1。很多初学代码能算出0,但忘了转成-1。

再来一组:

2 -1 -1

最大子段是(-1)*(-1)=1,输出1,不能因为看到负数就输出-1。

再来一组:

1 -5

唯一的连续子序列就是它自己,乘积是-5,非正数,输出-1。

5.3 为什么我的动态规划输出不对

如果你用上面的动态规划代码去测[-1, -1],手动推一遍:

  • i=0:curMax=-1,curMin=-1,ans=-1
  • i=1:tmpMax=-1,tmpMin=-1
    • curMax = max(-1, 1, 1) = 1
    • curMin = min(-1, 1, 1) = -1?

等等,这里tmpMin * a[1]是(-1)*(-1)=1,不是1吗?min(-1, 1, 1)得到-1。然后ans更新为1。输出1,符合预期。

这个手动推导过程特别值得自己走一遍。很多人在纸上推一遍,自己对状态转移的理解就会上一个台阶。

5.4 常见的OJ报错判读

第一次提交时如果返回了 Compile Error(CE),别急着摔键盘。先看错误信息,东华OJ会返回编译器的提示。大部分CE都是因为头文件写错、变量名冲突、或者忘了C++版本。

Running Time Error(RE)的话,优先怀疑数组越界。n很小的时候也有可能出现,比如你定义数组为a[n]但访问了a[n]这个不存在的下标。C++数组下标从0开始,最后一个是a[n-1]。

Time Limit Exceeded(TLE)基本不用太担心,n=18暴力都能过,何况O(n)的DP。真超时就检查一下是不是死循环,比如某个while循环忘了更新变量,或者EOF判断写错了。

6. 这道题还能怎么延伸

6.1 和力扣152的对比

LeetCode第152题“乘积最大子数组”和东华这题是孪生题,区别在于LeetCode的返回值直接是最大乘积,不用特判“非正数输出-1”,而且数据范围更大。如果你把这篇的动态规划代码稍作修改,去掉最后的if判断,就能直接提交到LeetCode。

这种“改改边界就能通吃多平台”的题型,非常适合用来练习一题多解和一解多题的能力。我刷题时特别喜欢对同类型题目做横向对比。

6.2 如果题目改成求最大非空子序列乘积

还有一个非常隐蔽的变种:子序列不一定连续,但同样要保证相对顺序。此时解法会变得很不一样,可能要统计负数的个数、0的位置,然后分类讨论。这就从“动态规划”变成了“贪心+分类讨论”。有兴趣的话可以想想,如果序列里有偶数个负数,整个序列乘起来就是正的;如果有奇数个负数,就得舍弃一个负数,具体舍弃哪一个需要比较两端的值。这也是LeetCode上另一道题的思路,留给大家自己去思考。

6.3 在竞赛中的实战定位

在竞赛里,这种题一般是送分题位置。但它的意义不在于题目本身多难,而在于帮你建立几个肌肉记忆:

  • 看到“乘积”立刻想溢出问题;
  • 看到“负数”立刻想负负得正;
  • 看到“连续子段”立刻想枚举或DP;
  • 看到“多组输入”立刻想EOF处理。

这些肌肉记忆一旦建立,后面刷其他DP题、贪心题、模拟题时都会吃香很多。我个人觉得,做题刷的不是数量,而是这种题感。所谓题感,就是看到题目描述第一眼,就能预判出这道题的坑点和算法方向。这道“最大乘积”就是训练题感的好素材。

说到底,OJ系统就像一个严格的教练,它不会告诉你哪里错了,只会给你一个冷冰冰的WA。你能做的,就是自己不断构造测试用例、反复审视代码逻辑、琢磨每一个边界条件。等你亲手把这道“最大乘积”从读题到AC完整走一遍,你就真正入门了OJ世界的游戏规则。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 21:06:14

计算机网络技术基础实训报告:从VLAN配置到抓包分析的完整操作手册

简介&#xff1a;这份《计算机网络技术基础实训报告》文档面向计算机相关专业学生及网络入门学习者&#xff0c;用于完成课程实训、撰写实验报告或复习网络基础操作。内容围绕硬件组装与系统安装、水晶头制作与共享式局域网组建、无线局域网配置等实训模块展开&#xff0c;配有…

作者头像 李华
网站建设 2026/11/1 8:43:36

text-to-cad 实战:从自然语言到参数化 CAD 模型的完整工程链路

1. 从一句话到三维模型&#xff1a;text-to-cad 到底在解决什么问题把一句自然语言描述直接变成可用的 CAD 模型&#xff0c;这件事在几年前还只存在于论文里。现在你打开任何一个 AI 编程工具&#xff0c;输入“生成一个长宽高 100x60x40mm、壁厚 3mm、四角带 M4 沉头孔安装位…

作者头像 李华
网站建设 2026/10/17 20:54:35

Agent-Reach:从工具调用到记忆管理的智能体能力扩展实践

做AI应用这一年多&#xff0c;我越来越强烈地感觉到一个矛盾&#xff1a;大模型的智商越来越高&#xff0c;但每个Agent真正能“摸到”的东西却少得可怜。你给它一个任务&#xff0c;它的大脑确实足够聪明&#xff0c;可手边只有一把螺丝刀&#xff0c;再聪明也只能反复拧螺丝。…

作者头像 李华
网站建设 2026/10/19 2:41:47

西工大软件学院计网实验报告:Wireshark抓包与Socket编程完整指南

简介&#xff1a;这份资源是西北工业大学软件学院计算机网络课程的实验报告&#xff0c;面向正在学习计算机网络、需要完成HTTP协议相关实验的高校学生。报告围绕Wireshark抓包分析展开&#xff0c;涵盖HTTP基础GET/响应交互、条件GET与缓存机制、长文档检索以及含嵌入对象的HT…

作者头像 李华
网站建设 2026/10/26 18:49:14

动态遮阴下光伏MPPT的粒子群算法优化与MATLAB仿真实践

做光伏MPPT仿真这些年&#xff0c;踩过最多的坑就是遮阴。组件被云、树叶、隔壁楼影子挡一块&#xff0c;P-V曲线瞬间从单峰变成多峰&#xff0c;传统的扰动观察法直接卡在局部极值点上不动&#xff0c;发电量白白损失一大截。后来我把粒子群算法&#xff08;PSO&#xff09;移…

作者头像 李华
网站建设 2026/10/17 20:54:45

计算机网络第五版PDF高效学习指南:拆书+TCP协议四件套

简介&#xff1a;谢希仁编著的《计算机网络&#xff08;第五版&#xff09;》配套课件PDF&#xff0c;是计算机网络课程经典且权威的学习资料。资源为单个PDF文件&#xff0c;整体17.15MB&#xff0c;将原版PowerPoint课件整合为便于阅读和打印的文档格式。内容从第1章概述起步…

作者头像 李华