C++ 子数组系列dp:最大子数组和、环形子数组的最大和、乘积最大子数组、乘积为正数的最长子数组长度

目录

最大子数组和:

环形子数组的最大和:

乘积最大子数组:

乘积为正数的最长子数组长度:


最大子数组和:

题目链接:53. 最大子数组和 - 力扣(LeetCode)

题目要求:

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

题目解析:

1、状态表示:

根据经验 + 题目要求可推出,该题为以某一个位置为结尾,然后xxx的题目。

设置dp[i],表示以 i 位置元素为结尾的所有子数组中的最大和

2、状态转移方程:

按照上图,可得dp[i]的方程分为两种情况,以dp[i]的长度来划分:

dp[i]的长度等于1:就是nums[i]了

dp[i]的长度大于1:那就要挑选出以i元素位置为结尾的所有子数组中最大的这个值,这个值就存在于dp[i - 1]中,然后再加上nums[i]。

然后我们只取其中的最大值所以方程为:dp[i] = max( nums[i],dp[i -1] + nums[i] );

3、初始化:

        因为dp有i - 1的情况,所以为了保证不越界,要初始化dp[0]这个位置,或者用加一个虚拟节点的办法,因为这题比较简单,所以就直接初始化dp[0]这个位置了,根据状态转移方程的第一种情况可直接初始化: dp[0] = nums[0]

4、填表顺序:从左往右

5、返回值:返回dp表中最大的那个值即可(因为最大值不一定是以dp[i]结尾的)

题解代码:

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

        vector<int> dp(n);
        dp[0] = nums[0];

        for(int i = 1; i < n; i++)
        {
            dp[i] = max(nums[i], dp[i - 1] + nums[i]);
        }

        int _max = INT_MIN;
        for(int i = 0; i < n; i++)
        {
            _max = max(_max, dp[i]);
        }
        return _max;
    }
};

环形子数组的最大和:

题目链接:918. 环形子数组的最大和 - 力扣(LeetCode)

题目要求:

给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。

 

题目解析:

像这种环形数组的题我们要想办法看看能不能转换成线性数组,或者直接以线性数组的角度来看:

1、状态表示:  

根据经验 + 题目要求可推出,该题为以某一个位置为结尾,然后xxx的题目。

设置dp[i],表示以 i 位置元素为结尾的所有子数组中的最大和,或者最小和。所以又细分为两个子状态:

 f[i] 表示:以 i 为结尾的所有子数组中的最大和

g[i] 表示:以 i 为结尾的所有子数组中的最小和

2、状态转移方程:

先来看f[i],求最大子数组和:f[i]的方程分为两种情况,以f[i]的长度来划分:

f[i]的长度等于1:就是nums[i]了

f[i]的长度大于1:那就要挑选出以i元素位置为结尾的所有子数组中最大的这个值,这个值就存在于f[i - 1]中,然后再加上nums[i]。

然后我们只取其中的最大值所以 f[i] 的方程为:f[i] = max( nums[i],f[i -1] + nums[i] );

再来看g[i],求最小子数组和:和 f[i] 一样,但是求的是最小,也分为两种情况:

g[i]的长度等于1:就是nums[i]了

g[i]的长度大于1:那就要挑选出以i元素位置为结尾的所有子数组中最小的这个值,这个值就存在于g[i - 1]中,然后再加上nums[i]。

然后我们只取其中的最小值所以 g[i] 的方程为:f[i] = min( nums[i],g[i -1] + nums[i] );

3、初始化:

             因为 g表和 f表有i - 1的情况,所以为了保证不越界,要初始化 g[0] 和 f[0] 这两个位置,或者用加一个虚拟节点的办法,这里就直接用初始化的方法了,根据状态转移方程的第一种情况可直接初始化: f[0] = nums[0],g[0] = nums[0]   

4、填表顺序:从左往右两个表同时往后填。

5、返回值:

        因为有两个表,f表和g表,所以返回值有点特殊,首先是找出f表中的最大值,然后再找g表中的最小值,因为g表中存储的是最小子数组和,由原数组的总和(sum)减去这个最小子数组和就能得到最大子数组和,所以拿到g表的最小值之后再由sum减去这个最小值,然后和f表的最大值作比较,返回两个数中大的这个值。

还有一种特殊情况:就是如果数组中都是负数的情况。

如果数组中都是负数的话,f表是没有问题的,但是g表就会出现问题,g表中最小值是数组所有元素的总和,也就是sum,到最后sum - 最小值,得出来的是0,就不符合题目要求了,因为题目要求必须选一个数,但是0的话就是什么都不选,这时候就要返回f表的最大值。

