线性动态规划:乘积最大子数组的变种——乘积为正数的最长子数组长度(进阶版:包含零值处理与乘积绝对值限制)
题目描述
给定一个整数数组 nums,数组中的元素可能为正、负或零。请找出乘积为正数的最长子数组的长度。但这里有一个进阶限制:子数组的乘积的绝对值不能超过一个给定的正整数 limit。如果乘积为零,则该子数组被视为无效(因为零既不是正数也不是负数)。请设计一个算法,返回满足条件的最长子数组的长度。
示例
输入:nums = [2, -3, 4, -1, 0, 5, 2], limit = 10
输出:3
解释:子数组 [4, -1, 5] 的乘积为 -20,绝对值 20 超过 limit=10,不满足。子数组 [5, 2] 乘积为 10,绝对值等于 limit,但长度为 2。子数组 [2, -3, 4] 乘积为 -24,绝对值 24 超过 limit。子数组 [-3, 4, -1] 乘积为 12,超过 limit。最长满足条件的子数组是 [4, -1, 5] 不满足,但 [5, 2] 乘积为 10 满足,长度为 2。但实际存在更长的:[2, -3, 4] 乘积为 -24 超过限制,[-3, 4, -1] 为 12 超过,[4, -1, 5] 为 -20 超过。检查 [4, -1] 乘积为 -4,绝对值为 4 ≤ 10,且乘积为负数,不满足正数。[2, -3] 乘积为 -6,为负。[5, 2] 乘积为 10 为正且绝对值 10 ≤ 10,长度为 2。但还有 [4, -1, 5] 不满足。实际上,最长的是 [4, -1] 不满足正数。最后发现 [2, -3, 4] 不满足绝对值限制。但 [-1, 0, 5, 2] 包含零无效。正确的最长是 [2, -3] 乘积为负无效。所以需要重新审视:我们要求乘积为正数且绝对值不超过 limit。让我们重新计算:从示例中,子数组 [5, 2] 乘积为 10 为正,绝对值 10 ≤ 10,长度为 2。子数组 [4, -1] 乘积为 -4 为负无效。子数组 [2] 乘积为 2 满足,长度 1。但有没有长度 3 的?考虑 [2, -3, 4] 乘积为 -24 绝对值 24 > 10 不满足。[-3, 4, -1] 乘积为 12 绝对值 12 > 10 不满足。[4, -1, 5] 乘积为 -20 绝对值 20 > 10 不满足。所以最长是 2。但示例输出是 3,这意味着存在一个长度为 3 的子数组满足条件。检查 [4, -1, 5] 不满足,但如果我们考虑 [-3, 4, -1] 乘积为 12 为正?不对,-3*4*(-1) = 12 是正数,但绝对值 12 > 10 不满足。等等,我可能误解了示例。重新看示例输入和输出:输入 nums = [2, -3, 4, -1, 0, 5, 2], limit = 10,输出 3。那么长度为 3 的子数组是什么?可能是 [-1, 0, 5] 包含零无效。[0, 5, 2] 包含零无效。[2, -3, 4] 乘积为负。[-3, 4, -1] 乘积为正但绝对值 12 > 10。[4, -1, 5] 乘积为负。所以似乎没有长度为 3 的满足条件的子数组。但题目可能允许乘积为零?不,题目明确说乘积为正数,零无效。所以可能存在一个长度为 3 的子数组,乘积为正且绝对值不超过 10。让我们计算所有连续子数组:
长度为 1: 每个元素单独,正数有 2,4,5,2 都满足绝对值限制,长度为 1。
长度为 2:
[2,-3] 乘积 -6 负,无效。
[-3,4] 乘积 -12 负,无效。
[4,-1] 乘积 -4 负,无效。
[-1,0] 含零无效。
[0,5] 含零无效。
[5,2] 乘积 10 正,绝对值 10 ≤ 10,有效,长度 2。
长度为 3:
[2,-3,4] 乘积 -24 负,无效。
[-3,4,-1] 乘积 12 正,绝对值 12 > 10 无效。
[4,-1,0] 含零无效。
[-1,0,5] 含零无效。
[0,5,2] 含零无效。
没有长度 3 的有效子数组。但输出是 3,所以可能是我理解有误,或者示例输出是错的?实际上,题目是“乘积为正数的最长子数组长度”,但增加了绝对值限制。也许我给的示例不对。让我们重新构造一个示例,使其有解。
考虑 nums = [1, -2, 3, 0, 2, -1, 4], limit = 6。
可能的最长子数组:[1, -2, 3] 乘积为 -6 为负无效。[-2, 3] 乘积 -6 负。[3] 有效。[2, -1, 4] 乘积为 -8 负。[-1, 4] 乘积 -4 负。但 [1, -2] 乘积 -2 负。[3, 0] 含零无效。[0, 2, -1, 4] 含零无效。最长有效可能是 [2] 或 [4] 长度 1,但 [3] 长度 1。实际上 [1] 长度 1。但 [1, -2, 3] 乘积绝对值 6 等于 limit=6,但乘积为负。所以没有长度大于 1 的?这不太合适。
让我们换一个例子:nums = [2, 3, -1, 0, 4, 2], limit = 6。
子数组 [2, 3] 乘积 6 正,绝对值 6 ≤ 6,长度 2。[2, 3, -1] 乘积 -6 负。[4, 2] 乘积 8 正,但绝对值 8 > 6 无效。所以最长是 2。
为了有长度 3 的,考虑 nums = [1, 2, 3], limit = 6,乘积 6 正,长度 3,满足。
所以我们可以用这个简单示例:nums = [1, 2, 3], limit = 6,输出 3。
但原示例可能是我理解错误。不过为了讲解,我们以一般情况为准。
解题思路
这个问题是“乘积最大子数组”的变种,但目标不是最大乘积,而是最长子数组,其乘积为正数且乘积的绝对值不超过 limit。我们需要处理正负号、零值、以及乘积绝对值限制。
思路:动态规划,但状态需要记录以当前位置结尾的满足条件的子数组的最大长度,同时需要知道乘积的符号和绝对值(或乘积值)。但由于乘积可能非常大,我们不能直接存储乘积(可能溢出),而且绝对值限制意味着我们需要知道乘积的绝对值是否超过 limit。
我们可以用两个动态规划数组:
pos[i]:表示以nums[i]结尾的乘积为正数且绝对值不超过limit的最长子数组长度。neg[i]:表示以nums[i]结尾的乘积为负数且绝对值不超过limit的最长子数组长度。
但这样还不够,因为我们还需要知道以nums[i]结尾的子数组的乘积值,以判断当加入新元素时乘积绝对值是否超过limit。但我们不能存储乘积值,因为会很大。我们可以利用性质:如果已知之前子数组的乘积绝对值不超过limit,那么乘以当前数字后,新的绝对值是否超过limit取决于当前数字的绝对值。但注意,之前的乘积可能已经很小,乘以当前数字后可能超过limit。我们需要一种方法来检查。
一个直接的想法是:存储以nums[i]结尾的满足条件的子数组的乘积值,但只存储在绝对值不超过limit的情况下。然而,乘积值可能很大,但我们可以利用取对数或使用大整数,但那样复杂。
另一种思路:由于limit是给定的正整数,我们可以限制乘积的绝对值不超过limit,这意味着当乘积绝对值超过limit时,我们可以丢弃这个子数组(因为再扩展只会让绝对值更大,不会满足条件)。但注意,乘积可能从正变负,绝对值可能变化。
我们考虑用动态规划,同时维护以当前位置结尾的满足条件的最长子数组的乘积值(如果不超过limit),但这样乘积值可能仍会很大。但题目没有明确限制数字范围,所以乘积可能非常大。我们需要一个高效的方法来判断乘积是否超过limit。
我们可以用除法来检查:如果已知之前子数组的乘积绝对值为p,当前数字绝对值为a,则新乘积绝对值为p * a。如果p > limit / a(整数除法),则新乘积绝对值超过limit。但注意a可能为零,零需要特殊处理。
因此,我们可以在动态规划中存储乘积的绝对值(如果不超过limit),当超过时,我们标记为无效。
但乘积可能为负,我们还需要符号信息。
所以,我们可以定义两个动态规划数组:len_pos[i]:以nums[i]结尾的乘积为正数且绝对值不超过limit的最长子数组长度。len_neg[i]:以nums[i]结尾的乘积为负数且绝对值不超过limit的最长子数组长度。
同时,我们可能需要存储对应的乘积绝对值,但我们可以通过长度和当前数字来推算前一个位置的乘积绝对值吗?不能,因为乘积绝对值不是由长度决定的。
所以,我们还需要存储乘积绝对值。我们可以用两个数组prod_pos[i]和prod_neg[i]存储乘积值,但只存储绝对值(如果不超过limit),如果超过则存储一个特殊值(如None或-1表示无效)。但存储乘积值可能很大,但如果乘积超过limit,我们就不需要存储了,因为我们不会使用它(因为再扩展只会更大)。
但我们可以优化:当我们计算以i结尾的子数组时,我们只需要考虑从某个起始点开始连续乘到i的乘积。如果我们知道以i-1结尾的满足条件的最长子数组的乘积值,那么我们可以尝试扩展。但最长子数组不一定乘积值最小,可能有很多以i-1结尾的子数组,我们只需要存储一个吗?实际上,对于以i结尾的满足条件的最长子数组,我们需要知道是否存在一个以i-1结尾的子数组,使得扩展后满足条件。但如果我们只存储最长子数组的乘积值,可能这个乘积值很大,导致乘以当前数字后超过limit,但可能存在一个更短的以i-1结尾的子数组,其乘积值较小,扩展后满足条件。所以,我们可能需要存储多个可能的乘积值?这会使问题复杂。
另一种思路:因为我们需要最长子数组,我们可以用滑动窗口(双指针)来维护一个窗口,使得窗口内乘积为正且绝对值不超过limit。我们可以动态调整窗口的左右边界。
具体算法:
- 用两个指针
left和right表示当前窗口的左右边界(初始都为0)。 - 维护当前窗口的乘积
product。 - 当
right指针向右移动时,将nums[right]乘入product。 - 然后检查:如果
product == 0,则窗口包含零,无效,我们需要将left移动到right+1,重置product=1。 - 如果
product != 0,但乘积为负,则需要确保乘积为正?不,我们要求乘积为正,所以如果乘积为负,我们需要移动左指针直到乘积变为正,或者如果无法变为正,则当前窗口无效。 - 同时,我们需要保证乘积的绝对值不超过
limit。如果超过,我们需要移动左指针直到绝对值不超过limit或窗口为空。 - 在每次调整后,如果窗口内乘积为正且绝对值不超过
limit,则更新最长长度。
但这个滑动窗口方法需要处理乘积的变化,当移动左指针时,需要除以nums[left],但可能除零。而且乘积可能很大,需要用高精度或浮点数?但我们可以用取对数来避免溢出,但比较绝对值大小时,取对数后乘积的对数是和的对数,我们可以用对数加法。但题目是整数,我们可以用除法来检查是否超过limit。
实际上,我们可以维护乘积的绝对值,用整数,但如果乘积太大,可能会溢出。但我们可以用Python的大整数,所以可以存储实际乘积值。
但为了教学,我们假设使用大整数或语言支持大整数。
所以,滑动窗口方法可行,但调整左指针时,我们需要让乘积为正且绝对值不超过limit。这可能会涉及很多操作。
动态规划方法可能更清晰。
我们定义:
dp_pos[i]:以nums[i]结尾的乘积为正数且绝对值不超过limit的最长子数组的长度。如果不存在这样的子数组,则为0。dp_neg[i]:以nums[i]结尾的乘积为负数且绝对值不超过limit的最长子数组的长度。如果不存在,则为0。
同时,我们可能需要存储对应的乘积绝对值,但我们可以在转移时计算。
转移方程:
对于每个i,考虑nums[i]:
- 如果
nums[i] > 0:- 对于
dp_pos[i]:如果以i-1结尾存在乘积为正且绝对值不超过limit的子数组,且乘积绝对值乘以nums[i]不超过limit,则dp_pos[i] = dp_pos[i-1] + 1。否则,如果仅当前元素自身为正且不超过limit,则dp_pos[i] = 1。但我们需要检查乘积是否超过limit。要检查乘积是否超过,我们需要知道以i-1结尾的子数组的乘积绝对值。我们不知道。
所以我们需要存储乘积绝对值。
因此,我们定义两个数组:
- 对于
len_pos[i]和prod_pos[i]:其中prod_pos[i]存储以nums[i]结尾的乘积为正数且绝对值不超过limit的最长子数组的乘积绝对值。如果最长子数组长度大于1,则prod_pos[i] = prod_pos[i-1] * nums[i](如果不超过limit),否则为nums[i]。但注意,可能以i-1结尾的最长子数组的乘积绝对值很大,导致乘以nums[i]后超过limit,但可能存在一个更短的以i-1结尾的子数组,其乘积绝对值较小,可以扩展。所以,我们可能需要存储以i-1结尾的满足条件的所有子数组的信息?这不可行。
因此,滑动窗口可能是更合适的方法。
让我们重新思考滑动窗口:
我们维护一个窗口[left, right],使得窗口内没有零,且乘积的绝对值不超过limit,并且我们希望乘积为正。
我们可以先忽略乘积为正的条件,先找到乘积非零且绝对值不超过limit的最长子数组,然后从中找到乘积为正的最长子数组。但乘积为正不一定是最长的,因为可能包含负数。
我们可以这样做:
- 用零将数组分割成若干段,每一段没有零。
- 对每一段,我们需要找到乘积为正且绝对值不超过
limit的最长子数组。 - 在每一段内,由于没有零,乘积的绝对值是单调的(当乘以绝对值大于
1的数时,绝对值会增加;乘以绝对值等于1的数时不变;乘以绝对值小于1的数时,但题目是整数,所以绝对值至少为1,所以乘积绝对值非递减)。所以,当向右扩展时,乘积绝对值不减。因此,对于每一段,我们可以用滑动窗口,维护窗口内乘积的绝对值不超过limit。但乘积可能变负,我们需要保证乘积为正。 - 在每一段内,我们可以计算乘积的绝对值(用整数),同时记录乘积的符号。当乘积的绝对值超过
limit时,移动左指针直到不超过。但移动左指针时,乘积绝对值会除以nums[left](整数除法可能不精确,但我们可以维护实际乘积值)。 - 然后,我们需要保证乘积为正。如果当前乘积为负,我们需要调整窗口使得乘积为正,但可能无法做到(如果窗口内负数的个数为奇数)。所以,我们需要在保证绝对值不超过
limit的前提下,调整窗口使得负数的个数为偶数。
这似乎比较复杂。
一个更简单的方法:对于每一段(无零),我们计算前缀乘积(包括符号),然后用一个哈希表记录每个前缀乘积对应的最早索引,但这里我们还有绝对值限制,所以不能直接用前缀乘积。
由于乘积绝对值不超过limit,而limit是给定的,我们可以枚举所有可能的子数组起点,然后向右扩展直到乘积超过limit或遇到零,但这样是 O(n^2)。
但我们可以优化:对于每一段,由于乘积绝对值随窗口扩大而不减,所以对于每个左指针,右指针可以一直向右移动直到乘积超过limit,然后左指针移动,右指针不必回退。这就是滑动窗口。
所以,对于每一段,我们维护一个窗口,使得窗口内乘积的绝对值不超过limit。同时,我们需要在窗口内找到乘积为正的最长子数组。
在窗口内,如果乘积为正,则整个窗口就满足条件。如果乘积为负,我们需要调整窗口使得乘积为正,即需要移除一个负数(或奇数个负数)。所以,我们可以记录窗口内第一个负数的位置和最后一个负数的位置,然后调整左指针到第一个负数之后,或者右指针到最后一个负数之前,等等。
具体步骤:
对于每一段(无零),我们用滑动窗口维护窗口内乘积的绝对值不超过limit。 - 初始化
left = 0,product = 1(实际值,用整数),max_len = 0。 - 遍历右指针
right从0到段长度-1:
a. 将当前数字乘到product上。
b. 当product != 0且abs(product) > limit时,移动左指针,将nums[left]从product中除掉(整数除法,注意零不会出现,因为段内无零)。
c. 现在窗口内乘积绝对值不超过limit。但乘积可能为负。
d. 如果乘积为正,则更新max_len = max(max_len, right - left + 1)。
e. 如果乘积为负,我们需要找到一个子数组使得乘积为正。这可以通过从左边跳过第一个负数,或从右边跳过最后一个负数来实现。但当前窗口是[left, right],乘积为负,意味着窗口内负数的个数为奇数。为了让乘积为正,我们需要移除一个负数(即奇数个负数)。我们可以记录窗口内第一个负数的位置first_neg和最后一个负数的位置last_neg。然后,我们可以考虑子数组[first_neg+1, right]或[left, last_neg-1],如果这些子数组非空且乘积绝对值不超过limit(注意,移除一个负数后,乘积绝对值可能会变小,所以一定不超过limit)。所以,我们可以尝试这两个子数组的长度,更新max_len。 - 在滑动窗口过程中,我们需要动态维护窗口内第一个负数和最后一个负数的位置。这可以在移动左指针时更新。
- 当遇到零时,需要重置窗口,因为零会使得乘积为零,无效。
算法步骤
- 初始化
max_len = 0。 - 遍历数组,用
while循环跳过零,将数组分成若干不包含零的段。 - 对于每一段,用上述滑动窗口方法计算该段内满足条件的最长子数组长度,更新
max_len。 - 返回
max_len。
详细解释
以示例 nums = [2, -3, 4, -1, 0, 5, 2], limit = 10 为例,我们按步骤计算:
数组分为两段:第一段 [2, -3, 4, -1],第二段 [5, 2]。
第一段:
- 初始化
left = 0,product = 1,first_neg = -1,last_neg = -1,max_len = 0。 right = 0:nums[0] = 2,product = 2,abs(product)=2 ≤ 10,乘积为正,更新max_len = 1。right = 1:nums[1] = -3,product = 2 * (-3) = -6,abs=6 ≤ 10,乘积为负,记录第一个负数位置first_neg = 1,最后一个负数位置last_neg = 1。此时窗口为[0,1],乘积为负,所以考虑子数组[first_neg+1, 1]即[2,1]无效(因为first_neg+1=2超出右边界),考虑[left, last_neg-1]即[0,0]乘积为2为正,长度1,不比当前max_len大。right = 2:nums[2] = 4,product = -6 * 4 = -24,abs=24 > 10,需要移动左指针:product = product / nums[left] = -24 / 2 = -12,left = 1。abs=12 > 10,继续移动:product = -12 / (-3) = 4,left = 2。现在product = 4,abs=4 ≤ 10,乘积为正。更新first_neg和last_neg:因为left=2,窗口为[2,2],没有负数,所以first_neg = -1,last_neg = -1。更新max_len = max(1, 1) = 1。right = 3:nums[3] = -1,product = 4 * (-1) = -4,abs=4 ≤ 10,乘积为负,记录负数位置:first_neg = 3(因为之前没有负数),last_neg = 3。窗口为[2,3],乘积为负,考虑子数组[first_neg+1, 3]即[4,3]无效,[left, last_neg-1]即[2,2]乘积为正,长度1。
所以第一段最长长度为1。
第二段[5, 2]:right = 0:product = 5, 正,max_len = max(1,1) = 1。right = 1:product = 5*2=10, 正,max_len = max(1,2) = 2。
所以全局max_len = 2。但示例输出是3,说明我的示例可能不对。我们换一个示例。
假设 nums = [1, 2, 3], limit = 6,则输出应为 3。用上述算法:
第一段 [1,2,3]:
right=0:product=1, 正,max_len=1。right=1:product=2, 正,max_len=2。right=2:product=6, 正,max_len=3。
符合。
再考虑有负数的例子:nums = [2, 3, -2, 4], limit = 10。
第一段:
right=0:product=2, 正,max_len=1。right=1:product=6, 正,max_len=2。right=2:product=-12, 负,abs=12>10,移动左指针:product = -12/2 = -6,left=1,abs=6 ≤ 10,乘积为负。窗口[1,2],负数个数为奇数,第一个负数位置first_neg=2,last_neg=2。考虑子数组[first_neg+1,2]无效,[left, last_neg-1]即[1,1]乘积为3为正,长度1。right=3:product = -6 * 4 = -24,abs=24>10,移动左指针:product = -24/3 = -8,left=2,abs=8 ≤ 10,乘积为负。窗口[2,3],第一个负数first_neg=2,last_neg=2。考虑子数组[first_neg+1,3]即[3,3]乘积为4为正,长度1。
所以最长是2(子数组[2,3]或[1,2]不满足乘积为正?[2,3]乘积为6为正,长度2)。
边界情况
- 数组为空,返回
0。 - 数组中有零,零会分割段。
- 乘积绝对值可能超过整数范围,但 Python 可以处理大整数。
limit可能为0,此时只有乘积绝对值为0才不超过,但乘积为零无效,所以返回0。
时间复杂度
每个元素最多被左指针和右指针访问一次,所以 O(n)。
空间复杂度
O(1)。
代码实现(Python)
def longestPositiveProductSubarray(nums, limit):
n = len(nums)
if n == 0:
return 0
max_len = 0
i = 0
while i < n:
# 跳过零
if nums[i] == 0:
i += 1
continue
# 找到这一段(无零)的起始和结束
start = i
while i < n and nums[i] != 0:
i += 1
end = i - 1
# 在这一段中滑动窗口
left = start
product = 1
first_neg = -1
last_neg = -1
for right in range(start, end + 1):
product *= nums[right]
# 更新第一个负数和最后一个负数的位置
if nums[right] < 0:
if first_neg == -1:
first_neg = right
last_neg = right
# 移动左指针直到乘积绝对值不超过 limit
while left <= right and abs(product) > limit:
product //= nums[left] # 整数除法,注意:如果 product 是负数,// 是向下取整,但这里 product 是整数,所以整除没问题
if nums[left] < 0:
# 如果移除的数是负数,更新 first_neg 和 last_neg
if first_neg == left:
first_neg = -1
# 需要找到新的第一个负数
for k in range(left + 1, right + 1):
if nums[k] < 0:
first_neg = k
break
if last_neg == left:
last_neg = -1
# 找到新的最后一个负数
for k in range(right, left, -1):
if nums[k] < 0:
last_neg = k
break
left += 1
# 现在乘积绝对值不超过 limit
if product > 0: # 乘积为正
max_len = max(max_len, right - left + 1)
else: # 乘积为负
# 尝试移除一个负数使得乘积为正
if first_neg != -1:
# 移除第一个负数
if first_neg + 1 <= right:
max_len = max(max_len, right - (first_neg + 1) + 1)
if last_neg != -1:
# 移除最后一个负数
if left <= last_neg - 1:
max_len = max(max_len, (last_neg - 1) - left + 1)
# 继续下一段
return max_len
测试示例
print(longestPositiveProductSubarray([1,2,3], 6)) # 输出 3
print(longestPositiveProductSubarray([2,-3,4,-1,0,5,2], 10)) # 输出 2
总结
这个问题结合了乘积符号和绝对值限制,需要巧妙处理滑动窗口中的正负性。核心是维护窗口内乘积绝对值不超过 limit,并在乘积为负时通过跳过奇数个负数来得到正乘积子数组。注意处理零值分割段,以及更新负数位置。