一、简介

动态规划主要任务,确定状态方程(fn 和 fn-1、fn-2),和边界条件(n=1,2),联想高中递推问题。
线性动态规划的主要特点是状态的推导是按照问题规模 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] 线性动态规划最简单的一类问题,输入是一个串,主要有两种方式:

  1. 依赖比 i 小的 O(1) 个子问题
    dp[n]只与常数个小规模子问题有关,状态的推导过程 dp[i] = f(dp[i - 1], dp[i - 2], ...)
  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有多重形式,dp[i], 或者dp[i][j]、dp[i][j][k]

最大子序和

  1. 问题分解:用 f ( i ) {f(i)} f(i)代表以第 ii 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是:
    max ⁡ 0 ≤ i ≤ n − 1 { f ( i ) } \max _{0 \leq i \leq n-1}\{f(i)\} 0≤i≤n−1max​{f(i)}
  2. 状态转移方程:
    dp[i] = max(nums[i], nums[i] + dp[i - 1])
    或者
    dp[i] = nums[i] + max(dp[i - 1], 0)
  3. 合并子问题求得最终解:
    max_sum = max(dp)
class Solution:def maxSubArray(self, nums: List[int]) -> int:n = len(nums)dp = [0]*ndp[0]=nums[0]for i in range(1,n):dp[i] = nums[i] + max(dp[i - 1], 0)#dp[i] = max(nums[i], nums[i] + dp[i - 1])return max(dp)

最长上升子序列(LIS算法,Longes Increasing Subsequence)

  1. 问题分解:求f(i),则需要求f(i-1)中最长子序列(满足子序列最大值小于nums[i])+1
  2. 状态转移方程:
for j in range(i): #这就是依赖比 i 小的 O(n) 个子问题,即dp[i]与dp[0~i-1]都有关if nums[i]>nums[j]:# 满足子序列+nums[i]仍然能构成递增序列dp[i] = max(dp[i],dp[j] + 1) # 经历了循环,dp[i]一直在变化
  1. 从子问题最优解获取最终最优解:max(dp)
class Solution:def lengthOfLIS(self, nums: List[int]) -> int:if not nums:return 0n = len(nums)dp = [1]*n #考虑只含nums[i]的序列,这个序列长度为1for i in range(1,n):for j in range(i): #这就是依赖比 i 小的 O(n) 个子问题,即dp[i]与dp[0~i-1]都有关if nums[i]>nums[j]:dp[i] = max(dp[i],dp[j] + 1) # 经历了循环,dp[i]一直在变化return max(dp)

最长递增子序列的个数

class Solution:def findNumberOfLIS(self, nums: List[int]) -> int:if nums ==[]:return(0)n = len(nums)length = [1]*ncounter = [1]*nfor i in range(1,n):for j in range(i):if nums[j]<nums[i]:if length[j]+1 > length[i]:# 代表第一次遇到最长子序列length[i] = 1+length[j]counter[i] = counter[j]elif length[j]+1 == length[i]:# 代表已经遇到过最长子序列counter[i] = counter[i]+counter[j]tmp = max(length)res = sum([counter[i] for i in range(n) if length[i]==tmp])return(res)

俄罗斯套娃信封

先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序。之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案。

画个图理解一下,先对这些数对进行排序:

然后在 h 上寻找最长递增子序列:

这个子序列就是最优的嵌套方案。

这个解法的关键在于,对于宽度 w 相同的数对,要对其高度 h 进行降序排序。因为两个宽度相同的信封不能相互包含的,逆序排序保证在 w 相同的数对中最多只选取一个。

from bisect import bisect_leftclass Solution:def maxEnvelopes(self, arr: List[List[int]]) -> int:# sort increasing in first dimension and decreasing on secondarr.sort(key=lambda x: (x[0], -x[1]))def lis(nums):dp = []for i in range(len(nums)):idx = bisect_left(dp, nums[i])if idx == len(dp):dp.append(nums[i])else:dp[idx] = nums[i]return len(dp)# extract the second dimension and run the LISreturn lis([i[1] for i in arr])

乘积最大子数组

我们只要记录前i的最小值, 和最大值, 那么 dp[i] = max(nums[i] * pre_max, nums[i] * pre_min, nums[i]), 这里0 不需要单独考虑, 因为当相乘不管最大值和最小值,都会置0

