[动态规划#1] 子数组 | 最大子数组和 | 环形子数组的最大和 | 乘积最大子数组 | 乘积为正数的最长子数组

1. 专题介绍

这一系列算法题都属于子数组专题:求数组中连续的一段。这是限制,也是抓手------通过对以该位置为结尾 / 开头设下限制,逐个推导。

Q:为什么能用?

A:

  • 按 i 结尾分类,n 个位置刚好对应 n 类,互不重叠、合起来是全集
  • 以 i 结尾的子数组,往前只有一种延伸方式(把 i-1 结尾的某段接上),所以 dp[i] 只依赖 dp[i-1]
  • 只依赖前一项 + 一维数组,仅需遍历一次,复杂度O(N)

重点!解题流程:

  1. 状态表示:以 i 结尾的所有子数组 + 题目所求
  2. 状态转移:dpi 只有两种来源 ------ 自己 / 接上一项 dpi-1
  3. 初始化:不越界 + 保证后续填表的正确性
  4. 填表顺序:不确定,通常从左往右
  5. 返回值:不确定,通常要返回整张表的最值

创建多少张表,根据经验 + 题目要求确定。

2. 最大子数组和

53. 最大子数组和

为什么用动态规划:

最直接的做法是枚举左右端点,O(N²) 个区间挨个求和。问题在于这些区间大量重合:以 i-1 结尾的区间和,在算以 i 结尾时又被从头加了一遍。重复计算就是 dp 要消掉的东西。

换个枚举维度:不枚举"哪一段",而是枚举"结尾在哪一段"。答案一定以某个位置结尾,所以把以 i 结尾的最大和全部求出来取最大值即可。

而以i结尾的区间只有两种:只含 nums[i] 自己,或者由以 i-1 结尾的某段加一个 nums[i] 拼成。后者要挑最大的那段,那就是 dp[i-1]。于是:dp[i] = max(nums[i], dp[i-1] + nums[i])

HOW:

  1. 状态表示:dp[i] 表示以 i 位置结尾的所有子数组中的最大和
  2. 状态转移:dp[i] = max(nums[i], dp[i-1] + nums[i])
  3. 初始化:最前面加一个辅助格子,dp[0] = 0;下标映射为 nums[i-1]
  4. 填表顺序:从左往右
  5. 返回值:整张 dp 表的最大值,不是 dp[n]

复杂度 :时 O(N),空 O(N)

注意点:下表映射,分类讨论,状态转移方程推导,状态表示

cpp 复制代码
class Solution {
public:
    int getMaxLen(vector<int>& nums)
    {
        //创建dp表+初始化
        int n = nums.size();
        vector<int> f(n + 1, 0);
        vector<int> g(n + 1, 0);

        //填表
        int ret = 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;
            }

            ret = max(ret, f[i]);
        }

        //返回值
        return ret;
    }
};

2. 环形子数组的最大和

918. 环形子数组的最大和

  1. 状态表示:f[i] 表示以 i 结尾的所有子数组中的最大和,g[i] 表示最小和
  2. 状态转移:f[i] = max(x, x + f[i-1]),g[i] = min(x, x + g[i-1])
  3. 初始化:f[0] = g[0] = 0
  4. 填表顺序:从左往右,两张表一起填
  5. 返回值:sum == gmin ? fmax : max(fmax, sum - gmin)

注意点:

  • 开两张表:将环形数组分类讨论,转化为普通数组的题型(正+正难则反)
  • 对状态转移分类讨论:(gi同理)
  • 多画图!子数组问题都可以转化为该元素 / 该元素之前的段两种状态,分类即可:
  • 返回值的特殊性:数组全是负数,gmin == sum,此时sum - gmin == 0。表示没有元素被正向选入,需要特殊判断

复杂度 :时 O(N),空 O(N)。

