赞
踩
借助解决实际代码问题来理解动态规划!
对于可以用动态规划求解的问题可以使用暴力求解!——穷举出所有可能的结果!时间复杂度为指数级别
有种说法:动态规划利用空间换时间,因为有利用记忆解决的
基本思想:自底向上解决问题,从最简单的情况出发。将大问题分解成一个一个小问题,解决小问题,大问题自然就解决了
动态规划的应用:最短路径(弗洛伊德算法)、库存管理、资源分配、设备更新、排序、装载
定义&理解:求解决策过程最优化的过程
1、动态规划的基本结构SRTBOT
2、利用动态规划求解问题步骤:找到状态转移方程,即n得不同情况,像fib这样式子是什么?自变量?因变量?根据原问题来确定。
3、如何利用动态规划求解问题?
4、对动态规划的理解:
动态规划对状态空间(所有子问题)的遍历构成一张有向无环图,遍历就是该有向无环图的一个拓扑序。
有向无环图中的节点对应问题中的「状态」,图中的边则对应状态之间的「转移」,转移的选取就是动态规划中的「决策」。
5、补充:
无后效性:为了保证计算子问题能够按照顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。
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[i] = nums[i]; }else{ dp[i] = dp[i-1]+nums[i]; } } //查看各个子问题的解 for(int i = 0; i < dp.length; i++){ System.out.print(dp[i] + " "); } /** 最简单的:找数组dp中的最大值也可以视为一种动态规划: * 1、定义状态(子问题):第1个是最大值;第2个是最大值;...;第i-1个是最大值 * 2、子问题之间的关系:当前比前一个大就是当前,比他小就是前一个 * 3、子问题初始值:第1个是最大值 * */ int res = dp[0]; for(int i = 1; i < dp.length; i++){ if(dp[i] > res){ res = dp[i]; } } return res; } }
说明:斐波那契数列没有求最值,所以严格来说不是动态规划问题
动态规划问题的一般形式就是求最值。一种最优化方法,比如求最长递增子序列、最小编辑距离等等。
求解动态规划的核心问题是穷举。例如求最值:把所有可行的答案穷举出来,然后在其中找最值。
动态规划的穷举:动态规划三要素:重叠子问题、最优子结构、状态转移方程
动态规划思维框架:
- 明确 base case:
- 明确「状态」:原问题和子问题中会变化的变量。即自变量x
- 明确「选择」:导致「状态」产生变化的行为。即因变量y
- 定义 dp 数组/函数的含义。动态规划的最优化过程
//初始化 base case
dp[0][0][...] = base
//进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
解法一:暴力递归
递归树:递归问题→递归树,便于分析算法复杂度=子问题个数*解决一个子问题需要的时间
如下递归树理解:想要计算原问题 f(20),就得先计算出子问题 f(19) 和 f(18),然后要计算 f(19),就要先算出子问题 f(18) 和 f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。
暴力递归:斐波那契的数学形式就是递归的
算法复杂度:子问题个数(递归树中节点的总数O(2^n))× 解决一个子问题需要的时间(O(1))= O(2^n) 。指数级别——爆炸
int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
暴力递归算法低效的原因:存在大量【重复计算】,比如 f(18) 被计算了两次,并且以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 f(18) 这一个节点被重复计算。这就是动态规划问题的第一个性质:重叠子问题。
解法二:带备忘录的递归解法
造一个「备忘录」解决暴力递归的重复计算问题,将每次算出某个子问题的答案先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。
递归树——带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。
带备忘录的递归解法
算法复杂度:子问题个数(子问题就是 f(1), f(2), f(3) … f(20)即图中节点的总数=输入规模n = 20成正比即O(n)) × 解决一个子问题需要的时间(没有循环即为O(1))。所以该算法的复杂度为O(n)。
int fib(int N) { if (N < 1) return 0; // 备忘录全初始化为 0 vector<int> memo(N + 1, 0); // 进行带备忘录的递归 return helper(memo, N); } int helper(vector<int>& memo, int n) { // base case if (n == 1 || n == 2) return 1; // 已经计算过 if (memo[n] != 0) return memo[n]; memo[n] = helper(memo, n - 1) + helper(memo, n - 2); return memo[n]; }
总结:带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。这种方法叫做「自顶向下」,动态规划叫做「自底向上」。
解法三:动态规划dp的迭代解法
把「备忘录」独立出来成为一张表,可以叫做 DP table,在这张表上完成「自底向上」的推算!
自底向上
动态规划迭代解法
int fib(int N) {
vector<int> dp(N + 1, 0);
// base case
dp[1] = dp[2] = 1;
for (int i = 3; i <= N; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[N];
}
细节优化:根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1):
状态压缩:如果每次状态转移只需要 DP table 中的一部分,那么可以尝试用状态压缩来缩小 DP table 的大小,只记录必要的数据,上述例子就相当于把DP table 的大小从 n 缩小到 2。一般来说是把一个二维的 DP table 压缩成一维,即把空间复杂度从 O(n^2) 压缩到 O(n)。
最优子结构:比如凑出总金额为11的硬币数量最少,那么其最优子结构如凑出目标金额为7/8/1的时候,使用的硬币数量也是最少的。
1、问题:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
输入举例:amount = 11, coins = {1,2,5}
// coins 中是可选硬币面值,amount 是目标金额
int coinChange(int[] coins, int amount);
2、解决问题
自变量:目标金额;因变量:硬币的数量。子问题中:因为总金额改变了导致硬币的数量改变。存在最优结构:各个阶段使得硬币数量最少的最优的金额,全部合起来得到11,此时硬币的数量也是最少的!
int coinChange(int[] coins, int amount) {
int[] dp = new dp[coins.length];
dp[0] = 0;
for (int i = 0; i <= amount; i++) {
for(int j= 0; j < coins.length; j++){
if (i - coin < 0) continue;//子问题无解跳过:注意硬币面额>目标金额要去掉!
dp[i] = Math.min(dp[i], dp[i-1]+1);//要么是当前目标金额 i 的硬币数量,要么是前一个目标金额为i-1的硬币数量+1
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。