我们已经掌握了较为常用和简单的分治算法思维,那么在分治的基础上,我们通过对比来学习动态规划的算法思维。
动态规划(Dynamic Programming,简称DP)算法思维常见于某类最优化问题,此类问题其绝大部分不能通过穷举和遍历来推导,相当一部分不能通过简单递归来实现,而是需要进行多阶段的决策过程最优解。
动态规划一般分为这么几步进行:
划分子问题不同阶段 --》抽象每个阶段的状态变量 --》确定决策方法和状态转移方程 --》寻找边界条件 --》以递推形式从小到大依次求解 --》以矩阵或其他数据结构的形式记录子问题的解避免重复计算(以空间换时间)
从上面能看到动态规划与分治两种算法都是针对可划分较小规模问题的解,不同点是动态规划的较小子问题是交叠的,而且要存储较小子问题的解;而分治的较小子问题是相互独立的。这也是为什么分治算法通常用递归就可以实现,而动态规划即使使用递归的同时还需要对多阶段进行决策。
一般来说,一个经典的动态规划算法是自底向上的(从较小问题的解,由交叠性质,逐步决策处较大问题的解),它需要解出给定问题的所有较小子问题。动态规划一个变种是试图避免对不必要的子问题求解。如果采用自顶向下的递归来解,那么就避免了不必要子问题的求解(相对于动态规划表现出优势),然而递归又会导致对同一个子问题多次求解(相对于动态规划表现出劣势),所以将递归和动态规划结合起来,就可以设计一种基于记忆功能的从顶向下的动态规划算法。
简单总结动态规划算法思维的几个关键点:
1.最优化原理
无论过去状态和决策如何,对前面的决策所形成的状态而言,余下的决策必须构成最优策略,简单来说就是一个最优化策略的子策略总是最优的。
2.无后效性
某个状态下的决策的收益,只与状态和决策相关,与达到该状态的方式无关。
3.子问题交叠
即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解。
4.在形式上往往表现为填矩阵的形式,填矩阵的顺序对应递推的依赖形式
有2点我觉得非常重要,一是填矩阵的顺序,二是动态规划空间复杂度的优化:这2点都跟递推式的依赖关系有关(这是本质),在形式上就表现为填矩阵的时候你的顺序要确保每填一个新位置时你所用到的那些位置(即它依赖的)要已经填好了,在空间优化上表现为当一个位置在以后还有用的时候你不能覆盖它。
练习1:如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
子问题的状态变量:总面值x元;子问题的解即状态:最少y枚硬币 ;用dp【x】记录状态值
决策方法:dp【x】=min(dp【x-5】+1;dp【x-3】+1;dp【x-1】+1)
边界条件:dp【1】=1;dp【3】=1;dp【5】=1;
练习2:LIS 最大非降子序列
子问题的状态变量:给定序列中以第i个元素为结尾的最大非降子序列;子问题的解即状态:这个以第i个元素为结尾的最大非降子序列的长度;用dp【i】记录状态值
决策方法:dp【i】=遍历dp【j】从dp【1】到dp【i-1】,如果A[j]<= A[i]则尝试更新dp【i】=dp【j】+1,直至找出最大的dp【i】;如果遍历完所有的dp【j】都没有A[j]<=A[i]那么dp【i】=1;
边界条件:dp【1】=1
用大白话解释就是,想要求d(i),就把i前面的各个子序列中,最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为d(i)。当然了,有可能i前面的各个子序列中最后一个数都大于A[i],那么d(i)=1,即它自身成为一个长度为1的子序列。
以上为复杂度o(n*n)的算法,另有更为巧妙的o(nlogn)复杂度的算法如下(类似最大和子序列问题解法)
用序列存储不同长度LIS的最小末尾数值,每增加一个元素将其与所有长度的LIS末尾数值比较和更新末尾数值,
注意更新末尾数值的操作并不一定影响最大LIS值,但是会影响后面新元素的判断。
https://www.felix021.com/blog/read.php?1587
初步入门:https://blog.csdn.net/baidu_28312631/article/details/47418773
从入门到熟练:https://blog.csdn.net/cangchen/article/details/45045315
加强学习:https://blog.csdn.net/cangchen/article/details/45045355
例题-最大公共子序列:https://blog.csdn.net/u013074465/article/details/45392687
经典案例:https://blog.csdn.net/uestclr/article/details/50760563