程序员必备基本算法-动态规划法

597 阅读12分钟

动态规划是在20世纪50年代由美国数学家贝尔曼为研究最优控制问题而提出的,当该方法在应用数学中的价值被大家认同以后,在计算机学界,动态规划法成为一种通用的算法设计技术用来求解多阶段决策最优化问题。

所以,同学们,大家觉得特别难的动态规划问题是人家1950年想出来的,据目前已经有70年了。另外,有些同学也不要问计算机能不能改变世界了好吗?写CRUD确实挺难改变世界的,但是如果能想出如动态规划、Paxos、Raft等,或者能将数学上已经有的内容转化为计算机,都是很有贡献的!!!

动态规划法是将待求解问题分解成若干个子问题,但是子问题间往往不是相互独立。动态规划法将每个子问题只求解一次并将其解保存在一个表格中,当需要再次求解此子问题时,只是简单地通过查表获得该子问题的解,从而避免了大量重复计算。

概述

最优化问题

有n个输入,它的解由这n个输入的一个子集组成,这个子集必须满足某些事先给定的条件,这些条件称为约束条件,满足约束条件的解称为问题的可行解。满足约束条件的可行解可能不止一个,为了衡量这些可行解的优劣,事先给出一定的标准,这些标准通常以函数的形式给出,这些标准函数称为目标函数。使目标函数取得极值的可行解称为最优解,这类问题就称为最优化问题

最优性原理

对于一个具有n个输入的最优化问题,求解过程往往划分为若干个阶段,每一阶段的决策仅依赖于前一阶段的状态,由决策所采取的动作使状态发生转移,成为下一阶段决策的依据。

S为状态,P为策略,如果一个状态可以做出多个决策,而每一个决策可以产生一个新的状态,动态规划的决策过程如下所示:

根据状态S0和P1策略集合,生成S1状态集合,把这些决策的集合作为这一阶段的子问题的解保存起来。然后再S1的基础上分别执行P2,产生状态集合S2,最终生成状态Sn。Sn中只有一个最优解,这个最优解对应一个决策Pn,kn,然后不断往回回溯,一直进行到P1,k1,从而获得最优决策序列。

多决策过程满足最优性原理:无论决策过程的初识状态和初识决策是什么,其余的决策都必须相对于初始决策所产生的的当前状态,构成一个最优决策序列

所以这是一个先从前往后跟进状态和策略不断计算,然后从后到前回溯找到最优链路的过程。

动态规划法的设计思想

动态规划法利用问题的最优性原理,以自底向上的方式从子问题的最优解逐步构造出整个问题的最优解。应用动态规划法设计算法一般分为3个阶段:

1)分段:将原问题分解为若干个相互重叠的子问题

2)分析:分析问题是否满足最优性原理,找出动态规划函数的递推式

3)求解:利用递推式自底向上计算,实现动态规划过程

图问题中的动态规划法

TSP问题

TSP问题是指旅行家要旅行n个城市,要求各个城市经历且仅经历一次,然后回到出发城市,并要求所走的路程最短。

