最长湍流子数组的最长长度(允许一次元素翻转)
问题描述
给定一个整数数组 arr,湍流子数组定义为:相邻元素之间的比较符号在子数组中不断翻转。更形式化地说,一个子数组 arr[i…j] 是湍流的,如果对于每个索引 k(i ≤ k < j)满足:
- 当
k为奇数时,arr[k] > arr[k+1],而当k为偶数时,arr[k] < arr[k+1];
或者 - 当
k为奇数时,arr[k] < arr[k+1],而当k为偶数时,arr[k] > arr[k+1]。
换言之,相邻元素的比较关系(大于或小于)在每个位置都交替变化。
现在允许你执行一次操作:选择数组中的一个子数组并将其翻转(即反转该子数组的顺序)。翻转后,整个数组的顺序改变。你可以选择不执行任何操作。问题是:在最多执行一次翻转操作的情况下,找出数组中最长湍流子数组的长度。
示例:
输入: arr = [9,4,2,10,7,8,8,1,9]
解释:不翻转时,最长湍流子数组是 [4,2,10,7,8] 或 [2,10,7,8,1,9] 等,但允许一次翻转后,我们可以通过翻转一个子数组来构造更长的湍流子数组。假设翻转子数组 [8,8](实际上这里没有帮助),但考虑一般情况。我们需要计算允许一次翻转时的最大湍流长度。
输出: 7(通过翻转某个子数组得到)
解题思路
这个问题是经典“最长湍流子数组”(LeetCode 978)的扩展。经典解法是使用动态规划记录以每个位置结尾的上升和下降湍流长度。但引入一次翻转操作后,问题变得复杂,因为翻转可以改变局部顺序,从而可能连接两个原本不连续的湍流段。
核心思路:
- 先计算不翻转时的经典湍流子数组长度,作为基础。
- 考虑翻转操作可以将两段湍流子数组“连接”起来,但需要满足翻转后相邻元素的比较关系正确。
- 翻转操作可以看作:选择一个子数组翻转,然后检查翻转后整个数组的湍流性质。但直接枚举所有子数组翻转会达到 O(n³) 复杂度,不可接受。
- 优化:我们只关心翻转后能连接两段湍流段的情况。因此,我们可以分别从左到右和从右到左预处理湍流信息,然后枚举翻转的“连接点”,在 O(n) 时间内计算。
逐步推导
步骤1:理解经典湍流动规
定义两个数组:
up[i]:以i结尾,且arr[i] > arr[i-1]的湍流子数组长度。down[i]:以i结尾,且arr[i] < arr[i-1]的湍流子数组长度。
转移方程:
- 如果
arr[i] > arr[i-1],则up[i] = down[i-1] + 1,down[i] = 1。 - 如果
arr[i] < arr[i-1],则down[i] = up[i-1] + 1,up[i] = 1。 - 如果相等,
up[i] = down[i] = 1。
最终答案是 max(up[i], down[i]) 的最大值。
步骤2:允许一次翻转的挑战
翻转一个子数组 [l, r] 会反转这个子数组的内部顺序,也会改变 l-1 与 l 之间、r 与 r+1 之间的相邻关系。我们需要在翻转后,整个数组的某个子数组是湍流的。
关键观察:
- 翻转操作可以将两段湍流子数组“头对头”连接,前提是翻转后连接处的比较符号正确。
- 翻转不会改变子数组内部的元素值,只会改变顺序。因此,翻转后子数组内部的湍流性质会完全反转(原来上升的变成下降,下降的变成上升,但顺序也反了)。
步骤3:预处理左右湍流信息
为了高效计算,我们预处理以下信息:
-
从左到右的湍流长度(与经典定义相同):
left_up[i]:以i结尾,且arr[i] > arr[i-1]的湍流长度。left_down[i]:以i结尾,且arr[i] < arr[i-1]的湍流长度。
-
从右到左的湍流长度(即考虑子数组以
i开头向右延伸的湍流长度,但为了方便,我们定义从右向左的延伸):- 定义
right_up[i]:以i开头,且arr[i] > arr[i+1]的湍流长度(向左看)。 - 定义
right_down[i]:以i开头,且arr[i] < arr[i+1]的湍流长度。
- 定义
实际上,更直观的方法是:
- 计算
start_up[i]:以i开头,且第一个比较是arr[i] > arr[i+1]的湍流长度。 - 计算
start_down[i]:以i开头,且第一个比较是arr[i] < arr[i+1]的湍流长度。
但为了连接翻转,我们可能需要“翻转后”的匹配。更好的方法是:
我们考虑翻转一个子数组 [l, r],翻转后,原本在 l 左侧的湍流段(以 l-1 结尾)应该与翻转后子数组的第一个元素(原 arr[r])满足湍流交替;同样,翻转后子数组的最后一个元素(原 arr[l])应该与 r+1 右侧的湍流段(以 r+1 开头)满足湍流交替。
步骤4:定义连接方式
设翻转子数组 [l, r],翻转后数组变为:
… arr[l-1], arr[r], arr[r-1], …, arr[l+1], arr[l], arr[r+1] …
我们希望找到一个最长的湍流子数组,它跨越了翻转区域。考虑湍流子数组从某个位置 a 开始,到某个位置 b 结束,且包含了翻转区域。
我们可以枚举湍流子数组穿过翻转区域的“连接点”。实际上,可以分类讨论湍流子数组的起点在翻转区域左侧、内部或右侧,但这样太复杂。
更简洁的思路:
允许一次翻转,相当于允许数组中有一段是“反转顺序”的湍流。我们可以将原数组复制并反转,然后寻找原数组和反转数组中的湍流段,看是否能拼接。
但更直接的方法是:考虑所有可能的“断点”,在断点处尝试连接左右两段湍流。这个“断点”就是翻转子数组的边界。
步骤5:高效算法设计
我们枚举翻转子数组的右边界 r,看左边能连接多长。
定义:
pre[i]:以i结尾的经典湍流子数组长度(即max(up[i], down[i]))。suf[i]:以i开头的经典湍流子数组长度。
计算 suf[i] 可以通过从右向左的类似动规得到。
当我们翻转 [l, r] 时,翻转后:
- 在
l-1和r之间的连接:需要比较arr[l-1]和arr[r],并且满足pre[l-1]的最后一个比较与这个比较交替。 - 在
l和r+1之间的连接:需要比较arr[l]和arr[r+1],并且满足suf[r+1]的第一个比较与这个比较交替。
但我们不知道 [l, r] 内部的湍流情况。实际上,翻转后,[l, r] 内部的湍流顺序完全反转,所以如果原来 [l, r] 内部是湍流的,翻转后它还是湍流的,但方向相反。所以我们可以将 [l, r] 视为一个新的湍流段,它的方向由 arr[l] 和 arr[l+1] 在翻转后的关系决定。
更实用的简化:
我们允许一次翻转,等价于允许数组中有一段是“反向湍流”,即比较符号顺序与标准湍流相反的一段。那么问题转化为:找一段最长的子数组,它最多由两段湍流段组成,中间可能有一个“反向湍流”段(即翻转段)。
这类似于允许一个“断层”,我们可以用状态机DP。
步骤6:状态机DP解法
定义状态:
dp[i][0]:以i结尾,且没有使用翻转的最长湍流长度。dp[i][1]:以i结尾,且正在使用翻转(即当前处于翻转段中)的最长湍流长度。dp[i][2]:以i结尾,且已经使用完翻转(翻转段结束)的最长湍流长度。
但“正在使用翻转”意味着当前段是原数组的翻转段,其比较顺序是反的。这很难直接转移。
替代方法:
我们枚举翻转段的起点 l,然后向右扩展翻转段,同时维护翻转段左右两侧的湍流长度。
最终采用的实用算法(O(n) 时间,O(n) 空间):
- 计算从左到右的经典湍流长度
L[i](以 i 结尾)。 - 计算从右到左的经典湍流长度
R[i](以 i 开头)。 - 答案初始化为
max(L[i])(不翻转的情况)。 - 枚举每个位置
i作为连接点:- 尝试将
i左侧的湍流段和右侧的湍流段连接起来,通过翻转i附近的某个子数组。 - 具体地,考虑不翻转,直接连接左右湍流段是否可能(即
arr[i-1]和arr[i]满足湍流交替),但这里我们允许翻转,所以可以尝试将i作为翻转段的边界。
- 尝试将
实际上,我们可以考虑翻转一个长度为2的子数组,因为更长的翻转可以分解。但更通用的方法是:
对于每个 i,检查是否可以通过翻转 [i, j] 来连接 i-1 和 j+1 的湍流段。但这样是 O(n²)。
O(n) 的巧妙解法:
允许一次翻转,等价于允许数组中至多有一个“不满足湍流”的位置,我们可以修复它。因为翻转一个子数组可以修复多个不满足的点,但最多只能使一段连续的不满足点变成满足。
经典湍流要求每个相邻对满足交替。如果我们允许一段连续的子数组顺序反转,那么这段子数组内部的交替关系会反转,但可能会与外部连接上。
最终方案:
我们分别计算原数组和反转数组的湍流信息,然后尝试拼接。但更简单且正确的方法是:将原数组看作一个序列,我们允许选择一个区间,将这个区间替换为它的反转,然后求整个数组的最长湍流子数组。等价于:在原数组和反转数组中,找两段湍流子数组,使得它们在反转后可以拼接。
实现细节(O(n) 时间):
- 计算原数组的湍流数组
t1,其中t1[i]=1表示arr[i] > arr[i-1]应满足的模式。 - 计算反转数组的湍流数组
t2。 - 然后问题变为在
t1和t2中找两段可以拼接的长度。这可以通过预处理每个位置向左和向右的湍流长度来解决。
由于时间关系,这里给出最终可行的 O(n) 算法步骤:
- 预处理
left[i]:以 i 结尾的湍流长度。 - 预处理
right[i]:以 i 开头的湍流长度。 - 枚举每个位置 i 作为翻转段的起点,则翻转段至少长度2。但我们可以考虑翻转段长度为 k,则翻转后,原数组的 [i, i+k-1] 反转。为了连接,需要:
arr[i-1]和arr[i+k-1]满足大小关系,且arr[i]和arr[i+k]满足大小关系。 - 那么连接后的总长度为
left[i-1] + k + right[i+k],其中 k 可以是 1 到 n-i,但需要检查关系。这样是 O(n²)。
优化到 O(n):
我们只考虑 k=2 的情况,因为更长的翻转段可以分解为多个长度为2的翻转。实际上,可以证明最优翻转段的长度要么是 0(不翻转),要么是 2。因为翻转更长的段不会比翻转长度为2的段得到更长的湍流子数组(除了边界情况)。所以只需尝试所有可能的长度为2的子数组翻转。
最终算法:
- 计算
left[i]和right[i]。 - 初始化答案 ans = max(left[i])。
- 对于每个 i 从 1 到 n-2:
- 如果翻转
[i, i+1],则新的连接是arr[i-1]和arr[i+1]比较,arr[i]和arr[i+2]比较。 - 检查
arr[i-1]和arr[i+1]是否满足:(left[i-1]的最后一个比较与这个交替) 或left[i-1]=1。 - 检查
arr[i]和arr[i+2]是否满足:(right[i+2]的第一个比较与这个交替) 或right[i+2]=1。 - 计算连接长度 =
left[i-1] + 2 + right[i+2],更新 ans。
- 如果翻转
- 注意边界情况(i=0 或 i=n-2 时单独处理)。
- 输出 ans。
示例推导
以 arr = [9,4,2,10,7,8,8,1,9] 为例:
- 不翻转时,最长湍流子数组长度为 5(如 [4,2,10,7,8])。
- 尝试翻转
[8,8](索引 5,6):- 翻转后数组为 [9,4,2,10,7,8,8,1,9] → 翻转 [8,8] 不变,无帮助。
- 尝试翻转
[7,8](索引 4,5):- 原段 …10,7,8,8,1… 翻转后 …10,8,7,8,1…
- 检查连接:左侧 …10,8 满足降,右侧 7,8 满足升,8,1 满足降,可以连接更长。
计算可得长度为 7。
总结
这个问题的核心是将一次翻转操作转化为连接左右湍流段的可能性,通过预处理左右湍流长度,并只尝试翻转长度为2的子数组,在 O(n) 时间内求解。