【算法训练-动态规划 二】【线性DP问题】连续子数组的最大和、乘积最大子数组、最长递增子序列

简介: 【算法训练-动态规划 二】【线性DP问题】连续子数组的最大和、乘积最大子数组、最长递增子序列

废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【动态规划】,使用【数组】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为:目标公司+最近一年+出现频率排序,由高到低的去牛客TOP101去找,只有两个地方都出现过才做这道题(CodeTop本身汇聚了LeetCode的来源),确保刷的题都是高频要面试考的题。

连续子数组的最大和【MID】

来从动态规划最简单的题开始训练

题干

题干中可以提炼的关键信息:连续的子数组,整数有正有负,求连续子数组的最大和,题目要我们找出和最大的连续子数组的值是多少,「连续」是关键字,连续很重要,不是子序列。题目只要求返回结果,不要求得到最大的连续子数组是哪一个。这样的问题通常可以使用「动态规划」解决

解题思路

按照动态规划的思路进行状态设计和状态转移方程编写

1 定义状态(定义子问题)

dp[i]:表示以 nums[i] 结尾的连续子数组的最大和。说明:「结尾」和「连续」是关键字。

2 状态转移方程(描述子问题之间的联系)

根据状态的定义,由于 nums[i] 一定会被选取,并且以 nums[i] 结尾的连续子数组与以 nums[i - 1] 结尾的连续子数组只相差一个元素 nums[i] 。假设数组 nums 的值全都严格大于 0,那么一定有 dp[i] = dp[i - 1] + nums[i]。可是 dp[i - 1] 有可能是负数,于是分类讨论:

  • 如果 dp[i - 1] > 0,那么可以把 nums[i] 直接接在 dp[i - 1] 表示的那个数组的后面,得到和更大的连续子数组;
  • 如果 dp[i - 1] <= 0,那么 nums[i] 加上前面的数 dp[i - 1] 以后值不会变大。于是 dp[i] 「另起炉灶」,此时单独的一个 nums[i] 的值,就是 dp[i]。

以上两种情况的最大值就是 dp[i] 的值,写出如下状态转移方程:

3 初始化状态

dp[0] 根据定义,只有 1 个数,一定以 nums[0] 结尾,因此 dp[0] = nums[0]

4 求解方向

这里采用自底向上,从最小的状态开始求解

5 找到最终解

这里的dp[i]只是以i为结尾的连续子数组的最大和,并不是题目中的问题,数组的连续子数组最大和,所以最终解并不是子问题的解,需要用一个MAX值承载,通过与dp[i]比较更新最终解

代码实现

给出代码实现基本档案

基本数据结构数组

辅助数据结构

算法动态规划

技巧

其中数据结构、算法和技巧分别来自:

  • 10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树
  • 10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
  • 技巧:双指针、滑动窗口、中心扩散

当然包括但不限于以上

import java.util.*;
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param array int整型一维数组
     * @return int整型
     */
    public int FindGreatestSumOfSubArray (int[] array) {
        // 1 定义dp数组,初始化状态,初始化最终解
        int[] dp = new int[array.length];
        dp[0] = array[0];
        int greatSum = dp[0];
        // 2 定义状态转移方程
        for (int i = 1; i < array.length; i++) {
            // 状态转移方程,求解当前的dp[i]
            dp[i] = dp[i - 1] <= 0 ? array[i] : dp[i - 1] + array[i];
            // 更新最大值
            greatSum = Math.max(greatSum, dp[i]);
        }
        return greatSum;
    }
}

复杂度分析

时间复杂度:遍历了一遍数组,所以时间复杂度为O(N)

空间复杂度:定义了动规数组,空间复杂度为O(N)

乘积最大子数组【MID】

处理一个合连续子数组的最大和类似的题目,乘积最大的子数组

题干

解题思路

原题解地址,数组的动态规划问题、子序列、连续子序列的一个常见的状态定义是:

以下标 i 结尾的连续子序列的乘积的最大值。

最后把整个 dp 数组看一遍求最大值即可。因此状态转移方程可能是:

dp[i] = max(dp[i - 1] * nums[i], nums[i])

说明:牢记状态的定义,一定以下标 i 结尾,即:乘积数组中 nums[i] 必须被选取,但是nums[i]可能是正数也可能是负数

  • 如果nums[i] 是负数,要想获得最大乘积,则dp[i-1]越小则dp[i]越大,于是我们可能就需要记录一下**dp[i-1]**的那个最小数。
  • 如果nums[i] 是正数,则取上一个状态的最大值*

所以要维护两个动态规划数组

1 定义状态(定义子问题)

maxDp[i]:表示以 nums[i] 结尾的连续子数组的最大乘积。说明:「结尾」和「连续」是关键字。

