算法之动态规划
文章目录
前言
借助解决实际代码问题来理解动态规划!
对于可以用动态规划求解的问题可以使用暴力求解!——穷举出所有可能的结果!时间复杂度为指数级别
有种说法:动态规划利用空间换时间,因为有利用记忆解决的
基本思想:自底向上解决问题,从最简单的情况出发。将大问题分解成一个一个小问题,解决小问题,大问题自然就解决了
动态规划的应用:最短路径(弗洛伊德算法)、库存管理、资源分配、设备更新、排序、装载
1.1 相关定义&理论
定义&理解:求解决策过程最优化的过程
1、动态规划的基本结构SRTBOT
- 子问题:状态
- 关系:状态转移
- 子问题之间的依赖和递归调用是该有向无环图的一个拓扑排序:动态规划对状态空间(所有子问题)的遍历构成一张有向无环图
- 最原始的情况:子问题的初始值
- 我们的原始问题:判断是否与子问题一致?
- 时间复杂度
2、利用动态规划求解问题步骤:找到状态转移方程,即n得不同情况,像fib这样式子是什么?自变量?因变量?根据原问题来确定。
- (1) 定义状态(子问题):dp[i]表示第i个子问题
- (2) 状态转移方程(子问题之间的联系):法一分情况考虑;法二直接几种情况总和考虑(如求最大值就取这几种情况的最大值)
- 「状态转移方程」:例如f(n) 的状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移。所有解法(暴力、备忘录、动态规划)例如 return f(n - 1) + f(n - 2),dp[i] = dp[i - 1] + dp[i - 2],以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。它是解决问题的核心,其实状态转移方程直接代表着暴力解法。
- 「状态」:原问题和子问题中会变化的变量。即自变量x;「选择」:导致「状态」产生变化的行为。即因变量y;dp数组/函数的含义。
- (3) 子问题初始值:能够显而易见得到的子问题的值
- (4) 输出:组合所有子问题的解从而获得原问题的解。判断:问的问题(原始问题) ?= 子问题,因为对于简单的动态规划问题,问的问题就可以设计成为子问题。但如果不一致,则不能直接将最后一个状态返回回去。
- (5) 是否优化空间.
3、如何利用动态规划求解问题?
- 自顶向下:利用备忘录实现递归:存在大量重复计算的子问题时,可以使用记忆法。比如最短路径查找,1→5,可能会查找多次,我们是为了找最短的,比如之前已经查找过了得到最短(已经记忆的肯定是最短的,因为通过递归我们得到的就是最短的),后面再用的话可以直接使用我们已经保存的最短路径,就无需重复计算了而且还未必是最短的。
- 自底向上:脱离递归由循环迭代完成计算。由边界条件求出fib(2)=fib(0)+fib(1),接着索引i从2依次递增到n,根据递归式使用循环,而非递归函数实现求解斐波那契函数。即把备忘录独立为一张表,在这张表上完成自底向上的推算!
自底向上实现递归的本质就是填表(动态规划表)。每一个节点表示一个子问题,该子问题要么依赖于前一个结点,要么依赖于前两个节点。
4、对动态规划的理解:
动态规划对状态空间(所有子问题)的遍历构成一张有向无环图,遍历就是该有向无环图的一个拓扑序。
有向无环图中的节点对应问题中的「状态」,图中的边则对应状态之间的「转移」,转移的选取就是动态规划中的「决策」。
5、补充:
无后效性:为了保证计算子问题能够按照顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。
1.2 体会寻找子问题:最大子数组和
1、问题描述:一个整数数组 nums ,找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。其中子数组是数组中的一个连续部分。
2、解决
子问题中的自变量:以…结尾的连续数组;因变量:数组和。子问题中:因为输入的不同连续数组导致了和的改变。存在最优子结构:任意连续数组的和都是最大的。
最优子结构:比如输入连续子数组的和最大。那么其最优子结构如输入连续数组(以数组中序号0/1/2/3/4开头的连续子数组)的时候,其和也是最大的。
无论输入的是以哪个元素为结尾的连续子数组,在这过程中的该子数组的和都是最大的即最优解。
1、问题分析:对于输入数组:[-2,1,-3,4,-1,2,1,-5,4]
- 初步定义子问题:经过输入数组的某个数(如-2)的连续子数组的最大和是多少?——这些子问题之间的联系不易看出,即子问题的描述还有不确定的地方(有后效性)
- 不确定性的解决方法:将该数(如-2)定义成连续子数组的最后一个元素。因为不确定该数究竟是连续数组的第几个元素,因此这里将输入序列的前缀当成是一个子问题
- 重新定义子问题:以输入数组的某个数(如-2)结尾的连续子数组的最大和是多少?——子问题之间有联系
2、动态规划解题步骤
- (1) 定义状态(子问题):dp[i]表示以nums[i]结尾的连续子数组的最大和
- (2) 状态转移方程(子问题之间的联系):法一分情况考虑:①dp[i-1]<=0得到dp[i]=nums[i]②dp[i-1]>0得到dp[i]=dp[i-1]+nums[i];
法二取这几种情况的最大值max{dp[i-1]+nums[i],nums[i]}- (3) 子问题初始值:dp[0]=nums[0],该子问题只有一个数一定以nums[0]结尾。
- (4) 输出:这里的状态定义不是题目中的问题的定义,因此不能直接将最后一个状态返回回去,如最后一个状态是以该数组的最后一个数结尾的连续数组的最大和是多少?这显然是不行的!
这个问题的输出需要把所有的dp[0]、dp[1]…dp[n-1]都看一遍,取最大值- (5) 优化空间
class Solution {
public int maxSubArray(int[] nums) {
//1、定义状态(子问题)
int[] dp = new int[nums.length];//dp[i]表示以nums[i]结尾的连续子数组的最大和
//3、子问题的初始值
dp[0] = nums[0];
//2、子问题之间的关系
for(int i = 1; i < nums.length; i++){
if(dp[i-1]<=0){
dp