动态规划roadmap
动态规划适于求解最优问题,如求最大值、最小值等。可显著降低时间复杂度,提高代码的执行效率。
难点和递归类似,求解问题的过程不太符合人类常规思维。
- 本文会先通过两个非常经典的动态规划问题模型,展示动态规划存在的意义及动态规划解法是如何演化的,学会后即可举一反三。
- 《动态规划理论》,我会总结动态规划适合解决的问题的特征,以及动态规划解题思路。除此之外,我还会将贪心、分治、回溯、动态规划这四种算法思想放在一起,对比分析它们各自的特点以及适用的场景
- 《动态规划实战》,我会教你应用第二节讲的动态规划理论知识,实战解决三个非常经典的动态规划问题,加深你对理论的理解
经典案例
0-1背包
不同重量、不可分割的物品,选择一些装入背包。
在满足背包最大重量限制的前提下,求背包中物品总重量的max?
可用回溯穷举搜索所有可能的装法,然后找出最大值。但回溯算法时间复杂度太高,为指数级:
假设:
- 背包最大承载重量9
- 有5个不同物品
- 每个物品重量2,2,4,6,3
回溯求解过程,用递归树:
每个节点表示一种状态(i, cw),如(2,2)决策第2个物品是否装入背包,在决策前,背包中物品的总重量是2。发现有些子问题求解重复,如f(2, 2)和f(3,4)都被重复计算两次。
可借助“备忘录”,记录已计算好的f(i, cw),当再次计算到重复的f(i, cw)的时候,直接从备忘录中取出来用,避免冗余计算。
跟动态规划的执行效率基本无差。
把整个求解过程分为n个阶段:
- 每个阶段会决策一个物品是否放到背包中
- 每个物品决策(放入或者不放入背包)完后,背包中的物品的重量会有多种情况,即会达到多种不同的状态,对应到递归树中,就是不同节点。
把每层重复的状态(节点)合并,只记录不同状态,然后基于上一层状态集合,推导下一层状态集合。可通过合并每一层重复状态,保证每层不同状态的个数都不会超过w个(w表示背包的承载重量)。这就避免每层状态个数的指数级增长。
二维数组states[n][w+1]
,记录每层可达到的不同状态:
- 第0个物品的重量是2,决策完后,对应背包两种状态,背包物品总重量0或2,即
states[0][0]=true
和states[0][2]=true
- 第1个物品的重量也是2,基于之前背包状态,在这个物品决策完后,不同状态有3个,背包中物品总重量分别是0(0+0),2(0+2 or 2+0),4(2+2),即
states[1][0]=true
,states[1][2]=true
,states[1][4]=true
- 考察完所有物品后,states状态数组计算完毕
0表false,1表true,只需在最后一层,找一个值为true的最接近w(这里是9)的值,即背包中物品总重量的最大值。
动态规划这个名字的由来:把问题分解为多个阶段,每个阶段对应一个决策。记录每个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,推导下一个阶段的状态集合,动态地往前推进。
回溯算法解决这个问题时间复杂度,那动态规划呢?
耗时最多就是代码中的两层for循环,所以时间复杂度。
理论上,指数级肯定要比O(n*w)高很多,如有10000个物品,重量分布1~15000,背包可承载总重量30000:
- 回溯算法解决,用具体的数值表示出时间复杂度,就是 2^10000
- 动态规划解决
动态规划执行效率较高,但需额外申请一个二维数组,空间消耗较多。空间换时间。
实际上,只需一个大小为 的一维数组。
0-1背包升级
引入物品价值。
对一组不同重量、不同价值、不可分割的物品,选择某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?
依旧可回溯:
递归树中,每个节点表示一个状态。现在需3个变量(counter, currentWeight, currentValue)表示一个状态。
有几个节点的counter和currentWeight完全相同,如f(2,2,4)和f(2,2,3)。
背包物品总重量一样,f(2,2,4)这种状态对应物品总价值更大,可舍弃f(2,2,3)这种状态,只需沿着f(2,2,4)继续往下决策。
即对于(counter, currentWeight)相同的不同状态,只需保留cv最大的继续递归处理。
用回溯算法,就没法再用“备忘录”了。需换思路,动态规划是不是可以呢?
把整个求解过程分为n个阶段:
- 每个阶段会决策一个物品是否放到背包中
- 每个阶段决策完后,背包中的物品的总重量以及总价值,会有多种情况,即会达到多种不同的状态
用一个二维数组states[n][w+1]
,记录每层可达到的不同状态,当前状态对应最大总价值。
每层中(i, cw)重复的状态(节点)合并,只记录cv值最大的状态,然后基于这些状态来推导下一层的状态。
- 时间复杂度是O(nw)
- 空间复杂度也是O(nw)
空间复杂度也可优化
应对满减
凡电商,必有促销,如双十一最常见的“满200元减30元”促销活动。
现假设你的购物车有n个商品,希望从中选几个天命物品,凑足满减条件前提下,让选出的商品价格总和最大程度接近满减条件(200元),极限省钱。
要高效解决此类问题,就需动态规划(Dynamic Programming)。
类似0-1背包,只是“重量”换成了“价格”。
购物车中有n个商品。针对每个商品都决策是否购买。每次决策之后,对应不同的状态集合。
用一个二维数组states[n][x],记录每次决策之后所有可达的状态。
0-1背包找的是≤w的最大值,x就是背包的最大承载重量w+1。
现在要找的是≥200(满减条件)的值中最小的,所以不能设置为200+1。
若要购买的物品的总价格超过200太多,如1000,那这个羊毛“薅”得就没有太大意义了,可限定x值为1001。
不过,这个问题不仅要求≥200的总价格中的最小的,还要找出这个最小总价格对应都要购买哪些商品。可利用states数组,倒推出这个被选择的商品序列。
重点看后半部分,如何打印出选择购买哪些商品呢?
状态(i, j)
只可能从
-
(i-1, j)
states[i-1][j]
==true,可达,说明没有购买第i个商品 - 或
(i-1, j-value[i])
states[i-1][j-value[i]]
==true,说明购买了第i个商品。从中选择一个可达的状态(如果两个都可达,就随意选择一个),然后,继续迭代地考察其他商品是否有选择购买。