2023-05-04 线性DP_力扣练习

Posted 空無一悟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2023-05-04 线性DP_力扣练习相关的知识,希望对你有一定的参考价值。

线性DP的力扣题目练习

这一章将会介绍线性动态规划的相关概念和经典问题,并给出一些练习题供大家演练。

用动态规划解决问题的过程有以下几个关键点:状态定义,状态的转移,初始化和边界条件。

状态定义 就是定义子问题,如何表示目标规模的问题和更小规模的问题。例如常见的方法:定义状态 dp[n],表示规模为 nn 的问题的解,dp[n - 1] 就表示规模为 n - 1n−1 的子问题的解。在实战中 dp[n] 的具体含义需要首先整理清楚再往下做。

状态转移 就是子问题之间的关系,例如定义好状态 dp[n],此时子问题是 dp[n-1] 等,并且大规模的问题的解依赖小规模问题的解,此时需要知道怎样通过小规模问题的解推出大规模问题的解。这一步就是列状态转移方程的过程。一般的状态转移方程可以写成如下形式

dp[n] = f(dp[i]) 其中 i < n

按照状态定义和状态转移的常见形式,可以对动态规划进行分类,可以参考上一章的内容。

其中线性动态规划的主要特点是状态的推导是按照问题规模 i 从小到大依次推过去的,较大规模的问题的解依赖较小规模的问题的解。

这里问题规模为 i 的含义是考虑前 i 个元素 [0..i] 时问题的解。

线性动态规划简介

线性动态规划的主要特点是状态的推导是按照问题规模 i 从小到大依次推过去的,较大规模的问题的解依赖较小规模的问题的解。

这里问题规模为 i 的含义是考虑前 i 个元素 [0..i] 时问题的解。

状态定义:

dp[n] := [0..n] 上问题的解

状态转移:

dp[n] = f(dp[n-1], ..., dp[0])

从以上状态定义和状态转移可以看出,大规模问题的状态只与较小规模的问题有关,而问题规模完全用一个变量 i 表示,i 的大小表示了问题规模的大小,因此从小到大推 i 直至推到 n,就得到了大规模问题的解,这就是线性动态规划的过程。

按照问题的输入格式,线性动态规划解决的问题主要是单串,双串,矩阵上的问题,因为在单串,双串,矩阵上问题规模可以完全用位置表示,并且位置的大小就是问题规模的大小。因此从前往后推位置就相当于从小到大推问题规模。

线性动态规划是动态规划中最基本的一类。问题的形式、dp 状态和方程的设计、以及与其它算法的结合上面变化很多。按照 dp 方程中各个维度的含义,可以大致总结出几个主流的问题类型,见后面的小节。除此之外还有很多没有总结进来的变种问题,小众问题,和困难问题,这些问题的解法更多地需要结合自己的做题经验去积累,除此之外,常见的,主流的问题和解法都可以总结成下面的四个小类别。

  • 一、单串
  • 二、带维度单串
  • 三、双串
  • 四、矩阵

下面按照这四个小专题刷题

一、单串

单串 dp[i] 线性动态规划最简单的一类问题,输入是一个串,状态一般定义为 dp[i] := 考虑[0..i]上,原问题的解,其中 i 位置的处理,根据不同的问题,主要有两种方式:

  • 第一种是 i 位置必须取,此时状态可以进一步描述为 dp[i] := 考虑[0..i]上,且取 i,原问题的解
  • 第二种是 i 位置可以取可以不取

大部分的问题,对 i 位置的处理是第一种方式,例如力扣:

  • 70 爬楼梯问题
  • 801 使序列递增的最小交换次数
  • 790 多米诺和托米诺平铺
  • 746 使用最小花费爬楼梯

线性动态规划中单串 dp[i] 的问题,状态的推导方向以及推导公式如下

1. 依赖比 i 小的 O(1) 个子问题

dp[n] 只与常数个小规模子问题有关,状态的推导过程 dp[i] = f(dp[i - 1], dp[i - 2], ...)。时间复杂度 O(n)O(n),空间复杂度 O(n) 可以优化为 O(1),例如上面提到的 70, 801, 790, 746 都属于这类。

