【动态规划】子数组系列一(数组中连续的一段)

子数组系列一(数组中连续的一段)

点赞 👍👍收藏 🌟🌟关注 💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.最大子数组和

题目链接: 53. 最大子数组和

题目分析:

子数组 是数组中的一个连续部分。子数组最少包含一个元素也就是说本身也是子数组!

算法原理:

1.状态表示

像这种研究的是子数组这样的模型,状态表示 依旧可以用 经验 + 题目要求

以 i 位置为结尾,巴拉巴拉。

以 i 位置为结尾,我们要的是最大子数组和,是不是先把以 i 位置为结尾的所有子数组拿到。单独 i 元素 是一个子数组,还有前面以 i 元素 为结尾的所有子数组,我要的是一个最大和。

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

2.状态转移方程

找出所有子数组最大和就可以了,所有子数组可以划分两大类。第一类就是单独自己构成子数组,第二类就是它自己与前面元素的结合构成子数组。所以可以根据长度来划分,长度为1 单独自己构成子数组,子数组最大和 nums[i],长度大于1,nums[i]一定是要的,然后在找到以 i - 1 位置元素为结尾的所有子数组中的最大和,两个相加就可以了,而dp[i - 1] 就是以 i - 1 位置元素为结尾的所有子数组中的最大和,因此 dp[i + 1] + nums[i],我要求的是 i 位置元素为结尾的所有子数组中的最大和。因此两种情况取最大就好了

3.初始化

多申请一个节点

  1. 里面的值要保证后面填表的正确
  2. 下标映射关系

先考虑如果不多加一个节点第一个位置应该填多少呢?是不是填自己本身啊,为了不让多加的节点影响后面的填表正确,因此可以给 0。注意下标映射关系,我们躲开了一个空间相当于整体往右移动一位,如果要回原数组下标要 -1

4.填表顺序

从左往右

5.返回值

注意这里可不是返回最后一个位置的值,因为最大子数组可能在这个数组中任何一个地方。所以返回的是dp中最大值

cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        // 1.创建 dp 表
        // 2.初始化
        // 3.填表
        // 4.返回值

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

        int ret = INT_MIN;
        for(int i = 1; i <= n; ++i)
            ret = max(ret,dp[i]);
        return ret;

    }
};

2.环形子数组的最大和

题目链接: 918. 环形子数组的最大和

题目分析:

给一个环形数组,让找子数组最大和。子数组可能是中间连续部分,包括自己本身也是子数组。子数组可能是绕一圈的。

算法原理:

如果直接就在环形上面做有很多边界问题需要考虑。前面我们做过一道题 " 打家劫舍II " 也是一个环形的,我们是将 " 打家劫舍II " 转换成 " 打家劫舍I ",也就是将一个环形数组转换成一个普通数组,然后在普通数组上用" 打家劫舍I "来做。

这里也是将环形数组转换成一个普通数组来解决问题。

分类讨论一:子数组没有跨过数组的边界,在数组的中间。

这个就和上面求最大子数组和一模一样。

分类讨论二:子数组跨过数组边界,在数组两端

那前面一部分和后面一部分求最大和,直接求是非常恶心的,但是可以看到空白的地方正好在数组内部,整段数组的和是一个定值 sum,如果阴影部分的和是最大的,那空白地方和是最小的!所以只要在整个数组找一个连续的区间使和最小,那剩下两部分拼起来和就是最大的。

因此这里就转化成两个问题,求子数组和最大值,求子数组和最小值。都没环形无关了。求子数组和最小值然后用sum减去这个最小值就是两端最大值。最后返回这两种情况的最大值就好了。

1.状态表示

经验 + 题目要求

以 i 位置为结尾,巴拉巴拉。

以 i 位置为结尾,我们要的是最大子数组和,是不是先把以 i 位置为结尾的所有子数组拿到。单独 i 元素 是一个子数组,还有前面以 i 元素 为结尾的所有子数组,我要的是一个最大和。

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

同理:

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

2.状态转移方程

我们可以把所有子数组分成两类,第一类是自己本身就是子数组,第二类就是自己加上前面一个或者两个等等构成的子数组。

3.初始化

注意到填表填 0 位置的时候会越界。我们这里可以多申请一个节点

  1. 里面的值要保证后面填表的正确
  2. 下标映射关系