以这道题为例,阐述一下解题思路

  1. 用反证法证明这道题符合最优性原理

    设起点为s,假设s、s1、s2、……、sp、s为最短简单回路,那么s1到s的最短路径仍然是s1、s2、……、sp、s,如果有s1、r1、r2、……、rq、s更短,则s、s1、r1、r2、……、rq、s为从s到s的最短简单回路,这导致矛盾

  2. 画决策树并找出动态规划递推式

    假设有四个节点0 1 2 3 递推式 表示从顶点i出发,令d(i,V')表示从顶点i出发经过V‘中各个顶点一次且仅一次,最后回到出发点i的最短路径,开始时V’=V-{i}

递推式要和问题相呼应,这是一个取巧点。

  1. 手工解题,发现规律-一般是自底向上计算,并记录计算结果 这题难在哪里,主要是
  • 找出递推公式
  • 发现规律,确定记录哪些结果。确定用什么方式记,使用表格还是数组,怎么查找这些值
  1. 64. 最小路径和 - 中等 代码 求解这道题啪啪打脸,按照模式走确实能求出正确解,但是超时了。因为规律那我用了自底向上的方法,时间复杂度和空间复杂度翻了一倍。其实按照自顶向下的方案更好一些。所以大家不要迷信模板,还是要保持灵活。当然,从另一个方面也说明这个模式是可行的,只不过看题的时候需要思考的更加通透一些。
  2. 剑指 Offer 47. 礼物的最大价值 - 中等 代码 该题为题1的变种

多段图的最短路径问题

设图G=(V,E)是一个带权有向连通图,如果把顶点集合V划分成k个互不相交的子集Vi(2<=k<=n,1<=i<=k),使得E中的任何一条边(u,v),必有u属于Vi,v属于Vi+m(1<=i<k,1<i+m<=k),则称图G为多段图,称s属于V1为源点,t属于Vk为终点。多段图的最短路径问题是求从源点到终点的最小代价路径。 多段图最短路径问题和TSP问题求解过程几乎一致

唯一的不同是是递推公式上:使用cost[n]作为存储子问题解的表格,cost[i]表示从顶点i到终点n-1的最短路径,

cost[i]=min{cij+cost[j]},ij为连接的两个点

可以看到记录中间结果值使用的是数组,但是TSP使用的是二维表格。

之所以产生这个问题的原因是:多段图的指定点到终点的过程是确定的,如顶点1肯定要经过3次到达终点,而且不会重复,导致只会对应一个最小值,所以使用一维数组存放,。但是TSP的顶点1,可能是从起点出发经历的第一个或者最后一个,对于处于的不同阶段都会有一个最小值,所以使用二维数组存放。

  1. 面试题 08.01. 三步问题 - 简单 代码 随手做道简单题
  2. 70. 爬楼梯 - 简单 代码 方案和第一题一样
  3. 120. 三角形最小路径和 - 中等 代码

组合问题中的动态规划法

0/1背包问题

给定n种物品和一个背包,物品i的重量是wi,其价值为vi,背包的容量为C。背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大?

递推公式:令V(i,j)表示在前i(1<=i<=n)个物品中能够装入容量为j(1<=j<=C)的背包中的物品的最大值,则可以得到如下动态规划函数

  1. V(i,0)=V(0,j)=0

    ​ V(i-1,j), j < wi

  2. V(i,j)=

    ​ max{ V(i-1,j), V(i-1,j-wi)+vi }, j >= wi

V(i,j)的第一个式子表明:如果第i个物品的重量大于背包容量,则装入前i个物品得到的最大价值和装入前i-1个物品得到的最大价值是相同的,即物品不能装入背包

第二个式子表明:如果第i个物品的重量小于背包的容量,则会有以下两种情况:(1)如果把第i个物品装入背包,则背包中物品的价值等于把前i-1个物品装入容量为j-wi的背包中的价值加上第i个物品的价值vi;(2)如果第i个物品没有装入背包,则背包中物品的价值就等于把前i-1个物品装入容量为j的背包中所取得的价值。显然取其中较大者。

分析:

0/1背包问题比图问题要复杂一些,因为又加上了容量的限制。但是展示的这个动态规划函数极好,V(i,j)表示的是i个物品在j容量下的最大值,很完美的说出了想要的结果,同时也清晰阐述出了代码的具体实现。

算法执行的时候,从0开始不断增大容量,查看将所有商品放入最大值会是多少。计算过程从前向后。其实这个问题也点名了动态规划的一个核心-记录子结果

如果有5个商品,重量分别为{2,2,6,5,4},价值分别为{6,3,5,4,6},背包容量为10,则计算过程中的二维表为: V[i,j]表示把前i个物品装入容量为j的背包中获得的最大价值。

判断把哪个商品放入背包的方法为:对容量为10的那列,如果下一列的值比上一列大,说明将商品放入。

  1. 剑指 Offer 63. 股票的最大利润 - 中等 代码

  2. 121. 买卖股票的最佳时机 - 简单 代码

这两道题也说明了我为啥以前说乐扣在专业性上可能弱一些,逻辑完全一致,但是难度等级却不一样。

背包问题我在乐扣上没有找到合适的例题,而且背包问题其实相对复杂一些,难度应该放在困难上。上面的两道题,多少有点相仿,而且也再次证明了动态规划的核心还是记录子结果。后面会找一些困难的题来做一下。

最长公共子序列问题

对给定序列X=(x1,x,2,……,xm)和序列Z=(z1,z2,……,zk),Z是X的子序列当且仅当存在一个严格递增下标序列(i1,i2,……,ik),使得对于所有j=1,2,……,k,有zj=xij(1<=ij<=m)。如序列X=(a,b,c,b,d,a,b),序列(b,c,d,b)是X的一个长度为4的子序列,相应的递增下标序列为(2,3,5,7)。

给定两个序列X和Y,当另一个序列Z即是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。最长公共子序列问题就是在序列X和Y的公共子序列中查找长度最长的公共子序列。

z(i,j)表示X的前i个字符和Y的前j个字符情况下的最大子串,j为row,i为col

​ z(i-1,j-1)+1 xi==yj ,左上角

z(i,j) =

​ max{z(i-1,j),z(i,j-1)} xi!=yj

这个递推公式是我自己想出来的,这么看对动态规划的了解更加深入了一些。

  1. 1143. 最长公共子序列 - 中等 代码

查找问题中的动态规划法

最优查找二叉树

设{r1,r2,……,rn}是n个记录的合集,其查找概率分别是{p1,p2,……,pn},最优二叉查找树是以这n个记录构成的二叉查找树中具有最少平均比较次数的二叉查找树,即sum(pi*ci)最小,其中pi是记录ri的查找概率,ci是在二叉查找树中查找ri的比较次数。

设T(i,j)是由记录{ri,……,rj}(1<=i<=j<=n)构成的二叉查找树,C(i,j)是这棵二叉查找树的平均比较次数,则动态规划函数

这道题难点主要在于推导出该动态规划函数,一旦推导出该函数后,剩下的流程和上面的题目便一致了。

乐扣上没有符合的算法题,就先不练习了,其实推导出动态规划函数之后,整个计算逻辑已经相对明确了。

近似串匹配问题

设样本P=p1p2……pm,文本T=t1t2……tn,对于一个非负整数K,样本P在文本T中的K-近似匹配是指P在T中包含至多K个差别的匹配。这里的差别是指下列三种情况之一:

1)修改:P与T中对应的字符不同

