dp入门——由分杆问题认识动态规划
简介
如果你常刷leetcode,会发现许多问题带有Dynamic Programming的标签。事实上带有dp标签的题目有115道,大部分为中等和难题,占所有题目的12.8%(2018年9月),是占比例第二大的问题。

如果能系统地对dp这个topic进行学习,相信会极大地提高解题速度,对今后解决实际问题也有思路上的帮助。
本文以分杆问题为切入点,介绍动态规划的算法动机、核心思想和常用的两种实现方法。
分杆问题
The rod-cutting problem(分杆问题)是动态规划问题的一个典例。
给出一根长度为n(n为整数)的杆,可以将杆切割为任意份长短不同的杆(其中包含完全不进行切割的情况),每一份的长度都为整数。给出一个整数数组p[],p[i]表示长度为i的杆的价格,问如何对杆进行切割可以使利润最大。
p[]数组的一个示例如下:

思路
在长度为n的杆上进行整数切割,共有2n-1种情况,因为有n-1个点可以选择是否切割。
将这些可以切割的点编号为1,2,3, ..., n-1,如果先试着在1处切割,则杆变成了长度为1和n-1的两段;如果试着在2处切割,则杆变为了长度为2和n-2的两段,以此类推,共有n种切法(包含完全不作切割)。这样,我们迈出了递归的第一步,即把长为n的杆的最优切割分成两个子问题:长为i的杆的最优切割和长为n-i的杆的最优切割(i = 1,2,...,n)。最终的利润为两个子杆的利润和。
如果用fn表示长度为n的杆切割后能得到的最大利润,经过以上分析,我们求取两个子杆的利润和的最大值即可。即
fn = max(pn, f1 + fn-1, f2 + fn-2, ..., fn-1 + f1).
这种思路是正确的,但不是太好,有心人可以注意到子问题之间有较大的重叠之处,比如计算fn-1时会需要查看f1 + fn-2,即f1 + fn-1这个子问题需要查看f1 + f1 + fn-2这个切法;而计算f2时又需要查看f1 + f1,即f2 + fn-2这个子问题也会查看到f1 + f1 + fn-2这个切法,相当于把一些可能性重复查看了多遍。
一个更简洁合理的思路是:设定左边这个长为i的杆不可再切割,只有右边长为n-i的杆可以再切割。则问题变为
fn = max(pi + fn-i), i = 1,2,...,n
传统递归实现
按照上面的分析,可以初步做一个递归实现如下:
int cutRod(int n, int[] p){
if(n == 0)
return 0;
int max = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++)
max = Math.max(max, p[i] + cutRod(n - i, p));
return max;
}
传统递归实现的复杂度
在节点n,算法的时间复杂度为
Tn = 1 + ∑ Ti (i = 0,1, ..., n-1)
(其中的1是在节点处做加法和max运算的常数复杂度)
这个式子很好推算,只要将Ti的值以此从后往前代入即可:
Tn = 1+T0+T1+ ... +Tn-1 = 1+T0+T1+ ... +Tn-2+(1+T0+T1+ ... +Tn-2)
= 2 (1+T0+T1+ ... +Tn-2) = 2 (1+T0+T1+ ... +Tn-3+(1+T0+T1+ ... +Tn-3))
= 22 (1+T0+T1+ ... +Tn-3) = ... (总结规律) = 2n-1 (1 + T0)
= 2n
即传统递归算法的时间复杂度为O(2n),为指数级别。
上一节中说到切割的可能性共有2n-1种,也就是说递归算法会将每种可能性都遍历到。是否还有优化的可能性呢?
优化
以n = 4为例,画出递归树结构(节点包含的数字为n的值)