先考虑如果不多加一个节点第一个位置应该填多少呢?是不是填自己本身啊,为了不让多加的节点影响后面的填表正确,因此可以给 0。f求最大和可以给0,g求最小和也可以给0。

注意下标映射关系,我们躲开了一个空间相当于整体往右移动一位,如果要回原数组下标要 -1

4.填表顺序

从左往右

5.返回值

回到分类讨论,我们是把问题分成两个子问题,一个是在数组不跨边界找最大子数组和 记为 fmax,一个在数组跨界也就是数组两端找子数组最小和 记为 gmin,然后sum - gmin 就是两端子数组最大和。然后比较一下。

但是如果数组全都是负数的,这样就有些问题。比如:

cpp 复制代码
class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        // 1. 创建 dp 表
        // 2.初始化
        // 3.填表
        // 4.返回值

        int n = nums.size();
        vector<int> f(n + 1);
        auto g = f;
        int sum = 0;
        int fmax = INT_MIN, gmin = INT_MAX;
        for(int i = 1; i <= n; ++i)
        {
            f[i] = max(nums[i - 1], f[i - 1] + nums[i - 1]);
            g[i] = min(nums[i - 1], g[i - 1] + nums[i - 1]);
            fmax = max(fmax, f[i]);
            gmin = min(gmin,g[i]);
            sum += nums[i -1];
        }
        return sum == gmin ? fmax : max(fmax, sum - gmin);
    }
};

3.乘积最大子数组

题目链接: 152. 乘积最大子数组

题目分析:

这道题和上面一样思想,不过求得是子数组中最大乘积

算法原理:

1.状态表示

经验 + 题目要求

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

现在我们先以这个状态表示分析状态转移方程。

i 本身也是子数组。还有以 i 位置为结尾的子数组,以 i 位置为结尾的子数组我们求最大,nums[i] 肯定要乘上,剩下的子数组都有一个特点就是以 i - 1结尾,所以我们可以将以i - 1位置为结束的子数组乘积最大拿到然后在乘nums[i],而dp[i-1]就是i - 1位置为结束的子数组乘积最大。

但是到这里先停住,我们发现了不对劲。i 位置 如果是 > 0的数,用dp[i-1]*nums[i]可以得到更大的数,但是如果 i 位置是 < 0 ,dp[i-1]*nums[i]就是一个很小的数。

因此我们的状态表示还不过,还要细分下去:


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

g[i] 表示:以 i 位置元素为结尾的所有子数组中最小乘积。

2.状态转移方程

有上面的基础,这里不在细说了

3.初始化

注意到填表填 0 位置的时候会越界。我们这里可以多申请一个节点

  1. 里面的值要保证后面填表的正确
  2. 下标映射关系

先不考虑虚拟节点填多少,先考虑不添加虚拟节点第一个位置填多少,是不是填自己本身啊。为了使填的值不影响后填表,可以给1,

下标映射,整体往右移动一位,回原本要往左移动一位

4.填表顺序

从左往右,两个表一起填

5.返回值

要的是最大乘积,f中放的是最大乘积,遍历一下f,找到最大乘积

cpp 复制代码
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值

        int n = nums.size();
        vector<double> f(n + 1);
        auto g = f;
        f[0] = g[0] = 1;
        double ret = INT_MIN;
        for(int i = 1; i <= n; ++i)
        {
            double x = nums[i - 1], y = f[i - 1] * nums[i - 1], z = g[i - 1] * nums[i - 1];
            f[i] = max(x, max(y,z));
            g[i] = min(x, min(y,z));
            ret = max(ret,f[i]);
        }
        return ret;
    }
};

4.乘积为正数的最长子数组长度

题目链接: 1567. 乘积为正数的最长子数组长度

题目分析:

求乘积为正数最大子数组长度。

算法原理:

1.状态表示

经验 + 题目要求

dp[i] 表示 :以 i 位置元素为结尾的所有子数组中乘积为正数的最大长度

还是先以这个分析

i 本身也是子数组,然后为空看nums[i] 是正还是负。还有以 i 位置为结尾的子数组,求所有子数组中乘积为正数的最大长度,nums[i]要分正还是负,剩下的子数组都有一个特点就是以 i - 1结尾,所以我们可以将以 i -1 位置元素为结尾的所有子数组中乘积为正数的最大长度拿到,然后在加上nums[i]为正为负的情况。