如图所示,虽然紫色部分的 dp[i-1], dp[i-2], ..., dp[0] 均已经计算过,但计算橙色的当前状态时,仅用到 dp[i-1],这属于比 i 小的 O(1) 个子问题。

例如,当 f(dp[i-1], ...) = dp[i-1] + nums[i] 时,当前状态 dp[i] 仅与 dp[i-1] 有关。这个例子是一种数据结构前缀和的状态计算方式,关于前缀和的详细内容请参考下一章。

2. 依赖比 i 小的 O(n) 个子问题

dp[n] 与此前的更小规模的所有子问题 dp[n - 1], dp[n - 2], ..., dp[1] 都可能有关系。

状态推导过程如下:

dp[i] = f(dp[i - 1], dp[i - 2], ..., dp[0])

依然如图所示,计算橙色的当前状态 dp[i] 时,紫色的此前计算过的状态 dp[i-1], ..., dp[0] 均有可能用到,在计算 dp[i] 时需要将它们遍历一遍完成计算。

其中 f 常见的有 max/min,可能还会对 i-1,i-2,...,0 有一些筛选条件,但推导 dp[n] 时依然是 O(n) 级的子问题数量。

例如:

  • 139 单词拆分
  • 818 赛车

以 min 函数为例,这种形式的问题的代码常见写法如下

