「动态规划」如何求乘积为正数的最长子数组长度?

1567. 乘积为正数的最长子数组长度icon-default.png?t=N7T8https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/description/

给你一个整数数组nums,请你求出乘积为正数的最长子数组的长度。一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。请你返回乘积为正数的最长子数组长度。

  1. 输入:nums = [1,-2,-3,4],输出:4,解释:数组本身乘积就是正数,值为24。
  2. 输入:nums = [0,1,-2,-3,-4],输出:3,解释:最长乘积为正数的子数组为[1,-2,-3],乘积为6。注意,我们不能把0也包括到子数组中,因为这样乘积为0,不是正数。
  3. 输入:nums = [-1,-2,-3,0,1],输出:2,解释:乘积为正数的最长子数组是[-1,-2]或者[-2,-3]。

提示:1 <= nums.length <= 10^5,-10^9 <= nums[i] <= 10^9。


我们用动态规划的思想来解决这个问题。

确定状态表示:根据经验和题目要求,我们的第一反应是,用dp[i]表示:以i位置为结尾的所有子数组中,乘积为正数的最长子数组长度。然而这个问题有些耐人寻味,乘积为正数,有可能是负负得正呀!比如nums[i]是负数,那么就需要乘一个负数,才能得到正数。

所以,我们需要分类讨论:

  • 用f[i]表示:以i位置为结尾的所有子数组中,乘积为正数的最长子数组长度。
  • 用g[i]表示:以i位置为结尾的所有子数组中,乘积为负数的最长子数组长度。

推导状态转移方程:考虑f[i],如果以i位置为结尾的子数组的乘积为正数,那么:

  • 如果nums[i]是正数:
    • 如果子数组的长度为1,即子数组只包含nums[i]本身,乘积是正数,符合条件。
    • 如果子数组的长度大于1,那么以i位置为结尾的子数组中,除去nums[i]之外的其余元素的乘积是正数。而除去nums[i]之外的其余元素构成了以i - 1位置为结尾的子数组。所以,以i位置为结尾的子数组的长度,等于以i - 1位置为结尾的子数组的长度加1,并且以i - 1位置为结尾的子数组的乘积是正数,所以此时的总长度是f[i - 1] + 1。注意:如果f[i - 1] = 0,说明以i - 1位置为结尾的子数组的乘积一定不是正数,此时f[i - 1] + 1 = 1,相当于子数组中只有nums[i],符合条件。
  • 如果nums[i]是负数:
    • 如果子数组的长度为1,即子数组只包含nums[i]本身,乘积是负数,不符合条件。
    • 如果子数组的长度大于1,那么以i位置为结尾的子数组中,除去nums[i]之外的其余元素的乘积是负数。而除去nums[i]之外的其余元素构成了以i - 1位置为结尾的子数组。所以,以i位置为结尾的子数组的长度,等于以i - 1位置为结尾的子数组的长度加1,并且以i - 1位置为结尾的子数组的乘积是负数,所以此时的总长度是g[i - 1] + 1。注意:如果g[i - 1] = 0,说明以i - 1位置为结尾的子数组的乘积一定不是负数,此时g[i - 1] + 1 = 1,相当于子数组中只有nums[i],然而nums[i]是负数,乘积是负数,不符合条件,也就是说,这种情况下无法构成乘积是正数的子数组,乘积是正数的最长子数组的长度是0。
  • 如果nums[i] = 0,那么无法构成乘积是正数的子数组,乘积是正数的最长子数组的长度是0。

综上所述:如果nums[i]是正数,那么f[i] = max(1, f[i - 1] + 1),又因为f[i - 1] >= 0,所以f[i] = f[i - 1] + 1。如果nums[i]是负数,要想以i位置为结尾的子数组的乘积是正数,子数组的长度不能等于1,只能大于1,若g[i - 1] = 0,那么无法构成乘积是正数的子数组,f[i] = 0;若g[i - 1] != 0,那么f[i] = g[i - 1] + 1。如果num[i] = 0,那么f[i] = 0。

