[小白也能懂的算法知识]动态规划03-线性动态规划

207 阅读7分钟

线性 DP 知识(一)

1. 线性动态规划简介

线性动态规划:具有「线性」阶段划分的动态规划方法统称为线性动态规划(简称为「线性 DP」),如下图所示。

线性 DP

如果状态包含多个维度,但是每个维度上都是线性划分的阶段,也属于线性 DP。比如背包问题、区间 DP、数位 DP 等都属于线性 DP。

线性 DP 问题的划分方法有多种方式。

  • 如果按照「状态的维度数」进行分类,我们可以将线性 DP 问题分为:一维线性 DP 问题、二维线性 DP 问题,以及多维线性 DP 问题。
  • 如果按照「问题的输入格式」进行分类,我们可以将线性 DP 问题分为:单串线性 DP 问题、双串线性 DP 问题、矩阵线性 DP 问题,以及无串线性 DP 问题。

本文中,我们将按照问题的输入格式进行分类,对线性 DP 问题中各种类型问题进行一一讲解。

2. 单串线性 DP 问题

单串线性 DP 问题:问题的输入为单个数组或单个字符串的线性 DP 问题。状态一般可定义为 dp[i]dp[i],表示为:

  1. 「以数组中第 i 个位置元素 nums[i]nums[i] 为结尾的子数组(nums[0]...nums[i]nums[0]...nums[i])」的相关解。
  2. 「以数组中第 i−1 个位置元素 nums[i1]nums[i−1] 为结尾的子数组(nums[0]...nums[i1]nums[0]...nums[i−1])」的相关解。
  3. 「以数组中前 i 个元素为子数组(nums[0]...nums[i1]nums[0]...nums[i−1])」的相关解。

这 3 种状态的定义区别在于相差一个元素 nums[i]nums[i]

  1. 第 1 种状态:子数组的长度为 i+1i+1,子数组长度不可为空;
  2. 第 2 种状态、第 3 种状态:这两种状态描述是相同的。子数组的长度为 ii,子数组长度可为空。在 i=0i=0 时,方便用于表示空数组(以数组中前 0 个元素为子数组)。

2.1 最长递增子序列

单串线性 DP 问题中最经典的问题就是「最长递增子序列(Longest Increasing Subsequence,简称 LIS)」。

2.1.1 题目链接
2.1.2 题目大意

描述:给定一个整数数组 nums。

要求:找到其中最长严格递增子序列的长度。

说明

  • 子序列:由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7][3,6,2,7] 是数组 [0,3,1,6,2,2,7][0,3,1,6,2,2,7] 的子序列。
  • 1nums.length25001≤nums.length≤2500
  • 104nums[i]104−104≤nums[i]≤104

示例

  • 示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4。
  • 示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
2.1.3 解题思路
思路 1:动态规划
1. 划分阶段

按照子序列的结尾位置进行阶段划分。

2. 定义状态

定义状态 dp[i]dp[i] 表示为:以 nums[i]nums[i] 结尾的最长递增子序列长度。

3. 状态转移方程

一个较小的数后边如果出现一个较大的数,则会形成一个更长的递增子序列。

对于满足 0j<i0≤j<i 的数组元素 nums[j]nums[j] 和 nums[i]nums[i] 来说:

  • 如果 nums[j]<nums[i]nums[j]<nums[i],则 nums[i]nums[i] 可以接在 nums[j]nums[j] 后面,此时以 nums[i]nums[i] 结尾的最长递增子序列长度会在「以 nums[j]nums[j] 结尾的最长递增子序列长度」的基础上加 1,即:dp[i]=dp[j]+1dp[i]=dp[j]+1
  • 如果 nums[j]nums[i]nums[j]≤nums[i],则 nums[i]nums[i] 不可以接在 nums[j]nums[j] 后面,可以直接跳过。

综上,我们的状态转移方程为:dp[i]=max(dp[i],dp[j]+1),0j<i,nums[j]<nums[i]dp[i]=max(dp[i],dp[j]+1),0≤j<i,nums[j]<nums[i]

4. 初始条件

默认状态下,把数组中的每个元素都作为长度为 1 的递增子序列。即 dp[i]=1dp[i]=1

5. 最终结果

根据我们之前定义的状态,dp[i]dp[i] 表示为:以 nums[i]nums[i] 结尾的最长递增子序列长度。那为了计算出最大的最长递增子序列长度,则需要再遍历一遍 dpdp 数组,求出最大值即为最终结果。

思路 1:动态规划代码

python

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        size = len(nums)
        dp = [1 for _ in range(size)]

        for i in range(size):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        
        return max(dp)
