《算法题讲解指南:动态规划算法--子数组系列》--21.乘积最大子数组,22.乘积为正数的最长子数组

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

21.乘积最大子数组

题目链接:

题目描述:

题目示例:

解法(动态规划):

算法思路:

C++算法代码:

算法总结及流程解析:

22.乘积为正数的最长子数组

题目链接:

题目描述:

题目示例:

解法(动态规划):

算法思路:

C++算法代码:

算法总结及流程解析:

结束语


21.乘积最大子数组

题目链接:

152. 乘积最大子数组 - 力扣(LeetCode)

题目描述:

题目示例:

解法(动态规划):

算法思路:

这道题与「最大子数组和」非常相似,我们可以效仿着定义一下状态表示以及状态转移:

i.dpi表示以i为结尾的所有子数组的最大乘积,

ii. dpi = max(numsi, dpi - 1 * numsi);

由于正负号的存在,我们很容易就可以得到,这样求dpi的值是不正确的。因为dpi-1的信息并不能让我们得到dpi的正确值。比如数组-2,5,-2,用上述状态转移得到的dp数组为-2,5,-2,最大乘积为5 。但是实际上的最大乘积应该是所有数相乘,结果为20。

究其原因,就是因为我们在求dp2的时候,因为nums2是一个负数,因此我们需要的是「i -1 位置结尾的最小的乘积(-10)」,这样一个负数乘以「最小值」,才会得到真实的最大值。

因此,我们不仅需要一个「乘积最大值的dp表」,还需要一个「乘积最小值的dp表」。

1.状态表示:

fi表示:以i结尾的所有子数组的最大乘积,

gi表示:以i结尾的所有子数组的最小乘积。

2.状态转移方程:

遍历每一个位置的时候,我们要同步更新两个dp 数组的值。

对于fi,也就是「以为结尾的所有子数组的最大乘积」,对于所有子数组,可以分为下面三种形式:

i.子数组的长度为1,也就是numsi;

ii.子数组的长度大于1 ,但numsi>0,此时需要的是i-1 为结尾的所有子数组的最大乘积fi-1,再乘上numsi,也就是numsi* fi- 1;

iii.子数组的长度大于1,但numsi<0,此时需要的是i-1为结尾的所有子数组的最小乘积gi-1,再乘上numsi,也就是numsi* gi-1;

(如果numsi=0,所有子数组的乘积均为0,三种情况其实都包含了)

综上所述,fi = max(numsi, max(numsi* fi - 1, numsi * gi - 1) ).

对于gi,也就是「以i为结尾的所有子数组的最小乘积」,对于所有子数组,可以分为下面三种形式:

i.子数组的长度为1,也就是numsi;

ii.子数组的长度大于1,但numsi>0,此时需要的是i-1为结尾的所有子数组的最小乘积gi-1,再乘上numsi,也就是numsi* gi-1;

iii.子数组的长度大于1 ,但numsi<0 ,此时需要的是i1 为结尾的所有子数组的最大乘积fi-1,再乘上 numsi,也就是 numsi * fi-1;

综上所述, gi = min(numsi, min(numsi * fi -1, numsi * gi - 1))

(如果numsi=0,所有子数组的乘积均为0,三种情况其实都包含了)

3.初始化:

可以在最前面加上一个辅助结点,帮助我们初始化。使用这种技巧要注意两个点:

i.辅助结点里面的值要保证后续填表是正确的;

ii.下标的映射关系。

在本题中,最前面加上一个格子,并且让f0=g0=1即可。

4.填表顺序:

根据状态转移方程易得,填表顺序为「从左往右,两个表一起填」。

5.返回值:

返回 f 表中的最大值。

C++算法代码:

cpp 复制代码
class Solution {
public:
    int maxProduct(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> max_dp(n);
        vector<int> min_dp(n);   
        max_dp[0] = min_dp[0] = nums[0];

        for(int i = 1; i < n; i++)
        {
            if(nums[i] >= 0)
            {
                max_dp[i] = max(max_dp[i - 1] * nums[i], nums[i]);
                min_dp[i] = min(min_dp[i - 1] * nums[i], nums[i]);
            }
            else
            {
                max_dp[i] = max(min_dp[i - 1] * nums[i], nums[i]);
                min_dp[i] = min(max_dp[i - 1] * nums[i], nums[i]);
            }
        }
        int ret = INT_MIN;
        for(int i = 0; i < n; i++)
        {
            ret = max(ret, max_dp[i]);
        }
        return ret;
    }
};

