1 题目
给你一个整数数组 nums 和一个整数 m。
返回任意大小为 m 的 子序列 中首尾元素乘积的最大值。
子序列是可以通过删除原数组中的一些元素(或不删除任何元素),且不改变剩余元素顺序而得到的数组。
示例 1:
输入: nums = [-1,-9,2,3,-2,-3,1], m = 1
输出: 81
解释:
子序列 [-9] 的首尾元素乘积最大:-9 * -9 = 81。因此,答案是 81。
示例 2:
输入: nums = [1,3,-5,5,6,-4], m = 3
输出: 20
解释:
子序列 [-5, 6, -4] 的首尾元素乘积最大。
示例 3:
输入: nums = [2,-1,2,-6,5,2,-5,7], m = 2
输出: 35
解释:
子序列 [5, 7] 的首尾元素乘积最大。
提示:
1 <= nums.length <= 105-105 <= nums[i] <= 1051 <= m <= nums.length
2 代码实现
思考
根据以前的经验来看,这种题目无非就是在迭代更新,同时维护一个最大值,但是在这里需要注意的是会出现负数。
回忆一下我们买卖股票的代码。
cpp
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0){
return 0 ;
}
int maxPro = 0 ;
int minP = prices[0];
for (int i = 0 ; i < n ; i ++){
int curPro = prices[i] - minP;
maxPro = max (maxPro ,curPro);
minP = min( minP, prices[i]);
}
return maxPro;
}
};
其实我感觉这个题目有点像定长滑窗,长度是m,然后更新的值是乘积。
先自己写一下试试看吧!
不对不对,子序列不是滑动窗口,注意子序列是可以删除元素的!!!
啊,不知道怎么写。如果这个题是全部正数的就很简单了,但是有负数,不知道怎么维护。
ai老师登场。
问题
你现在需要解决的问题是:
给定整数数组 nums 和整数 m,找到任意长度为 m 的子序列中首尾元素乘积的最大值。
核心难点在于子序列的特性(不改变顺序但可删元素)和存在负数的情况,这让直接暴力枚举所有可能的子序列变得不可行。
解题思路分析
首先明确核心逻辑:
- 长度为
m的子序列,其首尾元素在原数组中的位置满足j - i >= m - 1(即从原数组第i个元素到第j个元素,中间至少有m-1个元素,才能选出长度为m的子序列)。 - 我们需要遍历所有满足条件的
(i, j)对,计算nums[i] * nums[j],并找到最大值。 - 由于数组长度可达
1e5,暴力枚举所有i,j会超时,因此需要优化:- 对于每个可能的结尾位置
j,能和它配对的起始位置i必须满足i <= j - (m-1)。 - 我们只需要在
[0, j - (m-1)]这个范围内,找到能让nums[i] * nums[j]最大的nums[i]即可。 - 由于存在负数,最大值可能来自:
- 最大的正数 × 正数
- 最小的负数 × 负数(负负得正,可能更大)
- 对于每个可能的结尾位置
自己写的代码错误百出
cpp
class Solution {
public:
long long maximumProduct(vector<int>& nums, int m) {
int n = nums.size();
long long max_val = LLONG_MIN;
if (m == 1){
for(int i = 0 ; i < n ; ++i){
long long maxNum = 0 ;
maxNum = max(maxNum , (long long)nums[i]);
max_val = maxNum * maxNum ;
}
return max_val;
}
long long maxNum = nums[0];
long long minNum = nums[0];
long long res = LLONG_MIN;
for (int j = m - 1 ; j < n ; j++){
int i = j - m +1 ;
if (i > 0){
maxNum = max(maxNum , (long long)nums[i]);
minNum = min(minNum , (long long)nums[i]);
long long cur1 = (long long)nums[i] * maxNum ;
long long cur2 = (long long)nums[i] * minNum ;
res = max(max(cur1 , cur2) , res);
}
}
return res ;
}
};
关键修改点说明
| 问题类型 | 错误代码行为 | 修复后行为 |
|---|---|---|
| m=1 逻辑错误 | 初始化 maxNum=0,截断负数 | 直接计算每个元素的平方,遍历取最大值(包含负数的平方) |
| 边界覆盖不全 | 只处理 i>0 的情况,漏掉 i=0 | 把计算逻辑移到 if 外,i=0 时直接用初始的 max_start/min_start 计算 |
| 乘积逻辑错误 | 计算 nums [i]*maxNum(同位置相乘) | 计算 max_startnums[j]/min_startnums [j](起始 |
修正后通过的代码
cpp
class Solution {
public:
long long maximumProduct(vector<int>& nums, int m) {
int n = nums.size();
long long max_val = LLONG_MIN;
if (m == 1){
for(int i = 0 ; i < n ; ++i){
long long square = (long long )nums[i] * nums[i];
max_val = max(square ,max_val);
}
return max_val;
}
long long maxNum = nums[0];
long long minNum = nums[0];
long long res = LLONG_MIN;
for (int j = m - 1 ; j < n ; j++){
int i = j - m +1 ;
if (i > 0){
maxNum = max(maxNum , (long long)nums[i]);
minNum = min(minNum , (long long)nums[i]);
}
long long cur1 = (long long)nums[j] * maxNum ;
long long cur2 = (long long)nums[j] * minNum ;
res = max(max(cur1 , cur2) , res);
}
return res ;
}
};
3 小结
反思
看起来很恐怖的代码(至少在过去的我看来比较麻烦),但实际上思路很清晰。
case 1 : 如果m给的长度是1 ,那么就是找一个最大平方,这里我原先还弄错了变成了找最大的数,再平方。
case 2 : m给的长度大于1 ,相当于我要在一个m长度的序列里面找到最大的乘积,这里采用的是j作为循环遍历,其实用i也是一样的。
判断i是否大于0 ,就是在判断是否还有余下的序列更新。所有注意更新的循环放在里面,但是计算的逻辑放在循环外面。(这里我又弄错)
计算两个cur ,处理存在负数,负负得正的情况。
修改
一、核心修改点(按优先级排序)
| 类别 | 原代码问题 | 修复方案 |
|---|---|---|
| 编译 / 作用域 | 无(本次修改后解决了编译问题,但核心是逻辑) | - 原代码曾将cur1/cur2定义在if(i>0)块内,外部访问报错;修复后移到块外,确保全程可访问 |
| m=1 逻辑错误 | 初始化maxNum=0截断负数,直接赋值覆盖结果 |
1. 取消maxNum=0的错误初始化,直接计算每个元素的平方;2. 用max(max_val, square)迭代更新最大值,而非直接赋值 |
| 边界覆盖不全 | 仅处理i>0的情况,漏掉i=0的关键场景 |
把乘积计算逻辑移出if(i>0)块,i=0时用初始的max_start/min_start计算,确保第一个有效j(j=m-1)能被处理 |
| 乘积逻辑错误 | 错误计算nums[i]*maxNum(同位置相乘) |
修正为max_start*nums[j]/min_start*nums[j](起始位置最值 × 结尾位置值),符合 "子序列首尾乘积" 的定义 |
| 变量维护错误 | 错误更新maxNum=max(maxNum, nums[j]) |
修正为max_start=max(max_start, nums[i_max]),维护的是起始位置范围的最值,而非结尾位置值 |
二、核心反思点(新手易踩坑 + 解题思路层面)
1. 对 "特殊场景" 的思考不足
- 反思:
m=1是题目中 "子序列首尾为同一个元素" 的特殊场景,最初错误地用 "正数最大值" 的思路处理,忽略了负数平方可能更大的特性。 - 改进:遇到 "乘积 / 平方" 类问题,只要有负数参与,必须优先考虑 "负负得正" 的情况,不能默认只取正数。
2. 对 "变量维护目标" 的理解模糊
- 反思:原代码混淆了 "维护起始位置的最值" 和 "维护结尾位置的值",错误地更新
maxNum为nums[j],本质是没理清 "子序列首尾位置的约束关系"(j - i >= m-1)。 - 改进:解题前先明确 "维护的变量对应什么场景"------ 本题中
max_start/min_start是 "所有能和j配对的起始位置i的最值",而非结尾位置的值。
3. 对 "边界条件" 的验证不足
- 反思:原代码只处理
i>0的情况,漏掉了i=0(第一个有效j),本质是没验证 "循环第一次执行时的场景"。 - 改进:写循环逻辑时,先手动代入第一个循环值 (如
j=m-1,i=0)验证,确认边界场景能被覆盖。
4. 对 "变量作用域" 的基础认知不扎实
- 反思:最初将
cur1/cur2定义在if块内,导致外部访问报错,是新手常见的语法漏洞。 - 改进:定义变量时先明确 "该变量是否需要跨块使用",跨块使用的变量需定义在块外,或在块内完成所有相关计算。
三、解题思路层面的核心总结
- 本题的本质是 "找满足
j - i >= m-1的(i,j)对,计算nums[i]*nums[j]的最大值",而非暴力枚举子序列; - 处理负数乘积时,必须同时维护 "最大值" 和 "最小值"------ 最小值(最负)× 负数可能得到更大的正数;
- 特殊场景(如
m=1)需单独处理,避免通用逻辑覆盖特殊情况。
这些反思不仅能解决本题,也适用于所有 "数组最值 + 负数参与运算" 的题目,核心是:先明确变量的语义,再验证边界场景,最后考虑负数的特殊运算规则。
概述
一、核心反思
- 特殊场景别漏判 :
m=1是首尾同元素的特殊情况,需直接算元素平方取最大,而非默认取正数再平方,忽略负数平方的更大值。 - 变量语义要盯死 :维护的
maxNum/minNum是可配对的起始位置最值 ,不是结尾位置值,更新时要加起始位置的nums[i],而非nums[j]。 - 边界条件先验证 :循环首次执行(
i=0)必须覆盖,计算逻辑不能只放在i>0的分支里,否则会漏掉初始的有效配对。 - 负数乘积双维护:涉及乘积最值且有负数时,要同时维护最大值和最小值 ------ 负负得正可能出现更大结果,只维护一个最值必错。
二、同类题解题心法
- 化繁为简抓本质 :子序列首尾配对问题 → 转化为满足
j-i≥m-1的(i,j)对乘积最值,避开暴力枚举子序列的坑。 - 遍历一端护两端 :固定结尾
j,维护j左侧可配对范围的最值,O (n) 遍历即可,无需嵌套循环。 - 负数乘积必双持:只要有负数参与乘积最值计算,必须同时维护当前范围的最大值和最小值,两种乘积都要算。
- 边界特殊先处理 :先解决
m=1等特殊情况,再写通用逻辑,避免特殊场景干扰正常流程。