Problem: 3640. 三段式数组 II
文章目录
- [1. 整体思路](#1. 整体思路)
- [2. 完整代码](#2. 完整代码)
- [3. 时空复杂度](#3. 时空复杂度)
-
-
- [时间复杂度: O ( N ) O(N) O(N)](#时间复杂度: O ( N ) O(N) O(N))
- [空间复杂度: O ( 1 ) O(1) O(1)](#空间复杂度: O ( 1 ) O(1) O(1))
-
1. 整体思路
核心问题
我们需要找一个子数组,满足 升 -> 降 -> 升 的形状,并使元素和最大。
状态定义
我们可以将这个波形结构拆解为三个阶段(状态机):
- 阶段 1 (Rise 1) :正在经历第一段上升。
- 状态
f1:表示以当前元素nums[i]结尾的、处于第一段上升阶段的最大子数组和。
- 状态
- 阶段 2 (Fall) :正在经历中间的下降。
- 状态
f2:表示以当前元素nums[i]结尾的、处于下降阶段的最大子数组和。 - 它可以从
f1转移而来(刚开始下降,即峰值转折),也可以从之前的f2延续而来(继续下降)。
- 状态
- 阶段 3 (Rise 2) :正在经历第二段上升。
- 状态
f3:表示以当前元素nums[i]结尾的、处于第二段上升阶段的最大子数组和。 - 它可以从
f2转移而来(刚开始上升,即谷值转折),也可以从之前的f3延续而来(继续上升)。
- 状态
状态转移
遍历数组,对于每一对相邻元素 x = nums[i-1] 和 y = nums[i]:
-
更新
f3(第二段上升):- 只有当
x < y(上升) 时,才能处于此状态。 - 来源 A:从谷底转折而来。前一个状态是
f2(下降段结尾)。此时y是上升的第一步。总和 =f2 + y。 - 来源 B:延续上升。前一个状态是
f3。总和 =f3 + y。 - 方程:
f3 = (x < y) ? Math.max(f3, f2) + y : NEG_INF。 - 注意 :这里用的是更新前的
f2和f3(上一轮的值)。但在代码中,由于是顺序更新,我们需要小心变量覆盖问题。
- 只有当
-
更新
f2(下降):- 只有当
x > y(下降) 时,才能处于此状态。 - 来源 A:从峰顶转折而来。前一个状态是
f1(第一段上升结尾)。此时y是下降的第一步。总和 =f1 + y。 - 来源 B:延续下降。前一个状态是
f2。总和 =f2 + y。 - 方程:
f2 = (x > y) ? Math.max(f2, f1) + y : NEG_INF。
- 只有当
-
更新
f1(第一段上升):- 只有当
x < y(上升) 时,才能处于此状态。 - 来源 A:新的开始。子数组只包含
x和y。总和 =x + y。 - 来源 B:延续上升。前一个状态是
f1。总和 =f1 + y。 - 方程:
f1 = (x < y) ? Math.max(f1, x) + y : NEG_INF。 - 注意 :这里
Math.max(f1, x)的x其实代表的是x作为起点的 sum,即之前的 sum 是x。
- 只有当
-
全局最优:
- 每次更新完
f3后,尝试更新ans。
- 每次更新完
关键细节 :
在代码中,f3, f2, f1 是按顺序更新的。
- 更新
f3时用到的是旧的f3和 旧的f2。 - 更新
f2时用到的是旧的f2和 旧的f1。 - 更新
f1时用到的是旧的f1。 - 只要按
f3 -> f2 -> f1的倒序更新,就可以直接使用变量而不需要临时变量保存上一轮状态。
2. 完整代码
java
class Solution {
public long maxSumTrionic(int[] nums) {
// 定义无穷小。使用 Long.MIN_VALUE / 2 是为了防止在后续加法运算中溢出变成正数
final long NEG_INF = Long.MIN_VALUE / 2;
long ans = NEG_INF;
// f1: 以当前元素结尾,处于第一上升段的最大和
long f1 = NEG_INF;
// f2: 以当前元素结尾,处于下降段的最大和
long f2 = NEG_INF;
// f3: 以当前元素结尾,处于第二上升段的最大和
long f3 = NEG_INF;
// 从第二个元素开始遍历
for (int i = 1; i < nums.length; i++) {
int x = nums[i - 1]; // 前一个数
int y = nums[i]; // 当前数
// 1. 更新 f3 (第二段上升状态)
// 必须满足上升条件 x < y
// 转移来源:
// - f2: 刚从下降段转折过来 (谷底)
// - f3: 延续之前的第二上升段
// 注意:这里使用的是上一轮循环的 f2 和 f3
if (x < y) {
f3 = Math.max(f3, f2) + y;
} else {
// 如果不满足上升,说明无法维持 f3 状态,重置为无效
f3 = NEG_INF;
}
// 2. 更新 f2 (下降状态)
// 必须满足下降条件 x > y
// 转移来源:
// - f1: 刚从第一上升段转折过来 (峰顶)
// - f2: 延续之前的下降段
if (x > y) {
f2 = Math.max(f2, f1) + y;
} else {
// 如果不满足下降,重置为无效
f2 = NEG_INF;
}
// 3. 更新 f1 (第一段上升状态)
// 必须满足上升条件 x < y
// 转移来源:
// - x: 新开启一段上升 (丢弃之前的,只取 x 和 y)
// - f1: 延续之前的第一上升段
if (x < y) {
// Math.max(f1, x) 的意思是:
// 要么接在之前的上升段后面 (f1 + y)
// 要么重新开始,只包含前一个数和当前数 (x + y)
// 这里利用了 Math.max(f1 + y, x + y) = Math.max(f1, x) + y 的分配律
f1 = Math.max(f1, x) + y;
} else {
f1 = NEG_INF;
}
// 每次迭代尝试更新全局最大值
// 只有当 f3 有效时 (即我们已经完整经历了 升-降-升) 才有资格更新 ans
ans = Math.max(ans, f3);
}
return ans;
}
}
3. 时空复杂度
假设数组 nums 的长度为 N N N。
时间复杂度: O ( N ) O(N) O(N)
- 计算依据 :
- 代码包含单次循环,遍历整个数组。
- 循环体内的操作(比较、加法、赋值)均为 O ( 1 ) O(1) O(1)。
- 结论 : O ( N ) O(N) O(N)。
空间复杂度: O ( 1 ) O(1) O(1)
- 计算依据 :
- 只使用了 5 个变量 (
ans,f1,f2,f3,i)。 - 没有使用 DP 数组,而是使用了滚动变量优化空间。
- 只使用了 5 个变量 (
- 结论 : O ( 1 ) O(1) O(1)。