动态规划原理

来源:互联网 发布:tcp对数据大小限制 编辑:程序博客网 时间:2024/06/02 01:55

    • 动态规划原理
      • 最优子结构
      • 重叠子问题
      • 重构最优解

动态规划原理

我们在之前的文章里面提到过两个问题:
钢条切割问题:http://blog.csdn.net/ii1245712564/article/details/44464689
矩阵链乘法问题:http://blog.csdn.net/ii1245712564/article/details/44464689

仔细观察这两个问题,我们发现他们都有比较明显的动态规划的两个大特征:

  • 最优子结构
  • 重叠子问题

最优子结构

在钢条切割问题中,我们知道长度为n的钢条是经过第一次切割后得到的两个子钢条的最优解组成的,而矩阵链乘法是由第一次将矩阵链分为两条规模更小的矩阵链,矩阵链乘法的最优解则是由这两个子矩阵链的最优解组成的。
实际中,我们在发掘一个问题的最优子结构过程中可以遵循以下模式:

  1. 在面对原问题时,首先需要做出一个选择,比如在钢条切割中,我们选择一个切割点,在矩阵链中,我们选择括号化的点。
  2. 在第一步的选择里面,我们假定那种选择会得到最优解。我们并不关心这种选择是怎么来的,只是假定已经知道这种选择,比如在切割问题里面,我们面对长度为n的钢条,我们假设切割点为k*(k>0 && k<= n)*
  3. .在做出最优选择之后,我们需要判断这样选择会产生那些子问题,以及如何更高的刻画子问题空间。
  4. 利用“剪切-粘贴”技术证明:假设构成原问题最优解的子问题的解并不是子问题的最优解,那么我们可以将这个非子问题最优解剪切出来,并将子问题最优解粘贴进去,从而等到一个更优的解,这与最初的假设矛盾,所以构成原问题的子问题的解一定是子问题的最优解!

对于不同问题领域,最优子结构的不同体现在两个方面:

  1. 原问题的最优解涉及到多少个原问题的解
  2. 在确定使用那些子问题最优解构成原问题最优解中,我们有多少种选择

比如在钢条切割中,长度为n的钢条一共有n个子问题,因为每个长度的钢条都被涉及,对于长度为n的钢条,我们有{1,n-1},{2,n-2},…,{n,0}种子问题的选择方法,最优我们会从里面选出一个最优的解。
我们可以通过子问题总数和每个子问题有多少种选择来粗略的估计算法的运行时间,比如钢条切割子问题总数为O(n),且每个子问题有O(n)种选择,所以钢条切割运行时间为O(n^2).

下面我们来看两个问题,看看它们是否具有最优子结构:
给定一个有向图G(V,E)和图上的两个顶点u,v。
问题一:无权最短路径:找到u到v的边数最少的简单路径(无环)。
问题二:无权最长路径:找到u到v边数最多的简单路径路径(无环)。

下面我们来证明无权最短路径具有最优子结构:

  1. 第一步,选择:在u到v的路径上选择一个节点w。
  2. 第二步,假设:假设w是在u到v最短路径上的一个点。
  3. 第三步,子问题:在这里就产生了两个子问题:u~w,w~v
  4. 第四步,证明:我们假设u~w走的路径不是u~w的最短路径,那么我们可以将这个u~w路径剪切出来,将u~w的路径粘贴进去得到一个更优的最短路径,这与原问题假设矛盾,故原问题的最优解是由子问题的最优解构成的!

那么既然有了最短路径的证明,那我们还可以按照上面的方法来证明无权最长路径的问题也是具有最优子结构的么?just do it
1. 第一步,选择:在u到v的路径上选择一个节点w。
2. 第二步,假设:假设w是在u到v最长路径上的一个点。
3. 第三步,子问题:在这里就产生了两个子问题:u~w,w~v

那么上面的u~w或者w~v的路径就一定是对应两点之间的最长路径么?
我们看一下下面这张图:
这里写图片描述