考虑g[i],如果以i位置为结尾的子数组的乘积为负数,那么:

  • 如果nums[i]是负数:
    • 如果子数组的长度为1,即子数组只包含nums[i]本身,乘积是负数,符合条件。
    • 如果子数组的长度大于1,那么以i位置为结尾的子数组中,除去nums[i]之外的其余元素的乘积是正数。而除去nums[i]之外的其余元素构成了以i - 1位置为结尾的子数组。所以,以i位置为结尾的子数组的长度,等于以i - 1位置为结尾的子数组的长度加1,并且以i - 1位置为结尾的子数组的乘积是正数,所以此时的总长度是f[i - 1] + 1。注意:如果f[i - 1] = 0,说明以i - 1位置为结尾的子数组的乘积一定不是正数,此时f[i - 1] + 1 = 1,相当于子数组中只有nums[i],符合条件。
  • 如果nums[i]是正数:
    • 如果子数组的长度为1,即子数组只包含nums[i]本身,乘积是正数,不符合条件。
    • 如果子数组的长度大于1,那么以i位置为结尾的子数组中,除去nums[i]之外的其余元素的乘积是负数。而除去nums[i]之外的其余元素构成了以i - 1位置为结尾的子数组。所以,以i位置为结尾的子数组的长度,等于以i - 1位置为结尾的子数组的长度加1,并且以i - 1位置为结尾的子数组的乘积是负数,所以此时的总长度是g[i - 1] + 1。注意:如果g[i - 1] = 0,说明以i - 1位置为结尾的子数组的乘积一定不是负数,此时g[i - 1] + 1 = 1,相当于子数组中只有nums[i],然而nums[i]是正数,乘积是正数,不符合条件,也就是说,这种情况下无法构成乘积是正数的子数组,乘积是负数的最长子数组的长度是0。
  • 如果nums[i] = 0,那么无法构成乘积是负数的子数组,乘积是负数的最长子数组的长度是0。

综上所述:如果nums[i]是负数,那么g[i] = max(1, f[i - 1] + 1),又因为f[i - 1] >= 0,所以g[i] = f[i - 1] + 1。如果nums[i]是正数,要想以i位置为结尾的子数组的乘积是负数,子数组的长度不能等于1,只能大于1,若g[i - 1] = 0,那么无法构成乘积是负数的子数组,g[i] = 0;若g[i - 1] != 0,那么g[i] = g[i - 1] + 1。如果num[i] = 0,那么g[i] = 0。

再总结一下:如果nums[i]是正数,那么f[i] = f[i - 1] + 1,g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;如果nums[i]是负数,那么f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1,g[i] = f[i - 1] + 1;如果nums[i] = 0,那么f[i] = g[i] = 0。

初始化:根据状态转移方程,我们需要初始化f[0]和g[0]的值,防止越界。我们可以在f和g的最前面加上一个辅助结点。想象一下,如果在nums的最前面加上一个0,并不会影响结果,因为0不会出现在乘积为正数或负数的子数组中。由于nums的最前面加上一个0,根据状态表示,f[0]和g[0]就应该初始化为0。也就是说,辅助结点应该初始化为0

填表顺序:根据状态转移方程,f[i]和g[i]依赖于f[i - 1]和g[i - 1],所以应从左往右同时填f表和g表

返回值:根据状态表示和题目要求,由于我们并不确定子数组的结束位置,所以要返回f表中除了辅助结点之外的最大值

细节问题:由于添加了一个辅助结点,所以f表和g表比nums多了一个元素,若nums有n个元素,f表和g表的规模就是1 x (n + 1),且f表和g表的i位置对应nums[i - 1]。

class Solution {
public:
    int getMaxLen(vector<int>& nums) {
        int n = nums.size();

        // 创建dp表
        vector<int> f(n + 1);
        auto g = f;

        // 填表
        for (int i = 1; i <= n; i++) {
            if (nums[i - 1] > 0) {
                f[i] = f[i - 1] + 1;
                g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;
            } else if (nums[i - 1] < 0) {
                f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;
                g[i] = f[i - 1] + 1;
            }
        }

        // 返回结果
        return *max_element(f.begin() + 1, f.end());
    }
};
  • 44
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力学习游泳的鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值