class Solution:def maxProduct(self, nums: List[int]) -> int:if not nums: return res = nums[0]pre_max = nums[0]pre_min = nums[0]for num in nums[1:]:cur_max = max(pre_max * num, pre_min * num, num)cur_min = min(pre_max * num, pre_min * num, num)res = max(res, cur_max)pre_max = cur_maxpre_min = cur_minreturn res

按摩师(使用多个dp表)

class Solution:def massage(self, nums: List[int]) -> int:n = len(nums)if n == 0:return 0dp0, dp1 = 0, nums[0]for i in range(1, n):tdp0 = max(dp0, dp1)   # 计算 dp[i][0]tdp1 = dp0 + nums[i]   # 计算 dp[i][1]dp0, dp1 = tdp0, tdp1return max(dp0, dp1)

使用最小花费爬楼梯(倒着动态规划)


计算花费 f[i] 有一个清楚的递归关系:f[i] = cost[i] + min(f[i+1], f[i+2])。我们可以使用动态规划来实现。

  • 当我们要计算 f[i] 时,要先计算出 f[i+1] 和 f[i+2]。所以我们应该从后往前计算 f。
  • 在第 i 步,让 f1,f2 为 f[i+1],f[i+2] 的旧值,并将其更新为f[i],f[i+1] 的新值。当我们从后遍历 i 时,我们会保持这些更新。在最后答案是 min(f1, f2)。
class Solution(object):def minCostClimbingStairs(self, cost):f1 = f2 = 0for x in reversed(cost):f1, f2 = x + min(f1, f2), f1return min(f1, f2)

难-环形子组数的最大和

难-最大子矩阵

难-矩形区域不超过 K 的最大数值和

双串

有两个输入从串,长度分别为 m, n,此时子问题需要用 i, j 两个变量表示,分别代表第一个串和第二个串考虑的位置 dp[i][j]:=第一串考虑[0…i],第二串考虑[0…j]时,原问题的解。这种形式的线性 DP 的代码常见写法

for i = 1..mfor j = 1..ndp[i][j] = f(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])

时间复杂度 O(mn)O(mn),空间复杂度 O(mn)O(mn)

最长公共子序列(LCS算法,Longest Common Subsequence)

图解

典型的二维动态规划

矩阵

线性动态规划中矩阵 dp[i][j] 的问题,状态的推导方向以及推导公式与双串 dp[i][j] 相同,但是物理意义不一样,且求 dp[i][j] 时所需的子问题的变化相对更多。

最小路径和

无串

线性动态规划有一类问题是没有显式的数组或字符串的。但在计算中依然可以分成若干子问题,且有动态规划的三条性质。因此也可以用动态规划来解。

  • 只有两个键的键盘
  • 丑数 II
  • 完全平方数
  • 整数拆分

二、总结

动态规划中最重要的三个概念:最有子结构,重复子问题,无后效性。

  • 最优子结构:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。
  • 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
  • 重复子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

线性动态规划是动态规划中变化最多的一类。

首先线性动态规划针对的问题是最常见的数组,字符串,矩阵等,这三种数据结构本身就是线性的,因此出现这些类型的输入的时候,如果要用到动态规划,首先考虑线性动态规划就很合理了,因此很多问题不论最后正解是不是线性动态规划,都会首先想一下线性动态规划是否可行。

单个数组或字符串上设计一维状态,两个数组或字符串上设计两维状态,以及矩阵上设计两维状态等等,同时以上三种情况的状态设计都有可能再加上额外的指标的状态,就是前面例题中的 k,这里面变化就很多了,比如有的题目在 k 这一维上要使用二分,贪心的策略,有的题目需要 DP 状态与数据结构配合来解决问题。

除此之外还有一类问题没有显式的数组,字符串,但是在求解的时候依然满足前面提到的动态规划三条基本概念,可以用动态规划求解,这种问题通常也是线性动态规划。