也就是到最后要进行一层判断,如果sum等于g表中的最小值,那就证明这整个数组都是负数,此时就要返回f表的最大值。

题解代码:

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        int n = nums.size();
        if(n == 1) return nums[0];

        vector<int> f(n);
        auto g = f;
        f[0] = nums[0];
        g[0] = nums[0];

        int sum = nums[0], fmax = -0x3f3f3f3f, gmin = 0x3f3f3f3f;
        for(int i = 1; i < n; i++)
        {
            f[i] = max(nums[i], f[i - 1] + nums[i]);
            fmax = max(fmax, f[i]);
            g[i] = min(nums[i], g[i - 1] + nums[i]);
            gmin = min(gmin, g[i]);
            sum += nums[i];
        }
        return sum == gmin ? fmax : max(fmax, (sum - gmin));
    }
};

乘积最大子数组:

题目链接:152. 乘积最大子数组 - 力扣(LeetCode)

题目要求:

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

题目解析:

1、状态表示:

根据经验 + 题目要求可推出,该题为以某一个位置为结尾,然后xxx的题目。

设置f[i],表示以 i 位置元素为结尾的所有子数组中的最大乘积:

然后还要设置一个g[i],表示以 i 位置结尾的所有子数组最小乘积(为什么要设置g[i],在状态转移方程处才能讲明白)        

2、状态转移方程:

        以i位置为结尾又可以继续划分方程,一类是长度等于1的,也就是nums[i],一类是长度大于1的,也就是上图下面的三条线,然后长度大于1的还要再划分两个子方程,就是nums[i]是正数还是负数,如果是正数那么就是乘f[i - 1]位置,因为f表中存的是最大的乘积数。如果nums[i]是负数的话,那就不能乘最大的数了,得要乘最小乘积数,也就是g[i - 1],g表同理,所以状态转移方程如下:

f[i] = max( nums[i],f[i - 1] * nums[i],g[i - 1] * nums[i] );

g[i] = min( nums[i],f[i - 1] * nums[i],g[i - 1] * nums[i] );

3、初始化:

        因为 g表和 f表有i - 1的情况,所以为了保证不越界,要初始化 g[0] 和 f[0] 这两个位置,或者用加一个虚拟节点的办法,这里就直接用初始化的方法了,根据状态转移方程的第一种情况可直接初始化: f[0] = nums[0],g[0] = nums[0]   

4、填表顺序:从左往右两个表同时往后填。

5、返回值:返回f表中的最大值

题解代码:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n);
        auto g = f;
        f[0] = nums[0]; g[0] = nums[0];

        for(int i = 1; i < n; i++)
        {
            f[i] = max(nums[i], max(f[i - 1] * nums[i], g[i - 1] * nums[i]));
            g[i] = min(nums[i], min(f[i - 1] * nums[i], g[i - 1] * nums[i]));
        }

        int _max = INT_MIN;
        for(int i = 0; i < n; i++)
        {
            _max = max(_max, f[i]);
        }

        return _max;
    }
};

乘积为正数的最长子数组长度:

题目链接:1567. 乘积为正数的最长子数组长度 - 力扣(LeetCode)

题目要求:

给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。

一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。

请你返回乘积为正数的最长子数组长度。

题目解析:

1、状态表示:

根据经验 + 题目要求可推出,该题为以某一个位置为结尾,然后xxx的题目。

设置f[i],表示以 i 位置元素为结尾的所有子数组乘积为正数最长长度。

然后还要设置一个g[i],表示以 i 位置结尾的所有子数组乘积为负数最长长度(为什么要设置g[i],在状态转移方程处才能讲明白)

2、状态转移方程:

来看状态分析图:

根据状态图合并完重复情况后得到状态转移方程为:

 f表:

当nums[i] > 0:f[i] = f[i - 1] + 1

当nums[i] < 0:f[i] = ( g[i - 1] == 0 ? 0 : g[i - 1] + 1 ) 

g表:

当nums[i] > 0:g[i] = ( g[i - 1] == 0 ? 0 : g[i - 1] + 1 ) 

当nums[i] < 0:g[i] = f[i - 1] + 1

3、初始化:

        因为会涉及到i - 1的位置,所以要初始化0这个节点,这里可以直接使用加虚拟节点的方法,在前面增加一个虚拟节点,然后根据状态转移方程可确定将两个虚拟节点都初始化成0。

4、填表顺序:从左到右两个表同时填写。

5、返回值:返回f表中的最大值即可。(无需考虑g表,因为g表存放的是负数)

题解代码:

class Solution {
public:
    int getMaxLen(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n + 1, 0);
        auto g = f;

        int _max = INT_MIN;
        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;
            }
            _max = max(_max, f[i]);
        }

        return _max;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值