算法总结及流程解析:

22.乘积为正数的最长子数组

题目链接:

1567. 乘积为正数的最长子数组长度 - 力扣(LeetCode)

题目描述:

题目示例:

解法(动态规划):

算法思路:

继续效仿「最大子数组和」中的状态表示,尝试解决这个问题。

状态表示:dpi表示「所有以i 结尾的子数组,乘积为正数的最长子数组的长度」。

思考状态转移:对于i位置上的numsi,我们可以分三种情况讨论:

i.如果numsi=0,那么所有以i为结尾的子数组的乘积都不可能是正数,此时dpi=0;

ii.如果numsi>0 ,那么直接找到dpi-1的值(这里请再读一遍dpi-1代表的意义,并且考虑如果dpi-1的结值是0 的话,影不影响结果),然后加1即可,此时dpi=dpi-1+1;

iii.如果numsi<0,这时候你该蛋疼了,因为在现有的条件下,你根本没办法得到此时的最长长度。因为乘法是存在「负负得正」的,单单靠一个dpi-1,我们无法推导出dpi的值。

但是,如果我们知道「以i-1为结尾的所有子数组,乘积为负数的最长子数组的长度」negi-1,那么此时的dpi是不是就等于negi-1+1呢?

通过上面的分析,我们可以得出,需要两个dp表,才能推导出最终的结果。不仅需要一个「乘积为正数的最长子数组」,还需要一个「乘积为负数的最长子数组」。

1.状态表示:

fi表示:以i结尾的所有子数组中,乘积为「正数」的最长子数组的长度;

gi表示:以i结尾的所有子数组中,乘积为「负数」的最长子数组的长度。

2.状态转移方程:

遍历每一个位置的时候,我们要同步更新两个dp数组的值。

对于fi,也就是以i为结尾的乘积为「正数」的最长子数组,根据numsi的值,可以分为三种情况:

i.numsi=0时,所有以i为结尾的子数组的乘积都不可能是正数,此时fi=0;

ii. numsi > 0时,那么直接找到fi - 1的值(这里请再读一遍fi-1代表的意义,并且考虑如果fi-1的结值是0的话,影不影响结果),然后加一即可,此时 fi= fi- 1 + 1;

iii. numsi<0时,此时我们要看gi-1 的值(这里请再读一遍 gi-1代表的意义。因为负负得正,如果我们知道以i-1为结尾的乘积为负数的最长子数组的长度,加上1即可),根据gi-1的值,又要分两种情况:

  1. gi-1=0 ,说明以i-1 为结尾的乘积为负数的最长子数组是不存在的,又因为numsi<0,所以以i 结尾的乘积为正数的最长子数组也是不存在的,此时fi=0;

  2. gi - 1 !=0,说明以i-1为结尾的乘积为负数的最长子数组是存在的,又因为numsi< 0,所以以i 结尾的乘积为正数的最长子数组就等于gi-1+1;

