动态规划总结
Posted Hearthougan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划总结相关的知识,希望对你有一定的参考价值。
动态规划(Dynamic Programming, DP)思想启发于分治算法的思想,也是将复杂问题化解若干子问题,先求解小问题,再根据小问题的解得到原问题的解。但是DP与分治算法不同的是,DP分解的若干子问题,往往是互相不独立的,这时如果用分治算法求解,那么会对重叠子问题重复进行求解,从而使得浪费大量的时间。那么如果我们保存已经计算过的子问题的解,这样当再次计算该子问题时,可以直接使用,这样可以节约大量的时间。
DP正是利用一个表记录所有已经求解过的子问题的答案,只要一个子问题被求解过,就将其答案保存起来,无论以后的计算是否会用到,这就是DP的基本思想。
设计动态规划的四个步骤:
1、刻画一个最优解的结构特征。
2、递归地定义最优解的值。
3、计算最优解的值,通常采用自底向上的方法。
4、利用计算出的信息构造一个最优解。
动态规划的应具备两个要素:
1、最优子结构性质。当问题的最优解包含了其子问题的最优解时,称该问题具有最有子结构性质;
2、重叠子问题性质。
首先看一个算法导论中第一个例子,钢条切割问题。
问题:给定一根长度为n英寸的钢条和一个价格表,求钢条的最佳切割方案,使得销售收益最大。
对于长度为n的钢条总共有种不同的切割方案。设距离钢条左端i(i = 1,2,....n-1)英寸处为可切割点,那么我们对每个切割点都可以选择切或者不切,则总共有种方法。因此暴力枚举每一种可能是不行的。
设长度为n的钢条的最大收益为(n>=1)。
1、最优子结构
对于长度为n的钢条,假设第一刀在i(i= 1,.....n-1)处切割,使得收益最大。得到的两个小段钢条长度为i,n-i,那么对于其收益和也是最大的。假设不是,即在长度为i和长度为n-i的钢条分别有更好的切割方案,使得对应的收益更大,那么将这两个更优切割方案的收益相加,会得到一个比在i处切割所得收益更大的方案,那么说明开始第一刀就不是最优的,与假设相矛盾!因此本问题具有最有子结构的性质。
2、递归地定义最优解的值
有一个更加简单明了的切割方法,假设我们最优的第一刀直接切出一个距离左端为i(i = 1, ....n-1)的钢条,这一段整体收益最大,即不再对这一段继续切割。我们只对剩余长度为n-i的钢条,继续切割,求其收益最大的切割方案,这样问题只化为一个更小的子问题。
由此,我们可以知道长度为n的递推公式为:
若i取n时表示整体不切割,收益最大,此时剩余部分n-i的收益为为0。
3、计算最优解的值
我们先根据2的递推公式,用递归的方式先求解,如下:
int CutRod(int p[], int n)
{
if(n == 0)
return 0;
int q = INT_MIN;
for(int i = 1; i <= n; ++i)
if(q < p[i]+CutRod(p, n-i))
q = p[i]+CutRod(p, n-i);
return q;
}
开始的时候,我们说过,对于切割点i(i = 1, ...., n-1),我们可以选择切或者不切,总共有种情况,由此我们可以知道该算法的时间复杂度为,(其实它枚举了每一种情况),这是一个指数级的算法,肯定是不行的。那么时间浪费在哪里了?
我们用当n=4时,做个例子,其递归过程如下:
每个节点表示子问题的规模,从父节点s到子节点t的边,表示从钢条的左端切下一段长度为s-t的小钢条,然后递归地求解剩余规模为t的子问题。
由上图可以知道,有很多相同的子问题,被大量的重复计算,从而浪费了大量的时间,那么如何解决这问题?我们可以用空间来换取时间,申请一张表,来储存每次新子问题的答案,这样下次再遇到相同的子问题,直接查表取得,而无需计算,从而可以节约大量的时间。
int CutRod(int p[], int r[], int n)
{
if(r[n] >= 0)
return r[n];
if(n == 0)
return 0;
int q = INT_MIN;
for(int i = 1; i <= n; ++i)
if(q < p[i]+CutRod(p, r, n-i))
q = p[i]+CutRod(p, r, n-i);
r[n] = q;
return q;
}
int MemoizedCutRod(int p[], int n)
{
int r[n+1];
for(int i = 0; i <= n; ++i)
r[i] = INT_MIN;
return
}
这是一个的算法,由此可以看出,算法的效率得到了大大的提高。
我们知道,每次递归调用函数,这也是需要时间花销的,我们上面的算法实现全部都是用自上而下的思想来求解最优值的,由1,我们知道子问题的最优解决定着原问题的最优解,那么可以从最小规模开始计算,这样当计算规模较大的问题时,所需的子问题已经全部计算出来,并且被记录下来,这样就可以直接拿来使用,这就自底向上的思想来求解最优值,该问题的自底向上方法的实现如下:
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main()
{
int n;
cin>>n;
int price[11] = {0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30};
int r[n+1];
for(int i = 0; i<= n; ++i)
r[i] = 0;
for(int i = 1; i <= n; ++i)//规模长度为i
{
int q = INT_MIN;
for(int j = 1; j <= i; ++j)//计算规模为i的最大收益
{
if(q < price[j] + r[i-j])//因为i>i-j,所以当计算r[i]时,r[i-j]已经解决,可以直接用
q = price[j] + r[i-j];
}
r[i] = q;
}
cout<<r[n];
}
4、利用计算出的信息构造一个最优解
如果只需求解最大收益,这么此步可以被省略,但是如果需要打印出来最佳的切割方案,那么原算法仍需稍微改动。我们另外申请一个数组label[],专门来存储最佳切割点的。其实现如下:
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main()
{
int n;
cin>>n;
int price[11] = {0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30};
int r[n+1];
int label[n+1];
memset(label, 0, sizeof(label));
for(int i = 0; i<= n; ++i)
r[i] = 0;
for(int i = 1; i <= n; ++i)//规模长度为i
{
int q = INT_MIN;
for(int j = 1; j <= i; ++j)//计算规模为i的最大收益
{
if(q < price[j] + r[i-j])//因为i>i-j,所以当计算r[i]时,r[i-j]已经解决,可以直接用
{
q = price[j] + r[i-j];
label[i] = j;
}
}
r[i] = q;
}
cout<<r[n]<<endl;
//打印最佳切割方案
while(n > 0)
{
cout<<label[n]<<" ";
n = n - label[n];
}
cout<<endl;
}
那么到此,我们利用DP的设计步骤全部实现钢条的切割问题,下面我们再看一个矩阵相乘的例子,这是把原问题分解为两个子问题,来求解矩阵相乘所需的最小次数的。请参考本人笔记:
http://blog.csdn.net/hearthougan/article/details/25834141
由这两个问题我们可以更加深刻地理解DP的两个要素。何时需要动态规划,主要也是考察问题是否具备这两个要素,现在分析一下这两个要素:
I、最优子结构
这是动态规划求解最优解的第一步。最优子结构性质是:一个问题的最优解包含其子问题的最优解。发掘最优子结构性质的过程,遵循如下模式:
1、证明问题最优解的第一个组成部分是做出一个选择。如,选择钢条第一次切割的位置,第一次选择矩阵链的划分位置等。做出这次选择会产生一个或者多个待解的子问题。
2、对于一个给定问题,在其可能的第一步选择中,你假,定已经知道哪种选择才会得到最优解。现在无需关心这种选择是如何得到的,只是假定已经知道了这种选择。
3、给定可获得最优解选择后,确定这次选择会产生哪些子问题,以及如何更好地刻画子空间。一个刻画子问题空间的经验是:保持子问题空间尽可能简单,只在必要时才扩展它。
4、利用Cut-and-paste方法证明:作为原问题最优解的组成部分,每个子问题的解就是它本身的最优解。证明这一点,就是利用反证法:假定子问题的解不是其自身的最优解,那么我们可以从原问题中“剪切”掉这些非最优解,将最优解“粘贴”进去,从而得到一个原问题更优的解,这与最初的解释原问题的最优解的前提假设相矛盾。
对于不同的问题,最优子结构体现在两个方面:
1、原问题的最优解涉及多少个子问题
2、在确定最优解使用哪些子问题时,我们需要考察多少种选择。
II、重叠子问题
适合动态规划方法求解最优解的问题应该具备:子问题的空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。重叠子问题性质:递归算法反复求解相同的子问题。
与之对应的分治算法,其求解问题时,通常在递归的每一步都生成全新的子问题。而动态规划则是,把子问题的解存放在一张表里,在需要的时候直接取出,此步操作的时间复杂仅是常量时间,故而降低了时间复杂度!
以上是关于动态规划总结的主要内容,如果未能解决你的问题,请参考以下文章