cpp 复制代码
class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums)
    {
        int n = nums.size();

        vector<int> f(n + 1, 0);
        vector<int> g(n + 1, 0);    
        int sum = 0;
        int fmax = INT_MIN;
        int gmin = INT_MAX;

        for(int i=1; i<=n; ++i)
        {
            sum += nums[i - 1];

            f[i] = max(f[i-1] + nums[i-1], nums[i - 1]);
            g[i] = min(g[i-1] + nums[i-1], nums[i-1]);
              
            fmax = max(f[i], fmax);
            gmin = min(g[i], gmin);
        }

        return sum == gmin ? fmax : max(fmax, sum - gmin);
    }
};

3. 乘积最大子数组

152. 乘积最大子数组

  1. 状态表示:f[i] 表示以 i 结尾的所有子数组的最大乘积,g[i] 表示最小乘积

  2. 状态转移:(gi同理)

  3. 初始化:f[0] = g[0] = 1,开n+1大小的数组

  4. 填表顺序:从左往右,两张表一起填

  5. 返回值:f 表的最大值

复杂度 :时 O(N),空 O(N)

注意点:

  • 开两个数组的必要性:乘法存在正负,仅凭一张表视野有限(无法通过上一个位置推出该位置的状态),必须另外开一张表同步负数的视野
  • 初始化为1而不是0
cpp 复制代码
class Solution {
public:
    int maxProduct(vector<int>& nums)
    {
        //1.创建dp数组+初始化
        int n = nums.size();
        vector<int> f(n + 1, 1);
        vector<int> g(n + 1, 1);

        //2.填表
        int ret = INT_MIN;
        for(int i=1; i<=n; ++i)
        {
            f[i] = max({f[i-1]*nums[i-1], nums[i-1], g[i-1]*nums[i-1]});
            g[i] = min({g[i-1]*nums[i-1], nums[i-1], f[i-1]*nums[i-1]});

            ret = max(ret, f[i]);
        }

        //3.返回
        return ret;
    }
};

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

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

  1. 状态表示:f[i] 表示以 i 结尾、乘积为正的最长子数组长度,g[i] 表示乘积为负的最长长度
  2. 状态转移:按 nums[i] 的符号分类讨论:
  3. 初始化:f[0] = g[0] = 0(存储长度)
  4. 填表顺序:从左往右,两张表一起填
  5. 返回值:f 表的最大值

复杂度 :时 O(N),空 O(N)。

注意点:

  • 表中存储的是长度,注意区分
  • x == 0 不需要在dp表中处理
  • 状态转移方程中三元运算符的处理原因
  • 分类讨论
cpp 复制代码
class Solution {
public:
    int getMaxLen(vector<int>& nums)
    {
        //创建dp表+初始化
        int n = nums.size();
        vector<int> f(n + 1, 0);
        vector<int> g(n + 1, 0);

        //填表
        int ret = 0;
        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;
            }

            ret = max(ret, f[i]);
        }

        //返回值
        return ret;
    }
};
相关推荐
凯瑟琳.奥古斯特1 小时前
动态调优IoU提升OCR文本合并精度
开发语言·python·算法
布莱克6052 小时前
A* 算法详解:从原理到实现
开发语言·c++·算法
段一凡-华北理工大学2 小时前
高炉炼铁机器视觉与智能识别十八讲~系列文章15:可靠性篇:防误报、容错与识别系统的鲁棒性设计
人工智能·算法·机器学习·高炉大模型·高炉机器视觉·高炉炼铁模型开发
某林2122 小时前
局部路径规划器 DWB:速度采样与评分权重的取值分析
人工智能·算法·架构·机器人·技术复盘
AI职业加油站2 小时前
模数共振行动解读|大模型应用工程师证书,匹配行业 RAG/Agent 落地人才需求
人工智能·学习·算法·职场和发展·职场发展
lengxuenong3 小时前
树状数组 · 信息学学习手册2-离散化与逆序对
c++·学习·算法
琳达爱分享3 小时前
五个线程同时去换 token,四个被踢下线:刷新接口其实不是幂等的
后端·算法·架构
十二3 小时前
【洛谷题解】P1115 最大子段和(前缀和 + 维护最小前缀)
数据结构·c++·算法·蓝桥杯
renhongxia14 小时前
RAG检索增强生成:打通大模型与外部知识的通道
大数据·人工智能·算法