动态规划-03-线性动态规划相关推荐

  1. 【动态规划】线性动态规划

    吐槽:动态规划这个东西,只要推不出状态转移方程,一切都白搭 基础知识 一. 动态规划 动态规划中最重要的三个概念:最优子结构,重复子问题,无后效性. 最优子结构:如果问题的最优解所包含的子问题的解也是 ...

  2. 动态规划之线性动态规划

    一.动态规划的定义以及术语 1.定义:将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做"动态规划" 2.阶段:把求解问题的过程恰当地分成若干个相互联系 ...

  3. 第十七章:线性动态规划

    通过上一章对01背包问题的学习,我相信同学们都动态规划都有了一个新的认识.在我们利用动态规划解决问题的过程通常会有以下几个常见的专业术语,分别是:状态定义,状态的转移,初始化和边界条件.下面我们对这几 ...

  4. 【leetcode记录03】动态规划

    1 线性动态规划 1.1 单串 T1 最长递增子序列 class Solution(object):def lengthOfLIS(self, nums):""":typ ...

  5. 算法模板:动态规划之线性DP【沈七】

    算法模板:动态规划之线性DP 前言 线性DP 数字三角形模型 摘花生 最小路径和 不同路径模型 不同路径(有障碍) 过河卒 (综合应用) 最长上升子序列模型 木棍加工 导弹拦截 完结散花 参考文献 前 ...

  6. 动态规划之线性DP题集

    动态规划之线性DP 文章目录 动态规划之线性DP (一)LIS问题 最长上升子序列 (朴素动规) (二分+贪心+动规) 最大子序和 (动规) (贪心) 最长连续递增序列 (动规) (双指针) 俄罗斯套 ...

  7. 桐爷开车 线性动态规划

    题目描述 桐爷人称计科老司机,有天要从深圳跑长途去帝都.桐爷的塞恩车每跑一千米耗油一升.塞恩车的油箱容量为200升.桐爷出发时有100升油,为了造成少耗油的假象到达帝都时至少有100升油.沿途有不少加 ...

  8. 《强化学习与最优控制》学习笔记(一):确定性动态规划和随机性动态规划

    写在前面的 这本书的作者是Dimitri Panteli Bertsekas教授,1942年出生于希腊雅典,美国工程院院士,麻省理工大学电子工程及计算机科学教授.Bertsekas教授因其在算法优化与 ...

  9. 【算法】动态规划 ④ ( 动态规划分类 | 坐标型动态规划 | 前缀划分型动态规划 | 前缀匹配型动态规划 | 区间型动态规划 | 背包型动态规划 )

    文章目录 一.动态规划场景 二.动态规划分类 1.坐标型动态规划 2.前缀划分型动态规划 3.前缀匹配型动态规划 4.区间型动态规划 5.背包型动态规划 一.动态规划场景 动态规划 动态规划使用场景 ...

最新文章

  1. Java多线程6:synchronized锁定类方法、volatile关键字及其他
  2. Tomcat源码分析 - 1
  3. 获取DOM元素方法小结
  4. python(22)--面向对象1-封装
  5. python里 try里怎么用int函数_如何在不使用try / catch的情况下测试Python枚举中是否存在int值?...
  6. Android UI的优化
  7. toast弹窗_弹窗实用素材模板|UI设计中的弹窗设计技巧,快get
  8. 该如何清理手机的垃圾?
  9. android gradle 设置,android gradle配置指南
  10. [转]Http Message结构学习总结
  11. 变压器绕组降低邻近效应_低频变压器初级短路的原因及解决方案
  12. FPN网络详解——feature pyramid network
  13. keyshot渲染很慢_提高Keyshot逼真渲染的小技巧!
  14. 12306抢票软件的一些学习
  15. 地方科技局重点科技项目在线申报管理系统
  16. 绿地控股2020年净利润下滑约7%,销售金额同比减少7.7%
  17. 同一个页面显示多个html界面,浏览器怎么设置在同一个界面/窗口打开多个网页...
  18. 知识库微信小程序开发
  19. EBC金融外汇原油专题|权威解读「七大因素主导全球原油价格走势」
  20. 谷歌浏览器chrome取消左右滑动

热门文章

  1. 文献 | 越想快乐,越不快乐
  2. FileSystemWatcher 基础用法
  3. sql查询数据库表中重复数值
  4. ubuntu下使用 RabbitVCSRapidSVN
  5. 什么是IDC?数据中心该如何选择?
  6. 2018最新Web前端经典面试试题及答案-史上最全前端面试题(含答案)--转载
  7. 【数学建模】十:MATLAB CUMCM真题求解实例一:数据型
  8. ubuntu中静态ip的修改配置
  9. 【太虚AR_v0.1】使用教程 | 云识别(图像识别)
  10. C++后端开发面试题精选