for i = 1, ..., n
    for j = 1, ..., i-1
        dp[i] = min(dp[i], f(dp[j])

时间复杂度 \\(O(n^2)\\),空间复杂度 O(n)

单串 dp[i] 经典问题

以下内容将涉及到的知识点对应的典型问题进行讲解,题目和解法具有代表性,可以从一个问题推广到一类问题。

1. 依赖比 i 小的 O(1) 个子问题

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

一个数组有很多个子数组,求哪个子数组的和最大。可以按照子数组的最后一个元素来分子问题,确定子问题后设计状态

dp[i] := [0..i] 中,以 nums[i] 结尾的最大子数组和

状态的推导是按照 i 从 0 到 n - 1 按顺序推的,推到 dp[i],时,dp[i - 1], ..., dp[0] 已经计算完。因为子数组是连续的,所以子问题 dp[i] 其实只与子问题 dp[i - 1] 有关。如果 [0..i-1] 上以 nums[i-1] 结尾的最大子数组和(缓存在 dp[i-1] )为非负数,则以 nums[i] 结尾的最大子数组和就在 dp[i-1] 的基础上加上 nums[i] 就是 dp[i] 的结果否则以 i 结尾的子数组就不要 i-1 及之前的数,因为选了的话子数组的和只会更小。

按照以上的分析,状态的转移可以写出来,如下

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

这个是单串 dp[i] 的问题,状态的推导方向,以及推导公式如下

在本题中,f(dp[i-1], ..., dp[0]) 即为 max(dp[i-1], 0) + nums[i]dp[i] 仅与 dp[i-1] 1 个子问题有关。因此虽然紫色部分的子问题已经计算完,但是推导当前的橙色状态时,只需要 dp[i-1] 这一个历史状态。

class Solution 
public:
    int maxSubArray(vector<int>& nums) 
        int n = nums.size(); // 数组的长度
        int dp[n]; // dp[i]表示以nums[i]结尾的最大和连续子数组的和
        memset(dp, -1e9, sizeof(dp)); // 数组初始化为最小值
        dp[0] = nums[0]; // 只有一个元素时,最大子数组显然是nums[0]自己
        int res = dp[0];
        for (int i = 1; i < n; i++) 
            if (dp[i - 1] < 0 ) 
                dp[i] = nums[i];
             else 
                dp[i] = dp[i - 1] + nums[i]; // 因为是连续子数组,所以只和前面一个元素有关
            
            res = max(dp[i], res); // 更新最大值
        
        return res;
    
;

2. 依赖比 i 小的 O(n) 个子问题

给定一个无序的整数数组,找到其中最长上升子序列的长度。

输入是一个单串,首先思考单串问题中设计状态 dp[i] 时拆分子问题的方式:枚举子串或子序列的结尾元素来拆分子问题,设计状态 dp[i] := 在子数组 [0..i] 上,且选了 nums[i] 时,的最长上升子序列。

因为子序列需要上升,因此以 i 结尾的子序列中,nums[i] 之前的数字一定要比 nums[i] 小才行,因此目标就是先找到以此前比 nums[i] 小的各个元素,然后每个所选元素对应一个以它们结尾的最长子序列,从这些子序列中选择最长的,其长度加 1 就是当前的问题的结果。如果此前没有比 nums[i] 小的数字,则当前问题的结果就是 1 。

按照以上的分析,状态的转移方程可以写出来,如下

\\[dp[i] = max_j(dp[j]) + 1 \\]

其中 \\(0 \\leq j < i, nums[j] < nums[i]\\)

本题依然是单串 dp[i] 的问题,状态的推导方向,以及推导公式与上一题的图示相同,

状态的推导依然是按照 i 从 0 到 n-1 推的,计算 dp[i] 时,dp[i-1], dp[i-2], ..., dp[0] 依然已经计算完。

但本题与上一题的区别是推导 dp[i] 时,dp[i-1]. dp[i-2], ..., dp[0] 均可能需要用上,即,因此计算当前的橙色状态时,紫色部分此前计算过的状态都可能需要用上。

单串相关练习题

    1. 最经典单串 LIS 系列
    1. 最大子数组和系列
    1. 打家劫舍系列
    1. 变形:需要两个位置的情况
    1. 与其它算法配合
    1. 其它单串 dp[i] 问题
    1. 带维度单串 dp[i][k]
    1. 股票系列

力扣 62. 不同路径 [线性DP]

题目

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28
示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向下
    示例 3:

输入:m = 7, n = 3
输出:28
示例 4:

输入:m = 3, n = 3
输出:6

解释与代码

上课没办法太集中写,再刷了一题力扣,线性DP

DP就是有多个的状态转移,也就是每一个dp状态与前面的dp状态有关,不知道大家写过爬楼梯没有,爬楼梯就是dp[n] = dp[n-1] + dp[n-2],每一次踩楼梯无非就是两种情况,踏一格和踏两格,那么我们可以和这道题进行对比,我们首先分析,到每一个格子的情况是不是可以由到前面其他格子的情况组成,然后我们可以发现,每个格子由上面和左边的格子的情况相加而成即dp[i][j] = dp[i][j-1] + dp[i-1][j];的由来,然后由于第一行和第一列的情况都是只有一种可能,然后,就可以写出代码了

class Solution {
public:
    int dp[109][109];
    int uniquePaths(int m, int n) {
        for (int i=1; i<=m; i++) dp[i][1] = 1;
        for (int i=1; i<=n; i++) dp[1][i] = 1;
        for (int i=2; i<=m; i++) {
            for (int j=2; j<=n; j++) {
                dp[i][j] = dp[i][j-1] + dp[i-1][j];
            }
        }
        return dp[m][n];
    }
};

递归写法:(超时)

代码反而意外的简单

class Solution {
public:
    int uniquePaths(int m, int n) {
        if (m==1 || n==1) return 1;
        return uniquePaths(m-1, n) + uniquePaths(m, n-1);
    }
};

记忆化写法:

class Solution {
public:
    int dp[109][109];
    int uniquePaths(int m, int n) {
        for (int i=1; i<=m; i++) dp[i][1] = 1;
        for (int i=1; i<=n; i++) dp[1][i] = 1;
        if (dp[m][n] != 0) return dp[m][n];
        return dp[m][n] = uniquePaths(m-1, n) + uniquePaths(m, n-1);
    }
};

以上是关于2023-05-04 线性DP_力扣练习的主要内容,如果未能解决你的问题,请参考以下文章

力扣 64. 最小路径和 [线性DP]

力扣 63. 不同路径 II [线性DP]

力扣 221. 最大正方形 [线性DP]

顺序表的实现以及力扣练习题

解题报告力扣 第 276 场周赛

9/4 经典dp+线性筛求质数+混合背包