先来说一下几种经典算法大概的意思是:
- 贪心算法:就一次机会,为了达到目的,其他什么都不用想
- 回溯算法:有无数次机会,一定要达到目的
- 动态规划算法:能随意在任何时候进行存档读档,用最华丽的方法走到终点
接下来主要围绕动态规划来说说,去解释动态规划算法的内在。。。。
简述
动态规划求解出最优策略的原理是因为显著的降低了时间复杂度,提高了代码的运行效率,但动态规划确实是有点难学,不过一旦了解了解决思路,对于很多动态规划的问题,都可以用相似的思路去解决。。
用实际问题更好的去理解动态规划
0-1背包问题
在前面的文章中的回溯算法中,知道了如何采用递归的思想穷举所有算法,选出最优去解决问题, 同样的问题如果要对于一组不同重量、不可分割的物品,选择一些装入背包,在满足最大重量限制的前提下,如何最贴近最大重量。。
首先用一下递归树来看一下回溯的思路:
首先假设每个物品重量是2 2 4 6 3,假设i表示将要决策第几个物品是否装入背包,cw 表示当前背包中物品的总重量,然后用(i,cw)。比如(2,2)表示将要决策第2个物品是否要装入背包,决策前,背包的总重为2。
可以从递归树明显的看出,有些子问题的求解是重复 的,重复的计算必然会造成性能的丢失,如果能保留之前的结果, 计算之前先检索一下,如果计算过直接拿过来用,肯定会提升下性能。。。
事实上,动态规划的思路已经和这相差无几,来看看动态规划的思路:
- 首先将整体求解分为n个阶段,每个阶段会决策一个物品是否放到包里。
- 不论放入还是不放入,背包的重量假设都造成了变化,而这些不同的变化,也对应着递归树的节点
- 合并重复的节点,避免节点指数级增长
- 基于这一层的节点,推导出下一层的状态集合。
用上面的思路,代入到0-1背包问题中做一个思路引导:
- 首先创建一个布尔类型的二维数组states[n][w],记录每个节点的不同状态。n代表第n个物品,w代表背包能承受的最大质量。。
- 第0个(假设物品序列从0开始计算)物品的重量是2,放入,就是states[0][2],不放入,就是states[0][0],但是因为最大重量w是9,所以上面两个表达式的结果都是true。
- 然后是第一个物品,重量也是2,那么造成的结果就是,states[1][4],states[1][2],states[1][2],states[1][0]
- 然后发现,有两个表达式重复了,所以合并掉,其他三个因为重量小于9,所以也是true
- 不断继续,最后只需要选择最接近9还是true的就行
false表示0,true表示1,图如下:
代码表示如下:
这是动态规划总体的思路,而且时间复杂度比回溯的n²也有了很大的改善,是wn,n表示物品个数,w表示可以承载的总量。
但是为了能够解决所有情况,在存档的时候还是很占用空间的,那么什么办法可以降低空间消耗呢??
实际上,可以将二维数组转换成一维数组,在动态转移的过程中都可以依靠这个一维数组来完成,看一下代码:
0-1背包问题升级版
如果将这些物品赋予价值概念,如何让背包最大可承受重量一定的时候让价值更高。。。
首先这个问题是可以通过回溯的算法解决的:
针对上面的代码画出递归树,和之前思路不同只不过加入了一个新的值:value
发现在很多情况下,重量相同但是价值有区别,这就是这个题目的目标,舍弃掉相对价值低的,保留相对价值更高的,保留递归的思路,其他状态不用考虑了。。
采用动态规划思想。。。
设一个二维数组states[n][w+1],来记录每层都可以达到的不同状态,不过这次不再是布尔类型了,而是int类型,用来记录当前状态的最大价值,然后按照之前的思路根据当前的阶段,推导出后续状态。。。
代码如下:
其实思路和之前是相同的,但是这里引入了第三个变量,就是value,和之前的例子大同小异,时间复杂度是wn。。。