综上所述,numsi< 0时,fi =gi-1 == 0 ? 0 : g\[i-1 + 1;

对于gi,也就是以i为结尾的乘积为「负数」的最长子数组,根据numsi的值,可以分为三种情况:

i.numsi=时,所有以i为结尾的子数组的乘积都不可能是负数,此时gi=0;

ii.numsi < 0时,那么直接找到fi-1的值(这里请再读一遍fi-1代表的意义,并且考虑如果fi-1的结值是的话,影不影响结果),然后加一即可(因为正数*负数=负数),此时 gi = fi -1+ 1 ;

iii.numsi>0 时,此时我们要看gi-1的值(这里请再读一遍 gi -1代表的意义。因为正数*负数=负数),根据gi-1的值,又要分两种情况:

1.gi- 1= ,说明以i- 1 为结尾的乘积为负数的最长子数组是不存在的,又因为numsi>0,所以以i结尾的乘积为负数的最长子数组也是不存在的,此时fi=0;

  1. gi-1 != 0 ,说明以i-1 为结尾的乘积为负数的最长子数组是存在的,又

因为numsi>0,所以以i 结尾的乘积为正数的最长子数组就等于gi-1 + 1;

综上所述, numsi > 0 时, gi = gi -1 == 0 ? 0 : g\[i - 1 + 1 ;

这里的推导比较绕,因为不断的出现「正数和负数」的分情况讨论,我们只需根据下面的规则,严格找到此状态下需要的dp 数组即可:

i.正数* 正数=正数

ii.负数*负数=正数

iii.负数*正数=正数*负数=负数

3.初始化:

可以在最前面加上一个「辅助结点」,帮助我们初始化。使用这种技巧要注意两个点:

i.辅助结点里面的值要「保证后续填表是正确的」;

ii. 「下标的映射关系」。

在本题中,最前面加上一个格子,并且让f0= g0= 0即可。

4.填表顺序:

根据「状态转移方程」易得,填表顺序为「从左往右,两个表一起填」。

5.返回值:

根据「状态表示」,我们要返回f 表中的最大值。

C++算法代码:

cpp 复制代码
class Solution {
public:
    int getMaxLen(vector<int>& nums) 
    {
        int n = nums.size();
        vector<int> pos_dp(n);
        vector<int> neg_dp(n);    
        if(nums[0] > 0)
        {
            pos_dp[0] = 1;
            neg_dp[0] = 0;
        }
        else if(nums[0] < 0)
        {
            pos_dp[0] = 0;
            neg_dp[0] = 1;
        }
        else
        {
            pos_dp[0] = neg_dp[0] = 0;
        }

        for(int i = 1; i < n; i++)
        {
            if(nums[i] > 0)
            {
                pos_dp[i] = pos_dp[i - 1] + 1;
                neg_dp[i] = neg_dp[i - 1] == 0 ? 0 : neg_dp[i - 1] + 1;
            }
            else if(nums[i] < 0)
            {
                pos_dp[i] = neg_dp[i - 1] == 0 ? 0 : neg_dp[i - 1] + 1;
                neg_dp[i] = pos_dp[i - 1] + 1;
            }
            else
            {
                pos_dp[i] = neg_dp[i] = 0;
            }
        }
        int ret = INT_MIN;
        for(int i = 0; i < n; i++)
        {
            ret = max(ret, pos_dp[i]);
        }
        return ret;
    }
};

算法总结及流程解析:

结束语

到此,21.乘积最大子数组,22.乘积为正数的最长子数组 这两道算法题就讲解完了。**对于乘积最大子数组问题:采用双DP数组分别记录以i结尾的最大和最小乘积,通过比较当前元素、与前驱最大乘积或最小乘积的组合来递推求解;对于乘积为正数的最长子数组问题:同样使用双DP数组分别记录以i结尾的正数和负数乘积最长子数组长度,根据当前元素正负分情况讨论状态转移。**希望大家能有所收获!

相关推荐
qq 13740186112 分钟前
医用无菌屏障系统加速老化标准解读:ASTM F1980-2016 全解析
人工智能·算法·加速老化·包装测试·astm·医疗器械包装·无菌屏障系统
wayz113 分钟前
Overlap:SLOPE(线性回归斜率)技术指标详解
算法·金融·数据分析·回归·线性回归·量化交易·特征工程
珊瑚里的鱼3 分钟前
C++14 和 C++17 的核心新特性
开发语言·c++
点云兔子4 分钟前
舱口检测:从点云到矩形定位的射线投影算法
opencv·算法·点云·舱口检测
小欣加油6 分钟前
leetcode169 多数元素
数据结构·c++·算法·leetcode·职场和发展
wayz1111 分钟前
Momentum:RVGI(相对活力指数)技术指标详解
算法·金融·数据分析·量化交易·特征工程
zxw61028 分钟前
UFOMap代码Debug
c++
fpcc28 分钟前
工具使用—CMake文件中的常见变量
c++
Promise微笑30 分钟前
洞察无形:红外热像仪行业标准解析与深度选型指南
网络·人工智能·算法
liu-yonggang32 分钟前
MISRA C++:2008 — Guidelines for the use of the C++ language in critical systems
c++