我们知道q到t的最长简单路径就是q->r->t,那么q->r的路径是q到r的最长路径么?我们知道q到r的最长路径是q->s->t->r,而构成原问题的子问题的q到r的路径是q->r却不是最长路径,所以这里并不满足最优子结构。
为什么最长简单路径问题的子结构与最短路径有这么大的区别?原因在于虽然最长路径问题与最短路径问题都可以划分为两个子问题,但是最长路径问题的子问题是彼此相关的,而最短路径的子问题是彼此无关的。这里子问题无关的含义就是,同一个原问题的一个子问题的解不影响另一个子问题的解,这个怎么理解呢?我么看上面最长路径问题,可知在求解q到r最长路径得到q->s->t->r,那么求解得r到t的最长路径为:r->q->s->t,但是为了保证是简单路径,那么在q到r的子问题里面已经使用了s,t,那么在r到t的子问题里面就不能再使用s,t了,但是r到t的子问题没有t怎么行呢?这使得两个问题之间互相影响,第一个子问题用到的节点导致第二个子问题不能使用,不能完成分析!
那为什么最短路径的子问题是无关的呢?首先我们假设最短路径的子问题是相关的,那么他们之间就一定会有公共的点,我们再这里设这个点为p,于是u~w,w~v两个子问题我们可以写成u~p~w,w~p~v,可见p~w与w~p这一段是重复的,那么我们现在可以找到一个比原来最短路径e更短的路径e-2(p到w之间的距离),产生矛盾!

重叠子问题

适用于动态规划的第二个特征就是子问题空间足够小,即问题的递归算法会重复的求解相同的子问题,而不是一直产生新的子问题,如果递归算法反复的求解子问题,那么我们就将这个性质定义为重叠子问题
既然一直在求解相同的子问题,那么我们倒不如将已经求解过得子问题的解保存起来,到下次遇到相同子问题的时候,我们直接返回已经算好的子问题的解。
我们来看一下矩阵链乘法的子问题性质:现在假设有[1…4]的矩阵链,开始选择一个括号化的位置,划分子问题:
这里写图片描述

通过观察上图我们发现,深色部分都是有重复过的子问题,如果我们不加备忘,蛮力求解每一个子问题的话,那将是很慢的!在动态规划中,我们就有两种方法来避免重复求解相同子问题:

  1. 第一种就是自上而下带备忘的方法,在求解每个子问题之前,我们先检测一下这个子问题之前有没有算过,若果有,那么不用计算直接返回结果,如果没有,那么就计算这个子问题,之后将结果保存起来,方便下次再遇到时使用。
  2. 第二个就是自下而上的构建原问题的解,首先求解最基本的情况,再从最基本的情况一部一部的向上求解,比如我要求解[2…4],那么我首先需要知道[2…2][3…4]和[2…3][4…4]的最优解,需要知道[3…4],那么首先需要知道[3…3][4…4]的最优解,所以,倒不如我们将原问题需要的解先构建出来,再慢慢向上一层一层的构建,最后组成原问题的解!

上面两个方法各有各的好处。

  • 自上而下的优点是并不需要求解每一个子问题的解,而是只求解有需要的子问题的解,缺点就是需要递归调用,函数调用浪费时间。
  • 自下而上的优点是并不需要递归调用,每个子问题求解的速度较快,缺点每个子问题都要计算,就算这个子问题的解对原问题的解并没有任何帮助!
    下面我们来看一段代码!LCS的自上而下和自下而上的解法:
/** 这里采用自底向上的算法实现 */#include <iostream>#include <vector>#include <climits>using namespace std;unsigned int dealLCS(std::vector<char>  X , std::vector<char>  Y ,             std::vector<std::vector<unsigned int> > & tempData);/** * 计算最长公共子序列长度 * @param  X X序列 * @param  Y Y序列 * @return   最大长度 */size_t LCS(std::vector<char> &X , std::vector<char> &Y){    if(X.size() == 0 ||  Y.size() == 0)        return 0;    /** 创建一个数组来保存临时数据 */    std::vector<std::vector<unsigned int> > tempData(X.size()+1 , std::vector<unsigned>());    for (int i = 0; i <= X.size(); ++i)    {        for (int j = 0; j <= Y.size(); ++j)        {            if(i == 0 || j == 0)                tempData[i].push_back(0);            else                tempData[i].push_back(UINT_MAX);        }    }    dealLCS(X , Y , tempData);//开始进行计算    return tempData[X.size()][Y.size()];}/** * 实际计算LCS的最大长度 * @param X        X序列 * @param Y        Y序列 * @param tempData 缓存数据 */unsigned int dealLCS(std::vector<char>  X , std::vector<char>  Y ,             std::vector<std::vector<unsigned int> > & tempData){    if(X.size() == 0 || Y.size() == 0)        return 0;    for (unsigned int i = 0; i < X.size(); ++i)    {        for (unsigned int j = 0; j < Y.size(); ++j)        {            if(X[i] == Y[j])            {                tempData[i+1][j+1] = tempData[i][j]+1;            }            else            {                unsigned int temp1 = tempData[i+1][j];                unsigned int temp2 = tempData[i][j+1];                tempData[i+1][j+1] = temp1 > temp2 ? temp1 : temp2;            }        }    }    return tempData[X.size()][Y.size()];}int main(int argc, char const *argv[]){    char array1[]={'A','B','C','B','D','A','B'};    char array2[]={'B','D','C','A','B','A'};    std::vector<char> X(array1 , array1+sizeof(array1));    std::vector<char> Y(array2 , array2+sizeof(array2));    cout<<"The Lcs length is :"<<LCS(X,Y)<<endl;    while(1);    return 0;} 