2)删去:T中含有一个未出现在P中的字符

3)插入:T中不含有出现在P中的一个字符

定义D(i,j)(0<=i<=m,0<=j<=n)表示样本前缀子串p1……pi与文本前缀子串t1……tj之间的最小差别数,递推公式为:

其实这道题的思路和最长公共子序列的思路是一致的,如果大家真正掌握了这种类型的题的话,这个递推公式自己可以推导出来。

这种重复的题我就不写了,另外乐扣上也确实没有合适的题目。

总结

终于搞完了动态规划篇。以前上大学的时候,挺怕动态规划的题目的,因为自己确实没有完全理解。不过幸运的是,这次重新学习之后,我给自己的定位是:总算入门了。

归根结底,动态规划也是按照套路走的:

  1. 反证法证明满足最优性原理
  2. 推导出动态规划函数 - 这是最重要的一点
  3. 记录中间过程

帮助点有

  1. 核心中的核心是记录中间过程
  2. 动态规划函数在推导前要确认该函数满足题目的解
  3. 可以画图来帮助做动态规划函数的推导

至于习题的话,建议本篇文章里的题目,大家都做一下,而且自己思考出动态规划函数。希望每一个同学都能学会这个算法。

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

算法

  1. 算法学习计划
  2. 蛮力法
  3. 分治法
  4. 减治法
  5. 动态规划法

技术

  1. 微服务之服务框架和注册中心
  2. Beego框架使用
  3. 浅谈微服务
  4. TCP性能优化
  5. 限流实现1
  6. Redis实现分布式锁
  7. Golang源码BUG追查
  8. 事务原子性、一致性、持久性的实现原理
  9. CDN请求过程详解
  10. 记博客服务被压垮的历程
  11. 常用缓存技巧
  12. 如何高效对接第三方支付
  13. Gin框架简洁版
  14. InnoDB锁与事务简析

读书笔记

  1. 敏捷革命
  2. 如何锻炼自己的记忆力
  3. 简单的逻辑学-读后感
  4. 热风-读后感
  5. 论语-读后感

思考

  1. 对项目管理的一些看法
  2. 对产品经理的一些思考
  3. 关于程序员职业发展的思考
  4. 关于代码review的思考
  5. Markdown编辑器推荐-typora