我们由上面的状态表示去分析,发现当子数组长度大于1,nums[i] < 0,分析不下去了,如果前面 i - 1 位置有乘积为负数最大长度,负数乘nums[i] > 0,乘积也大于0,那长度不也应该加+1嘛。

所以一个状态表示不够。

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

g[i] 表示:以 i 位置元素为结尾所有子数组中乘积为负数的最大长度

2.状态转移方程

当长度大于1,nums[i] < 0,所以我们要找到的是 i - 1位置元素为结尾所有子数组中乘积为负数的最大长度,但是要注意的是,万一 g[i - 1] 位置为0呢?也就是说前面根本就没有乘积为负数的最大长度。那nums[i] 又小于 0,乘积肯定不为正!

因此这里不能这样写,需要判断一下:

再来分析一下g

长度大于1,nums[i] > 0,所以我们要找到的是 i - 1位置元素为结尾所有子数组中乘积为负数的最大长度,但是万一 g[i - 1] 是 0呢?乘积不可能为负,而nums[i] > 0,这种情况以 i 位置元素为结尾所有子数组中乘积为负数的最大长度就是0,因此下面写的就不对。

所以最终状态转移方程如下:

有人可能会有疑问,为什么f 当长度大于1,nums[i] > 0,不去考虑 f[i -1] 为0的情况呢,其实我们已经考虑过了,f[i -1] 为0就为0好了,反正nums[i] > 0至少有1个。

同样g 当长度大于1,nums[i] < 0, 不去考虑 f[i -1]为0的情况,都是一样的,f[i -1] 为0就为0好了,反正nums[i] < 0至少有1个。如果大于0 就加上好了。

下面可以整体一下状态转移方程

f[i],可以把 nums[i] > 0 两种情况合并成 f[i - 1] + 1,因为nums[i] > 0 至少保证有一个了,如果f[i - 1] == 0 最终结果可以是两个1中任何一个,如果 f[i - 1] > 0,最大值是由f[i - 1] + 1来决定,所以不用考虑上面单独1了。

nums[i] < 0 两种情况合并成 g[i -1] == 0 ?:g[i -1] + 1,要么是0,要么是比0更大的数,最大值由g[i -1] == 0 ?:g[i -1]决定

同理g也是这样合并

3.初始化

注意到填表填 0 位置的时候会越界。我们这里可以多申请一个节点

  1. 里面的值要保证后面填表的正确
  2. 下标映射关系

考虑没有填表的时候g第一个位置填什么,nums[0] > 0 填0,nums[0] < 0填1。

代入是不是f[i - 1] 和 g[i -1] 的位置都填0啊,所以虚拟节点填0。

4.填表顺序

从左往右,两个表一起填

5.返回值

f表中最大值

cpp 复制代码
class Solution {
public:
    int getMaxLen(vector<int>& nums) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值

        int n = nums.size();
        vector<int> f(n + 1), g(n + 1);
        int ret = INT_MIN;
        for(int i = 1; i <= n; ++i)
        {
            //不要考虑nums[i - 1] == 0的情况,因为初始化为0 
            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;
            }
            ret = max(ret, f[i]);
        }
        return ret;
    }
};
相关推荐
刚学HTML27 分钟前
leetcode 05 回文字符串
算法·leetcode
AC使者1 小时前
#B1630. 数字走向4
算法
冠位观测者1 小时前
【Leetcode 每日一题】2545. 根据第 K 场考试的分数排序
数据结构·算法·leetcode
古希腊掌管学习的神2 小时前
[搜广推]王树森推荐系统笔记——曝光过滤 & Bloom Filter
算法·推荐算法
qystca2 小时前
洛谷 P1706 全排列问题 C语言
算法
浊酒南街2 小时前
决策树(理论知识1)
算法·决策树·机器学习
就爱学编程2 小时前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
学术头条2 小时前
清华、智谱团队:探索 RLHF 的 scaling laws
人工智能·深度学习·算法·机器学习·语言模型·计算语言学
Schwertlilien2 小时前
图像处理-Ch4-频率域处理
算法
IT猿手3 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解TP1-TP10及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·深度学习·算法·机器学习·matlab·多目标算法