本图摘自算法导论(英文版)3rd Ed. 15.1 P346
可以看到子树之间存在重叠情况。最明显的是n = 2的子问题和n = 3的子问题调用的子树完全相同,进行了两遍同样的计算。而这个子树中又包含n = 1的子树,也就是说浪费的幅度是相乘的。
一个优化思路是将每个子问题的计算结果记录下来,下一次再遇到同样的问题时直接使用记录值,这就是动态规划的核心思想。
动态规划
如上节所述的,动态规划是一种“以空间换时间”的思想,适用于子问题之间存在重叠情况的优化问题。它的基本思想是将计算过的子问题的答案记录下来,从而达到每个子问题只计算一次的目的。
动态规划的实现方法分为top-down和bottom-up两种,可以理解为前者从递归树的根节点向下递归调用,而后者从树的叶结点开始不停地向上循环。
top-down with memoization
top-down方法比较容易理解,就是在传统递归的基础上加入memoization(注意与memorization的区别。memoization来自memo,有备忘的意思),即用数组或表等结构缓存计算结果。在每次递归运算时,先判断想要的结果是否在缓存中,如果没有才进行运算并存入缓存。
int cutRod(int n, int[] p){
int[] memo = new int[n + 1];
for(int i = 0; i < memo.length; i++)
memo[i] = Integer.MIN_VALUE; //initialization
return cutRod(n, p, memo);
}
int cutRod(int n, int[] p, int[] memo){
if(memo[n] != Integer.MIN_VALUE)
return memo[n]; //return value directly if memoized
if(n == 0)
return 0;
int max = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++)
max = Math.max(max, p[i] + cutRod(n - i, p, memo));
memo[n] = max; //memoize it
return max;
}
bottom-up with tabulation
相比于top-down,bottom-up的特点是使用循环而非递归,先解决子问题,再利用子问题的答案解决父问题。tabulation也很好理解,即用一个表格存放子问题的答案,然后查表获得父问题需要的所有信息去解决父问题,解决后也填在表中,直至把表填满。
事实上,dynamic programming这个令人费解的名字即来源于此。programming在数学界有“列表法”(tabular method)的意思,指的是为了求某函数的最大/最小值,将函数的所有变量的所有可能值列在表中并对表进行某些操作来获得结果。在这里,表格是“静态”的,每个格子中的信息是独立的;而在动态规划中,表格是“动态”的,一些格子中的信息依赖于另一些格子中的计算答案。所以,dynamic programming也可以理解为“动态列表法”,也即此处的tabulation。
top-down的实现如下:
int cutRod(int n, int[] p){
int[] table = new int[n + 1];
for(int j = 1; j <= n; j++){ //fill table from j = 1 to n
int max = Integer.MIN_VALUE;
for(int i = 1; i <= j; i++)
max = Math.max(max, p[i] + table[j - i]); //calculate f(j)
table[j] = max;
}
return table[n];
}
复杂度
在bottom-up解法中,我们从1至n填入表格,在填入table[j]时,需要查询table[j-1]到table[0]的所有元素,即要做j次查询。则填满表格共要做1+2+3+...+n = O(n2)次查询。则bottom-up解法的时间复杂度为O(n2)。
在top-down解法中,可以这样分析复杂度:首先由于缓存机制,每个子问题只会被计算一次;为了解决大小为n的问题,我们需要计算大小为0,1,2,...,n-1的问题(第15行);计算大小为n的问题又需要n次计算(第14行),因此top-down解法的复杂度也为O(n2)。
实际上,动态规划将前文图中的递归树做了简化,将互相重叠的子树合并,得到了一个子问题树。子问题树中的边和节点都减少了,意味着时间复杂度得到了优化。
-> 
概念
看完例子,我们来总结一下动态规划算法的相关概念。
Dynamic Programming
- dynamic programming (动态规划)是一类常用来解决优化问题的算法。它的最大特点是使用子问题的信息帮助解决父问题,使解题的难度减小。比如,求第1,000,002个斐波那契数这个问题看起来很复杂,但如果已知了第1,000,000和第1,000,001斐波那契数,事情就简单多了。
- 动态规划问题有一个显著的特点,就是子问题之间存在相互重叠。动态规划通过记录子问题的结果,保证每个子问题只计算一次,减少了时间浪费。动态规划的时间复杂度通常是多项式复杂度(即O(nk),k为非负常数),而不记录结果的算法由于重复计算,复杂度通常远高于动态规划,达到指数复杂度。
- dynamic programming这个英文名词有些让人难懂。实际上,这里的programming不是指编程,而是数学上的一种解决优化问题的方法,叫做列表法(tabular method),大致过程是将函数的不同变量值在表中列出并对表进行各种操作来求得结果。如果列表法是静态(static)的,则动态规划算法中,表格是慢慢增长的,先解决相对简单的子问题,然后通过子问题的结合求取父问题,这样表格好像是“动态”的。这就是dynamic programming的意思。
Memoization vs. Tabulation 简介
- 动态规划通常有两种解法:top-down和bottom-up。
- top-down通常以递归形式出现,从父问题开始,递归地求解子问题。top-down的求解过程通常与memoization结合,即将计算过的结果缓存在数组或者哈希表等结构中。当进入递归求解问题时,先查看缓存中是否已有结果,如果有则直接返回缓存的结果。
- bottom-up通常以循环形式出现。bottom-up的求解过程通常与tabulation结合,即先解最小的子问题,解决后将结果记录在表格中(通常是一维或二维数组),解决父问题时直接查表拿到子问题的结果,然后将父问题的结果也填在表中,直到把表格填满,最后填入的就是起始问题的结果。
参考资料
dynamic programming -- wikipedia
What is dynamic programming? -- Stackoverflow
Tabular Method of Minimisation
Dynamic programming and memoization: bottom-up vs top-down approaches
dp系列下一篇:dp方法论——由矩阵相乘问题学习dp解题思路
dp入门——由分杆问题认识动态规划的更多相关文章
- 【专章】dp入门
动态规划(简称dp),可以说是各种程序设计中遇到的第一个坎吧,这篇博文是我对dp的一点点理解,希望可以帮助更多人dp入门. ***实践是检验真理的唯一标准,看再多文章不如自己动手做几道!!!*** 先 ...
- 树形dp 入门
今天学了树形dp,发现树形dp就是入门难一些,于是好心的我便立志要发一篇树形dp入门的博客了. 树形dp的概念什么的,相信大家都已经明白,这里就不再多说.直接上例题. 一.常规树形DP P1352 没 ...
- 【学习笔记】dp入门
知识点 动态规划(简称dp),可以说是各种程序设计中遇到的第一个坎吧,这篇博文是我对dp的一点点理解,希望可以帮助更多人dp入门. 先看看这段话 动态规划(dynamic programming) ...
- 【DP入门到入土】
DP例题较多,可以根据自己需求食用~ update:下翻有状压DP入门讲解,也只有讲解了(逃~ DP的实质,就是状态的枚举. 一般用DP解决的问题,都是求计数或最优问题,所以这类问题,我们也可以用搜索 ...
- poj 3254 状压dp入门题
1.poj 3254 Corn Fields 状态压缩dp入门题 2.总结:二进制实在巧妙,以前从来没想过可以这样用. 题意:n行m列,1表示肥沃,0表示贫瘠,把牛放在肥沃处,要求所有牛不能相 ...
- xbz分组题B 吉利数字 数位dp入门
B吉利数字时限:1s [题目描述]算卦大湿biboyouyun最近得出一个神奇的结论,如果一个数字,它的各个数位相加能够被10整除,则称它为吉利数.现在叫你计算某个区间内有多少个吉利数字. [输入]第 ...
- 【dp入门题】【跟着14练dp吧...囧】
A HDU_2048 数塔 dp入门题——数塔问题:求路径的最大和: 状态方程: dp[i][j] = max(dp[i+1][j], dp[i+1][j+1])+a[i][j];dp[n][j] = ...
- 数位dp入门 hdu2089 不要62
数位dp入门 hdu2089 不要62 题意: 给定一个区间[n,m] (0< n ≤ m<1000000),找出不含4和'62'的数的个数 (ps:开始以为直接暴力可以..貌似可以,但是 ...
- POJ 2342 树形DP入门题
有一个大学的庆典晚会,想邀请一些在大学任职的人来參加,每一个人有自己的搞笑值,可是如今遇到一个问题就是假设两个人之间有直接的上下级关系,那么他们中仅仅能有一个来參加,求请来一部分人之后,搞笑值的最大是 ...
随机推荐
- Expo大作战(四十)--expo sdk api之 Calendar,Constants
简要:本系列文章讲会对expo进行全面的介绍,本人从2017年6月份接触expo以来,对expo的研究断断续续,一路走来将近10个月,废话不多说,接下来你看到内容,讲全部来与官网 我猜去全部机翻+个人 ...
- asp.net学习——Response对象
(2011-03-29 07:33:03) 转载▼ 标签: 杂谈 分类: asp.net学习 响应的缓冲输出:为了提高服务器的性能,asp.net向浏览器Write的时候默认并不会每Write一次都会 ...
- 慕学在线网0.4_xadmin后台管理
admin是基于Django开发的后台管理框架,方便,快捷,而且简单: 而xadmin就相当于admin的升级版,更加强大. 1.安装xadmin(源码安装方式) 教程 PS: - 卸载pip安装的x ...
- Python之随机森林实战
代码实现: # -*- coding: utf-8 -*- """ Created on Tue Sep 4 09:38:57 2018 @author: zhen &q ...
- Spark内存分配诊断
1.JVM自带众多内存诊断的工具,例如:JMap,JConsole等,第三方IBM JVM Profile Tools等. 2.日志!在开发.测试.生产环境中最合适的就是日志,特别是Driver产生的 ...
- 关于SQL Server将一列的多行内容拼接成一行,合并显示在另外表中
select '['+title_a+','+title_b +']' from A for xml path('') SELECT *, (select '['+title_a+','+titl ...
- CSS:float: right 靠右换行的解决方法
1.float: right的使用用法:使用html代码<span style="float: right">*****</SPAN>,其中*****就是你 ...
- Nginx:413 Request Entity Too Large
现象:在 Post 文件的时候遇到413 错误 :Request Entity Too Large: 原因:Nginx 限制了上传文件的大小,需在Nginx中修改/增加允许的最大文件大小: 操作:编辑 ...
- Ubuntu 16.04 LTS 降级安装GCC 4.8
转载自https://www.linuxidc.com/Linux/2017-03/142299.htm Ubuntu 16.04 LTS 降级安装GCC 4.8 [日期:2017-03-28] 来源 ...
- [bug] 验证selenium的显式和隐式等待而发现的一个低级错误
隐式等待:如果在规定时间内网页加载完成,则执行下一步,否则一直等到时间截止,然后执行下一步.按照这说法举了个例子为啥不会按照预期执行了,难不成是这个定义有问题(~~~~~直接否定不是定义的问题,相信它 ...