class Solution {
    public int lengthOfLIS(int[] nums) {
        // 初始化一下子序列长度
        int[] lengthTable = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            lengthTable[i] = 1;
        }
        // 计算子序列长度
        int maxLength = 1;
        for (int i = 1; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                // 可以拼在子序列后面
                if (nums[i] > nums[j]) {
                    // 需要从所有的子序列长度中取最大值,不然可能出现长度小的覆盖长度大的
                    lengthTable[i] = Math.max(lengthTable[i], lengthTable[j] + 1);
                }
            }
            maxLength = Math.max(maxLength, lengthTable[i]);
        }

        return maxLength;
    }
}
思路 1:复杂度分析]
  • 时间复杂度O(n2)O(n^2)。两重循环遍历的时间复杂度是 O(n2)O(n^2),最后求最大值的时间复杂度是 O(n)O(n),所以总体时间复杂度为 O(n2)O(n^2)
  • 空间复杂度O(n)O(n)。用到了一维数组保存状态,所以总体空间复杂度为 O(n)O(n)

2.2 最大子数组和

单串线性 DP 问题中除了子序列相关的线性 DP 问题,还有子数组相关的线性 DP 问题。

注意

  • 子序列:由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。
  • 子数组:指的是数组中的一个连续子序列。

「子序列」与「子数组」都可以看做是原数组的一部分,而且都不会改变原来数组中元素的相对顺序。其区别在于数组元素是否要求连续。

2.2.1 题目链接
2.2.2 题目大意

描述:给定一个整数数组 numsnums

要求:找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

说明

  • 子数组:指的是数组中的一个连续部分。
  • 1nums.length1051≤nums.length≤105
  • 104nums[i]104−104≤nums[i]≤104

示例

  • 示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。
  • 示例 2:
输入:nums = [1]
输出:1
2.2.3 解题思路
思路 1:动态规划
1. 划分阶段

按照连续子数组的结束位置进行阶段划分。

2. 定义状态

定义状态 dp[i]dp[i] 为:以第 i 个数结尾的连续子数组的最大和。

3. 状态转移方程

状态 dp[i]dp[i] 为:以第 i 个数结尾的连续子数组的最大和。则我们可以从「第 i−1 个数结尾的连续子数组的最大和」,以及「第 i 个数的值」来讨论 dp[i]dp[i]

  • 如果 dp[i1]<0dp[i−1]<0,则「第 i−1 个数结尾的连续子数组的最大和」+「第 i 个数的值」<「第 i 个数的值」,即:dp[i1]+nums[i]<nums[i]dp[i−1]+nums[i]<nums[i]。所以,此时 dp[i]dp[i] 应取「第 i 个数的值」,即 dp[i]=nums[i]dp[i]=nums[i]
  • 如果 dp[i1]0dp[i−1]≥0,则「第 i−1 个数结尾的连续子数组的最大和」 +「第 i 个数的值」 >= 第 i 个数的值,即:dp[i1]+nums[i]nums[i]dp[i−1]+nums[i]≥nums[i]。所以,此时 dp[i]dp[i] 应取「第 i−1 个数结尾的连续子数组的最大和」+「 第 i 个数的值」,即 dp[i]=dp[i1]+nums[i]dp[i]=dp[i−1]+nums[i]

归纳一下,状态转移方程为:

dp[i]={nums[i],dp[i1]<0dp[i1]+nums[i],dp[i1]>=0dp[i]=\begin{cases} nums[i],\,\,dp[i-1]<0\\ dp[i-1]+nums[i],\,\,dp[i-1]>=0\\ \end{cases}

4. 初始条件
  • 第 0 个数结尾的连续子数组的最大和为 nums[0]nums[0],即 dp[0]=nums[0]dp[0]=nums[0]
5. 最终结果

根据状态定义,dp[i]dp[i] 为:以第 i 个数结尾的连续子数组的最大和。则最终结果应为所有 dp[i]dp[i] 的最大值,即 max(dp)max(dp)

思路 1:代码

python

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        size = len(nums)
        dp = [0 for _ in range(size)]

        dp[0] = nums[0]
        for i in range(1, size):
            if dp[i - 1] < 0:
                dp[i] = nums[i]
            else:
                dp[i] = dp[i - 1] + nums[i]
        return max(dp)

java

class Solution {
    public int maxSubArray(int[] nums) {

        int[] dp = new int[nums.length];
        dp[0] = nums[0];

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

        return max;
    }
}
思路 1:复杂度分析
  • 时间复杂度O(n)O(n),其中 n 为数组 nums 的元素个数。
  • 空间复杂度O(n)O(n)
思路 2:动态规划 + 滚动优化

因为 dp[i]dp[i] 只和 dp[i1]dp[i−1] 和当前元素 nums[i]nums[i] 相关,我们也可以使用一个变量 subMax 来表示以第 i 个数结尾的连续子数组的最大和。然后使用 ansMax 来保存全局中最大值。

思路 2:代码
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        size = len(nums)
        subMax = nums[0]
        ansMax = nums[0]

        for i in range(1, size):
            if subMax < 0:
                subMax = nums[i]
            else:
                subMax += nums[i]
            ansMax = max(ansMax, subMax)
        return ansMax
思路 2:复杂度分析
  • 时间复杂度O(n)O(n),其中 n 为数组 nums 的元素个数。
  • 空间复杂度O(1)O(1)

转载自:algo.itcharge.cn/10.Dynamic-…

系列技术文章:java面试(持续更新中)