动态规划的基本思路:动态规划使用分而治之的策略,但是具有针对性。此种策略由理查德.贝尔曼(Richard E. Bellman)于1957年在dynamic programming一书中提出。同年Bellman和Lester Ford一起设计了图论中最短路径的Bellman-Ford算法。
动态规划和贪心算法的区别:
http://blog.csdn.net/penzo/article/details/6001314
http://hi.baidu.com/abcdcamey/item/0d1d6746c9ef4616896d10ac
http://www.360doc.com/content/11/0522/11/6173582_118519858.shtml
动态规划的特点:1 分析一个最有解决方案应该具备的结构(能否使用动态规划策略);2递归定义最有解决方案(状态转移方程);3 由底至上构建一个最优解决方案(备忘录构建)
动态规划的几个例子:
补充(以下例子全是基于备忘录的动态规划)
流水装配线问题:问题描述:产品从“出发”到“到达”要经过流水线,经过n个步骤,每个步骤可以有两种选择:S1,i 和S2,i;图1中Ci,j Xi,j是花销;要求产品总花销最少。(扩展为和图论算法最短路径的关系)
最基本的想法:类似于组合数学上过程相乘的问题,每一个相乘的元素有2中状态,所以生产的路径数一共有2n种,程序复杂度为指数时间复杂度,空间复杂度为n。实际操作中会发现,当固定k种处理组合方式,计算其他步骤取不同值时,相同处理方式的花销组合会重复计算。
动态规划方法:一个问题是 假如整体最优解通过S1,j状态,那么从出发点到S1,j和S1,j到到达点两段路径是不是也是最优解(最短路径)?可以使用反证法证明:两段路径也是最优解。这样的话我们就可以把大问题分解为子问题。在分解的时候我们从出发点开始累计计算最优花销,也可以从到达点开始计算最优花销,这里我们采用前者,这样符合直观顺序。考虑其中的状态转移过程,当进入S1,j状态时,要么从S1,j-1要么从S2,j-1流入。那么到达S1,j时的处理花销C(S1,j)=C(S1,j-1)+C1,j-1 或者C(S1,j)=C(S2,j-1)+C2,j-1 +X2,j-1,最优解时取两者中的最小值。下面来推算下时间复杂度。
如果不适用备忘录来保存状态信息,来分析状态转移方程的递归树。假设n=3,有如下递归树:
可以看出递归树仍然是指数级。然而可以发现图中有较多的重叠计算子问题,所以以下所有算法均保存了各个状态信息,从而减少时间复杂度。
我们要计算所有的状态集,即{Si,j}一共2n个,在计算每个状态集中的元素时,我们至少需要计算四次加法和一次比较运算,至多也是常数次运算,所以整体的时间复杂度为O(2n*5)=O(n),线性算法。在计算到到达点时也就获取了最优路径。空间复杂度,我们要记录每个状态所对应的花销,以及最优路径,所以一共需要2n+n个单位空间(其实在计算到第j+1个步骤时,前面j-1个状态花销空间可以释放掉,这可以作为一个优化技巧)。
如何理解状态转移方程的表达式和时间复杂度的关系?程序基本上可以基于状态转移方程写出来:1 初始化所有的Si,j为最大值;2执行状态转移方程,如下伪代码所示。其中r1(j) r2(j)是记录上一步的位置,用于输出最优路径。
for(int j=0;j<=n;++k) {
if (C(S1,j-1)+C1,j-1>C(S2,j-1)+C2,j-1 +X2,j-1)
{
C(S1,j)=C(S2,j-1)+C2,j-1 +X2,j-1;
r1(j)=2;
}
if (C(S2,j-1)+C2,j-1>C(S1,j-1)+C1,j-1 +X1,j-1)
{
C(S2,j)=C(S1,j-1)+C1,j-1 +X1,j-1;
r2(j)=1;
}
}
最长公共子序列(Longest common subsequence problem):一个数列 ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 称为已知序列的最长公共子序列。
普通算法:假设序列1有m个元素,序列2有n个元素,第一个序列的子序列有2m种,当将其中一个子序列和序列2比对时,时间复杂度为O(n),所以整个算法的时间复杂度为O(n2m),空间复杂度为O(m)
动态规划算法:首先验证是否有最有子结构;这个第一眼还是需要gut的,然后才会认真分析具体过程,在寻找状态转移方程时最困难的是寻找变量,确定状态集。上个例子中选取的变量是步骤数和流水线编号,而该问题中涉及到两个对象(序列),不妨猜测需要建立两个变量i,j,只是目前还不清楚这两个变量到底是什么含义。思考问题从最基本的状态变化开始,并从上例中递增变量的变化得到启发,我们设i为序列1前缀,j为序列2前缀。那么两个前缀的LCS c[i,j]是不是整体LCS c[m,n]的一个最有子结构呢?同样适用反证法可以证明是的。那么状态转移方程为:
公式具体含义省略,因为介绍LCS的文章很多。下面分析其复杂度。当从c[1,1]开始构建c[i,j]时,一共最多有mn个状态,而在计算每个状态的LCS时,需要进行常数次运算(2次比较,一次相加),所以时间复杂度为o(3mn)=O(mn),空间上需要保持每个状态的LCS,所以需要O(mmn),如果单纯计算长度,则是O(mn).
这里再探索几个问题:实际上LCS问题和装配线问题都是在两个对象上变化,区别在装配线的步骤只能进行一次,而LCS问题在每个字符处都要与另一个序列的字符进行比较,所以时间复杂度上 前者是 加(n+n), 而后者是 乘 (mn). 另外为什么状态转移方程的变化是增1减1,而不是增2减2?我曾幻想着能够增减一半长度(听上去肯定是无稽之谈了,不过思考无边界,想象无极限,懒惰造英雄 哈哈),其实有些结论是和具体问题相关的,如果问题中的条件限制了或暗含着每次变化是转移两步,那么状态转移方程就会增减2了。我试图寻找和制造出一个增减大于1步的优秀例子,不过目前还没想好 。
最优二叉搜索树(Optimal Binary Search Tree):
二叉查找树(Binary Search Tree),或者是一棵空树,或者是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
而最优表现为搜索成本最低的二叉查找树。给定键值序列{k1,...,ki,...,kn},其中键值ki被搜索的概率为pi,则搜索期望成本最低。
普通算法:穷举每一种二叉搜索树,我们来计算对于n个键值,所对应的二叉搜索树的种类,由卡特兰数可知,n个元素所构成的二叉搜索树种类有 (h(n)=bn)右图来自算法导论12.4思考题及答案。
可以看出使用普通算法需要指数级的时间复杂度,空间复杂度为O(n)。
动态规划算法:仔细分析BST,如果BST是最优的,那么其子树也是一个最优BST,所以满足最优子结构条件,一个子树会包括ki~kj个键值,所以一个子问题是计算子树的查找花销状态e[i,j]。e[i,j]如果选取kr作为树根,会有最优左子树e[i-1,r]和最优优子树e[r+1,j],则最优子问题的基础是一步步的计算状态e[i,j],从e[1,2],e[2,3]到e[1,3],e[2,4]到。。。到e[1,n]。在状态转移方程中,一共有n2个状态,在计算每个状态的花销时,需要进行(j-i)次计算,用于寻找根节点,所以时间复杂度为O(n*n*(j-i))=O(n3).程序设计中最外层循环是状态中i与j的差,接着两层循环分别对i和j遍历。空间复杂度:至少需要保存每个状态的花销O(n*n),还需要保存每个状态的根节点,最多O(n),所以空间复杂度为O(n*n)。
这里有几个问题:1 算法之道上说随机构造BST(十分接近最优BST),需要O(nlogn)时间复杂度,?
2考虑k叉树,也满足最优子结构条件,区别在于状态转移方程中,需要找出2个根节点r1,r2,那么时间复杂度就变成了O(n*n*(j-i-r1)*(r1))=O(n4),仍然是多项式的时间复杂度。
矩阵连乘:问题定义:
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2,…,n-1。考察这n个矩阵的连乘积A1A2…An。由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定,则可以依此次序反复调用2个矩阵相乘的标准算法(有改进的方法,这里不考虑)计算出矩阵连乘积。若A是一个p×q矩阵,B是一个q×r矩阵,则计算其乘积C=AB的标准算法中,需要进行pqr次数乘。矩阵连乘积的计算次序不同,计算量也不同。如果对矩阵加括号,等价于修改了计算次序,所以最优的计算顺序就是最优的加括号方式,最优的一维数组分割,这又有点像是最优BST了。
普通算法:计算所有的计算次序,那么一个n个序列一共有多少个计算顺序呢?不妨转化为加括号的方法数,这样又用到了卡特兰数,参见上个链接,即使不清楚具体多少种,也可以确定是指数级。
动态规划算法:首先检验是否满足最优子结构?满足。 基本状态是m[i,j],即计算Ai~Aj的花销。状态转移方程是m[i][j]=m[i][k]+m[k+1][j]+pipk+1pj+1,其中k取j-i个值。状态数有O(n*n)个,对于每一个状态,需要计算j-i次,所以整体的时间复杂度是O(n3)。空间复杂度为O(n2)。
这里的几个问题:
1 单纯看状态转移方程,类似于在一维数组上的最优化,和最优BST很相似:一维数组,而且数据顺序都不能交换(矩阵无交换性,查找树数据放置有特定顺序)。
邮局选址:有一条公路经过V个村庄,每一个村庄都处在整数的坐标点上(这里假设公路拉直为X轴).规划在这条公路上建立P个邮局,当然为了方便,这些邮局应建在某P个村庄上,但是要求让不同村庄的人到邮局要走的总路程最小。
普通算法:建立邮局的种类:从V个村庄挑出p个村庄,然后计算路径,选择最短路径Cpv。计算路径的时间复杂度为v*p
动态规划算法:乍一看很难看出来具有最优子结构性质。问题中有两个变量:村庄数,邮局数,固定村庄数,依次递增邮局数,貌似没办法从中发现最优子结构,当把邮局数从1增加到2时,难以找出其中的状态转移法则;固定邮局数,递增村庄数,当增加一个村庄时,可能由于村庄位置距离已有村庄超级远,所以会改动之前的邮局位置,所以不具有最优子结构;现在变化两个变量,m[i,j]表示在前i个村庄建立j个邮局的最短路径,产生于在前k个村庄建立j-1个邮局的最短路径m[k,j-1]和在k+1到i个村庄之间建立1个邮局的最短路径w[k+1,i],即在前i个村庄找到一个划分k,使得K个村庄之后的村庄使用新建的邮局。下面来说明最优子结构m[k,j-1]性质。倘若m[k,j-1]不是最短路径,因为此处的k是取最小值的选择,则m[i,j]不是最短路径。一共有v*p个状态,状态转移方程中,为找到k,需要进行i次划分,所以时间复杂度为O(v*p*i)=O(v3)次路径计算次数。每次路径计算复杂度为v*p。空间复杂度:一共有v*p个状态最优路径值保存,每个状态记录最后面的一次划分,所以空间复杂度为O(v*p)
这里的几个问题:
和前两个例子类似,都属于区域划分问题。
这个好奇葩,状态数O(n5),在每个状态里,计算最佳边界O(n2),时间复杂度O(n7)
我试图把图论算法中的动态规划算法也写到这一篇中,但是发现内容太多了。关于动态规划就这样把,很多东西需要在对比中发现本质。图论算法另外总结一篇,到时候附上连接。
另外其他有趣的例子持续更新中
Somebody recommends me to read the book "Introduction to Algorithms:A Creative Approach" chapter 5. He thinks this part can solve my problem. Thanks him(a handsome guy , M.E. Candidate in HFUT)!!!