我们观察一下tempData数组里面每一个位置的情况:
这里写图片描述
发现里面每一个数字都不是初始化值INT_MAX,tempData[i][j]就是[i…j]矩阵链的解,我们看出来每一个子问题都被求解出来了!

那么我们看一下自上而下的解法:

/** 这里采用自顶向下的算法 */#include <iostream>#include <vector>#include <climits>using namespace std;unsigned int dealLCS(std::vector<char>  X , std::vector<char>  Y ,             std::vector<std::vector<unsigned int> > & tempData);/** * 计算最长公共子序列长度 * @param  X X序列 * @param  Y Y序列 * @return   最大长度 */size_t LCS(std::vector<char> &X , std::vector<char> &Y){    if(X.size() == 0 ||  Y.size() == 0)        return 0;    /** 创建一个数组来保存临时数据 */    std::vector<std::vector<unsigned int> > tempData(X.size()+1 , std::vector<unsigned>());    for (int i = 0; i <= X.size(); ++i)    {        for (int j = 0; j <= Y.size(); ++j)        {            if(i == 0 || j == 0)                tempData[i].push_back(0);            else                tempData[i].push_back(UINT_MAX);        }    }    dealLCS(X , Y , tempData);//开始进行计算    return tempData[X.size()][Y.size()];}/** * 实际计算LCS的最大长度 * @param X        X序列 * @param Y        Y序列 * @param tempData 缓存数据 */unsigned int dealLCS(std::vector<char>  X , std::vector<char>  Y ,             std::vector<std::vector<unsigned int> > & tempData){    if(X.size() == 0 || Y.size() == 0 )        return 0;    //首先看看这个问题有没有被计算过    if(tempData[X.size()][Y.size()] != UINT_MAX)        return tempData[X.size()][Y.size()];    //表示没有计算过,下面开始计算    unsigned int maxLength = 0;    if(X[X.size()-1] == Y[Y.size()-1])// 最后两个元素相等的情况    {        maxLength = dealLCS(std::vector<char>(X.begin() , X.end()-1),\                            std::vector<char>(Y.begin() , Y.end()-1),\                            tempData)+1;    }    else//最后两个元素不相等的情况    {        unsigned int temp1 = dealLCS(std::vector<char>(X.begin() , X.end()-1),\                                     std::vector<char>(Y.begin() , Y.end()) , tempData);        unsigned int temp2 = dealLCS(std::vector<char>(X.begin() , X.end()),\                                     std::vector<char>(Y.begin() , Y.end()-1), tempData);        maxLength = temp1 > temp2 ? temp1 : temp2;    }    tempData[X.size()][Y.size()] = maxLength;    return maxLength;}  int main(int argc, char const *argv[]){    char array1[]={'A','B','C','B','D','A','B'};    char array2[]={'B','D','C','A','B','A'};    std::vector<char> X(array1 , array1+sizeof(array1));    std::vector<char> Y(array2 , array2+sizeof(array2));    cout<<"The Lcs length is :"<<LCS(X,Y)<<endl;    while(1);    return 0;} 

我们再观察一下tempData数组的情况:
这里写图片描述
我们看出来,并不是所有子问题都被求解了,而只是求解了部分子问题就将原问题的解算出来了

所以在选择自上而下还是自下而上的解法时,应该衡量子问题数量与原问题到底有多大的依赖比重,从而选择最快的解法!

重构最优解

我们知道每一个问题首先面对的就是一个选择,且这个选择就是最优的选择,所有我们用一个数组来保存对应的选择方案既可保存最优解!

1 0