minDp[i]: 表示以 nums[i] 结尾的连续子数组的最小乘积。说明:「结尾」和「连续」是关键字。

2 状态转移方程(描述子问题之间的联系)

if (nums[i] >= 0) {
      // 如果当前值为正数,则最大值为 Math.max(nums[i], maxDp[i - 1] * nums[i]),因为maxDp[i-1]可能为负数,这样不如舍弃重新开始
       maxDp[i] = Math.max(nums[i], maxDp[i - 1] * nums[i]);
      // 同时需要更新最小值,nums[i]为正值,所以就用最小乘积,minDp[i - 1] * nums[i]更小
      minDp[i] = Math.min(nums[i], minDp[i - 1] * nums[i]);
} else {
    // 如果当前值为负数,则最大值为Math.max(nums[i], minDp[i - 1] * nums[i]),minDp[i - 1]可能为负数,这样不如使用minDp[i - 1] * nums[i],
    maxDp[i] = Math.max(nums[i], minDp[i - 1] * nums[i]);
    // 同时需要更新最小值,nums[i]为负值,所以就用最大乘积,maxDp[i - 1] * nums[i]更小
    minDp[i] = Math.min(nums[i], maxDp[i - 1] * nums[i]);
 }

3 初始化状态

// base case 初始状态为数组的第一个元素
maxDp[0] = nums[0];
minDp[0] = nums[0];
int ans =  nums[0];

4 求解方向

这里采用自底向上,从最小的状态开始求解

5 找到最终解

最终解采用实时更新的方式

// 更新整体最大值
ans = Math.max(ans, maxDp[i]);

代码实现

给出代码实现基本档案

基本数据结构数组

辅助数据结构

算法动态规划

技巧

其中数据结构、算法和技巧分别来自:

  • 10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树
  • 10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
  • 技巧:双指针、滑动窗口、中心扩散

当然包括但不限于以上

import java.util.*;
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param number int整型
     * @return int整型
     */
    public int maxProduct(int[] nums) {
        // 1 入参合法性判断
        if (nums == null || nums.length == 0) {
            return 0;
        }
        // 2 定义两个动态规划数组,分别记录以i为结尾的最大乘积子数组,并维护最大乘积值
        int[] maxDp = new int[nums.length];
        int[] minDp = new int[nums.length];
        // 3 base case 初始状态为数组的第一个元素
        maxDp[0] = nums[0];
        minDp[0] = nums[0];
        int ans =  nums[0];
        // 4 状态转移方程
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] >= 0) {
                // 如果当前值为正数,则最大值为 Math.max(nums[i], maxDp[i - 1] * nums[i]),因为maxDp[i-1]可能为负数,这样不如舍弃重新开始
                maxDp[i] = Math.max(nums[i], maxDp[i - 1] * nums[i]);
                // 同时需要更新最小值,nums[i]为正值,所以就用最小乘积,minDp[i - 1] * nums[i]更小
                minDp[i] = Math.min(nums[i], minDp[i - 1] * nums[i]);
            } else {
                // 如果当前值为负数,则最大值为Math.max(nums[i], minDp[i - 1] * nums[i]),minDp[i - 1]可能为负数,这样不如使用minDp[i - 1] * nums[i],
                maxDp[i] = Math.max(nums[i], minDp[i - 1] * nums[i]);
                // 同时需要更新最小值,nums[i]为负值,所以就用最大乘积,maxDp[i - 1] * nums[i]更小
                minDp[i] = Math.min(nums[i], maxDp[i - 1] * nums[i]);
            }
            // 更新整体最大值
            ans = Math.max(ans, maxDp[i]);
        }
        return ans;
    }
}

复杂度分析

时间复杂度:遍历了一遍数组,所以时间复杂度为O(N)

空间复杂度:定义了动规数组,空间复杂度为O(N)

最长递增子序列【MID】

终于又来到一道看了很久的高频题目这里

题干

注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的

解题思路

按照动态规划的思路进行状态设计和状态转移方程编写,这里用数学归纳法进行状态转移公式的推导。

数学归纳法:比如我们想证明一个数学结论,那么我们先假设这个结论在 k < n 时成立,然后根据这个假设,想办法推导证明出 k = n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立

我们设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 dp[0...i-1] 都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]

1 定义状态(定义子问题)

我们的定义是这样的:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度,这个定义中 nums[i] 必须被选取,且必须是这个子序列的最后一个元素;

2 状态转移方程(描述子问题之间的联系)

算法演进过程中每个 dp[i] 的结果是我们肉眼看出来的,我们应该怎么设计算法逻辑来正确计算每个 dp[i] 呢?这就是动态规划的重头戏,如何设计算法逻辑进行状态转移,才能正确运行呢?这里需要使用数学归纳的思想:

假设我们已经知道了 dp[0…4] 的所有结果,我们如何通过这些已知结果推出 dp[5] 呢

根据刚才我们对 dp 数组的定义,现在想求 dp[5] 的值,也就是想求以 nums[5] 为结尾的最长递增子序列

nums[5] = 3既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一

  • nums[5] 前面有哪些元素小于 nums[5]?这个好算,用 for 循环比较一波就能把这些元素找出来。
  • 以这些元素为结尾的最长递增子序列的长度是多少?回顾一下我们对 dp 数组的定义,它记录的正是以每个元素为末尾的最长递增子序列的长度

以我们举的例子来说,nums[0]nums[4] 都是小于 nums[5] 的,然后对比 dp[0] 和 dp[4] 的值,我们让 nums[5] 和更长的递增子序列结合,得出 dp[5] = 3

for (int j = 0; j < i; j++) {
    if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
    }
}

3 初始化状态

根据这个定义,我们就可以推出 base case:dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列起码要包含它自己

4 求解方向

这里采用自底向上,从最小的状态开始求解

5 找到最终解

根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值

int res = 0;
for (int i = 0; i < dp.length; i++) {
    res = Math.max(res, dp[i]);
}
return res;

代码实现

给出代码实现基本档案

基本数据结构数组

辅助数据结构

算法动态规划

技巧

其中数据结构、算法和技巧分别来自:

  • 10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树
  • 10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
  • 技巧:双指针、滑动窗口、中心扩散

当然包括但不限于以上

import java.util.*;
class Solution {
    // 最长上升子序列
    public int lengthOfLIS(int[] nums) {
        // 1 入参校验判断
        if (nums.length < 1) {
            return -1;
        }
        // 2 定义DP数组:dp[i]表示以nums[i]为结尾的上升子序列的最大长度
        int[] dp = new int[nums.length];
        // 3 base case, 所有元素都至少包含自己,且dp[0]=1 因为只有一个元素,二者合并
        Arrays.fill(dp, 1);
        // 4 状态转移,填充状态转移表
        for (int i = 1; i < nums.length; i++) {
            // dp[i]为i之前的小于nums[i]的所有dp[i]的最大值组成
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        // 5 最终解为dp数组的最大值
        int result = 0;
        for (int i = 0; i < dp.length; i++) {
            result = Math.max(result, dp[i]);
        }
        return result;
    }
}

当然最值的更新也可以在双重循环中同步进行

import java.util.*;
class Solution {
    // 最长上升子序列
    public int lengthOfLIS(int[] nums) {
        // 1 入参校验判断
        if (nums.length < 1) {
            return -1;
        }
        // 2 定义DP数组:dp[i]表示以nums[i]为结尾的上升子序列的最大长度
        int[] dp = new int[nums.length];
        // 3 base case, 所有元素都至少包含自己,且dp[0]=1 因为只有一个元素,二者合并
        Arrays.fill(dp, 1);
        // 4 状态转移,填充状态转移表
        int result = 1;
        for (int i = 1; i < nums.length; i++) {
            // dp[i]为i之前的小于nums[i]的所有dp[i]的最大值组成
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            result = Math.max(result, dp[i]);
        }
        return result;
    }
}

复杂度分析

时间复杂度:O(N^2),这里 N 是数组的长度,我们写了两个 for 循环,每个 for 循环的时间复杂度都是线性的;

空间复杂度:O(N),要使用和输入数组长度相等的状态数组,因此空间复杂度是 O(N)。

相关文章
|
20天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
36 2
|
2月前
|
算法
动态规划算法学习三:0-1背包问题
这篇文章是关于0-1背包问题的动态规划算法详解,包括问题描述、解决步骤、最优子结构性质、状态表示和递推方程、算法设计与分析、计算最优值、算法实现以及对算法缺点的思考。
69 2
动态规划算法学习三:0-1背包问题
|
28天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
2月前
|
算法
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
这篇文章介绍了动态规划算法中解决最大上升子序列问题(LIS)的方法,包括问题的描述、动态规划的步骤、状态表示、递推方程、计算最优值以及优化方法,如非动态规划的二分法。
71 0
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
|
2月前
|
算法
动态规划算法学习二:最长公共子序列
这篇文章介绍了如何使用动态规划算法解决最长公共子序列(LCS)问题,包括问题描述、最优子结构性质、状态表示、状态递归方程、计算最优值的方法,以及具体的代码实现。
145 0
动态规划算法学习二:最长公共子序列
|
2月前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
13天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
|
21天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
22天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
23天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。