[动态规划#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;
    }
};
相关推荐
青少儿编程课堂8 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:折半搜索优化子集枚举(Meet in the Middle)解析
c++·python·算法·bfs·信息学竞赛
Oneshot11 小时前
DLX(Dancing Links)算法详解:求解数独最快的算法
算法
青少儿编程课堂11 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会Scratch考级:校园连连看解析
c++·python·算法·bfs·信息学竞赛
kongdong11 小时前
Hikyuu | RSI均值回归交易策略实现
算法·均值算法·回归·量化交易·交易策略·hikyuu
小王C语言12 小时前
优选算法:分治-归并排序
算法
非优秀程序员1 天前
第05章 播放器大战,暴风、快播与那一夜|《互联网产品恩仇录》
算法
武子康1 天前
驾驶轨迹换个坐标,就能给移动机器人用吗?
人工智能·算法·机器学习
lisw051 天前
Dayhoff的PAM替换矩阵理论评介!
线性代数·算法·矩阵
一起学安全技术1 天前
拆大模型 03|注意力的一句话版本:不用论文讲清 Attention
深度学习·算法·语言模型·transformer