数据结构与算法 | 动态规划算法(Dynamic Programming)
上一篇文末已经提到了记忆化搜索是动态规划(Dynamic Programming)的一种形式,是一种自顶向下(Top-Down)的思考方式,通常采用递归的编码形式;既然动态规划有自顶向下(Top-Down)的递归形式,自然想到对应的另外一种思考方式自底向上( Bottom-Up ),也就是本篇要写的内容。
什么是自底向上的思考?不空谈理论,还是借个实际题目来体会。
自底向上( Bottom-Up )
LeetCode 53. 最大子数组和【中等】
给你一个整数数组nums请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
示例:
输入:nums = -2,1,-3,4,-1,2,1,-5,4
输出:6
解释:连续子数组 4,-1,2,1 的和最大为 6 。
对于连续子数组求和,先 想象 一些简单场景
- 场景一 :数组只有1个元素,那么最大和就1种情况 也就是
第1个元素;undefined对比场景一 与 场景二,由于第2个元素的出现 从而导致最大和子数组 可能包含第2个元素,从而新出现了:第1个元素+第2个元素、第2个元素两种情况,多出来的其实是:第2个元素作为子数组尾元素的情况。 - 场景二 :数组扩展到2个元素,那么最大和就有3种情况了:
第1个元素、第1个元素+第2个元素、第2个元素。
分析下 以第2个元素作为子数组尾元素 的最大和? 第2个元素 + 某个数 要最大,自然这里的 某个数 越大越好,因此它的一个选择就是 第2个元素 + 前一个元素(第1个元素)作为子数组尾元素的最大和 ; 但考虑到 某个数 可能为负数,所以 最大和还有一种选择 就是 第2个元素 了。
综上分析,以第2个元素作为子数组尾元素的最大和 就是在:以第1个元素作为数组尾元素最大和 + 第2个元素,第2个元素 中选一个较大的。
如果再加入第3个元素,以第3个元素作为子数组尾元素的最大和选择同理也是:第3个元素,第3个元素+以第2个元素作为子数组尾元素的最大和中选一个较大的。
依次类推第n个元素 a[n] 作为子数组尾元素的最大和 s[n],用公式来说就是:
s[n] = Max( a[n] , s[n-1] + a[n] )
所谓整个数组的最大和的连续子数组,无非是在 第1、第2...第n个元素结尾的 最大和 中选取最大的,这样也就完成题目的解答了。分析清楚了,代码实现上就比较简单了:
public int maxSubArray(int[] nums) {
int state = nums[0],max = state;
for(int i = 1; i < nums.length; i++){
// s[n] = Max(a[n], s[n-1] + a[n])
state = Math.max(nums[i],state + nums[i]);
max = Math.max( max , state );
}
return max;
}
可以看到解题的整个过程,从最简单基本的情况开始,一步一步推导到结果。这就是自底向上( Bottom-Up )的推导过程,实现上通常使用的是递推的编码方式。
阶段(Stage)、状态(State)、决策(Decision)
如果把推导 第1个元素、第2个元素...第n个元素结尾子数组最大和 看成一个个决策 阶段,s[n]可以看作第n个阶段决策后的 状态 ,所谓上题的 决策 就是指取最大值。这里值得注意点的是,第n个阶段决策只依赖于第n-1个阶段的状态。
以上就是动态规划中的三个重要概念:阶段、状态、决策。
阶段(Stage): 阶段表示解决问题的不同时间点或步骤。问题通常可以划分为多个阶段,每个阶段都对应于解决问题的一个关键步骤。动态规划算法通过逐个处理这些阶段来解决问题。状态(State): 状态是描述问题局部情况的变量。在每个阶段,问题的状态会发生变化。动态规划算法的目标是定义状态,使得问题的解可以通过不同阶段的状态之间的转移关系来表示。状态是问题的关键抽象,对于不同的问题,状态的选择可能会有很大的差异。决策(Decision): 决策是在每个阶段基于当前状态做出的选择。这些决策会影响问题的状态转移,从而影响问题的最终解。动态规划算法的关键之一是找到在每个阶段应该做出的最优决策,以达到整体问题的最优解。
对于递推公式: s[n] = Max(a[n], s[n-1] + a[n]),动态规划中也被称作 状态转移方程。
一般性解题步骤(General Problem-solving Steps)
如果非常细致的朋友会注意到,上述解题过程中最初用了 “ 想象 ” 一词;行文如此是为了更好的描述自底向上的过程。如果是解答一道从来没接触过的动态规划类算法题,通过“想象”推导分析出阶段来难度可能过高,当然不排除想象力丰富的朋友有此能力。
前文提到的阶段也好、决策也好都围绕的是状态;定义动态规划的状态是关键,可以说找到状态转移方程问题就已经解决了60%。这里给出一些一般性的解题步骤_参考_,以降低 “想象”的难度。
- 定义状态: 确定问题的状态,即描述问题局部情况的变量。状态的选择对问题的建模至关重要,不同的状态选择可能导致完全不同的动态规划解法。
- 找到状态转移方程: 对于每个状态,找到它与其他状态之间的关系,即状态转移方程。状态转移方程描述了问题从一个阶段到下一个阶段的转移规则,是动态规划问题的核心。
- 初始化: 定义问题的初始状态并进行初始化。
- 确定边界条件: 确定问题的边界条件,即在最小规模下直接解决问题的情况。这是递归或递推开始的基础,通常是问题的最小子问题。
- 计算顺序: 确定计算状态的顺序,也是划分阶段。可以采用自顶向下(Top-Down)的递归方法,也可以采用自底向上(Bottom-Up)的递推方法。
- 计算最终解: 根据 1-5 的分析结果进行编码,最终解通常是在问题的最后一个阶段计算得到的,它可能存储在动态规划表格的特定位置。
一些场景下可能需要记录路径或决策,如果需要记录最优解的具体路径或决策,可以在计算的过程中进行记录.当然,以上也只是建议的步骤并非一定要如此。
PS: 这里不妨多说一点状态,在上一篇中有提到过动态规划是最灵活的算法之一;灵活的原因就是面对不同问题状态设计的多样性;如果要想快速解出动态规划类题,还是需要多多练习,积累一些设计状态的经验。因此初学时不用太在意对比经验丰富的朋友,解决动态规划类效率较慢,差距可能只在于缺少各种状态设计的经验积累。
基本应用示例(Basic Examples)
LeetCode 122. 买卖股票的最佳时机 II【中等】
给你一个整数数组 prices ,其中 pricesi 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。
示例:
输入:prices = 1,2,3,4,5
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。总利润为 4 。
- 状态定义,preSell 定义为上一天没有持股情况下 最大利润,preBuy 定位为 持有股票情况下 最大利润。
- 状态转移方程:
currentSell = Max( preBuy + prices[i], preSell)
currentBuy = Max( preSell - prices[i], preBuy)
计算下一个状态时,current 也就变成了 pre。考虑到最终最大利润一定时 不持股情况,因此 max 只需记录过程中 currentSell 的最大值。
- 初始化以及边界条件,不持股下 preSell 为
0,持股下 买入了第一只股票 preBuy 为-prices[0]
public int maxProfit(int[] prices) {
int preSell = 0,preBuy = -prices[0];
int max = 0;
for(int i = 1; i < prices.length; i++){
int currentSell = Math.max(preSell,preBuy + prices[i]);
int currentBuy = Math.max(preBuy, preSell - prices[i]);
preSell = currentSell;
preBuy = currentBuy;
max = Math.max(currentSell,max);
}
return max;
}
LeetCode 198. 打家劫舍【中等】
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例:
- 输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
这里可以体会下设计状态的经验,相信有 前一题的铺垫,对于这道题的状态设计就比较类似了。
- 定义状态,preSteal 前一夜偷了的最高金额,preNosteal 前一夜没偷的最高金额
- 状态转移方程:
nosteal = Max( preNosteal , preSteal )
steal = preNosteal + nums[i]
- 初始化以及边界条件,略
public int rob(int[] nums) {
int preSteal = nums[0],preNosteal = 0;
for(int i = 1; i < nums.length; i++){
int nosteal = Math.max(preNosteal,preSteal);
int steal = preNosteal + nums[i];
preSteal = steal;
preNosteal = nosteal;
}
return Math.max(preNosteal,preSteal);
}
LeetCode 377. 组合总和 Ⅳ【中等】
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例:
输入:nums = 1,2,3, target = 4
输出:7
解释:所有可能的组合为:(1, 1, 1, 1)、(1, 1, 2)、(1, 2, 1)、(1, 3)、(2, 1, 1)、(2, 2)、(3, 1)。请注意,顺序不同的序列被视作不同的组合。
这个类似于01背包的问题,状态设计上也比较类似
- 定义状态,
s[i]代表目标整数 i 时,组合的个数 - 状态转移方程:
对于整数 nums[j] , 如果 i >= nums[j] 代表有可能被 nums[j] 组成
相当于 s[ i-nums[j] ] 组合里面 都加上个 nums[j]元素 ,
所以s[i] 组合数新增了 s[ i-nums[j] ] 个。
s[i] += s[ i-nums[j] ]
- 初始化状态:s[0] 代表着不选取任何元素的是 target = 0,此时不选取任何元素为 唯一的方案。
public int combinationSum4(int[] nums, int target) {
int[] s = new int[target+1];
s[0] = 1;
for(int i = 1; i <= target; i++){
for(int j = 0; j < nums.length; j++){
if(i - nums[j] < 0) continue;
s[i] += s[i - nums[j]] ;
}
}
return s[target];
}
欢迎关注 Java研究者专栏、博客、公众号等
数据结构与算法 | 动态规划算法(Dynamic Programming)的更多相关文章
- 笔试算法题(44):简介 - 动态规划(Dynamic Programming)
议题:动态规划(Dynamic Programming) 分析: DP主要用于解决包含重叠子问题(Overlapping Subproblems)的最优化问题,其基本策略是将原问题分解为相似的子问题, ...
- 动态规划算法详解 Dynamic Programming
博客出处: https://blog.csdn.net/u013309870/article/details/75193592 前言 最近在牛客网上做了几套公司的真题,发现有关动态规划(Dynamic ...
- 对动态规划(Dynamic Programming)的理解:从穷举开始(转)
转自:http://janfan.cn/chinese/2015/01/21/dynamic-programming.html 动态规划(Dynamic Programming,以下简称dp)是算法设 ...
- 【动态规划】Dynamic Programming
动态规划 一.动态规划 动态规划(Dynamic Programming)是一种设计的技巧,是解决多阶段决策过程最优化问题的通用方法. 基本思想:将待求解问题分解成若干个子问题,先求解子问题,然后从这 ...
- 动态规划(Dynamic Programming, DP)---- 最大连续子序列和
动态规划(Dynamic Programming, DP)是一种用来解决一类最优化问题的算法思想,简单来使,动态规划是将一个复杂的问题分解成若干个子问题,或者说若干个阶段,下一个阶段通过上一个阶段的结 ...
- 详解动态规划(Dynamic Programming)& 背包问题
详解动态规划(Dynamic Programming)& 背包问题 引入 有序号为1~n这n项工作,每项工作在Si时间开始,在Ti时间结束.对于每项工作都可以选择参加与否.如果选择了参与,那么 ...
- 算法导论学习-Dynamic Programming
转载自:http://blog.csdn.net/speedme/article/details/24231197 1. 什么是动态规划 ------------------------------- ...
- #C++初学记录(动态规划(dynamic programming)例题1 钞票)
浅入动态规划 dynamic programming is a method for solving a complex problem by breaking it down into a coll ...
- 动态规划(Dynamic Programming)
introduction 大部分书籍介绍"动态规划"时,都会从"菲波纳切数列"讲起. 菲波纳切数列 递归解法 C++ 代码如下 unsigned long in ...
- 动态规划小结(dynamic programming)
动态规划在很多情况下可以降低代码的空间时间复杂度. 判断一道问题能否应用动态规划,需要关注问题是否具有最优子结构,当前规模的问题的解在之前问题的解里面,还要注意的是要满足无后效性的原则.随后就是寻找递 ...
随机推荐
- Linux chroot的关联操作 mount --bind
假设新的根文件系统已经挂载到 /mnt mount --bind /dev /mnt/dev/ mount --bind /sys /mnt/sys/ mount --bind /proc /mnt/ ...
- linux top中 VSS,RSS,PSS,USS 4个字段的解读
参考文章:linux中top命令 VSS,RSS,PSS,USS 四个内存字段的解读
- Blazor前后端框架Known-V1.2.8
V1.2.8 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行. Gitee: https://gitee.com/known/Known Gith ...
- 面试再也不怕问ThreadLocal了
要解决多线程并发问题,常见的手段无非就几种.加锁,如使用synchronized,ReentrantLock,加锁可以限制资源只能被一个线程访问:CAS机制,如AtomicInterger,Atomi ...
- Unity的IUnityLinkerProcessor:深入解析与实用案例
Unity IUnityLinkerProcessor Unity IUnityLinkerProcessor是Unity引擎中的一个接口,它允许开发者在Unity项目构建时对代码进行链接处理.这个接 ...
- Jitpack发布Android库带文档和源码
原文地址: Jitpack发布Android库带文档和源码 - Stars-One的杂货小窝 忽然发现自己发布的xAndroidUtil库 写代码的时候看方法注释都看不到,研究了下如何让Jitpack ...
- 高德Android高性能高稳定性代码覆盖率技术实践
前言 代码覆盖率(Code coverage)是软件测试中的一种度量方式,用于反映代码被测试的比例和程度. 在软件迭代过程中,除了应该关注测试过程中的代码覆盖率,用户使用过程中的代码覆盖率也是一个非 ...
- Vue源码学习(一):数据劫持(对象类型)
好家伙,了解一下Vue如何实现数据劫持 1.Vue中data的使用 首先,我得搞清楚这玩意的概念,我们先从vue的使用开始吧 想想看,我们平时是如何使用vue的data部分的? 无非是这两种情况 (你 ...
- 如何正确实现一个自定义 Exception
最近在公司的项目中,编写了几个自定义的 Exception 类.提交 PR 的时候,sonarqube 提示这几个自定义异常不符合 ISerializable patten. 花了点时间稍微研究了一下 ...
- vue列表逐个进入过渡动画
vue实现列表依次逐渐进入动画 利用vue 中transition-group 实现列表逐个进入动画效果 1.vue3代码, <template> <section class=&q ...