第15章 动态规划

一: 分治算法和动态规划的区别:

  • 分治算法将问题分为互不相交的子问题,递归地求解子问题,然后再将它们的解组合起来,以求出原问题的解。

  • 动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。

如果用分治算法来求解重叠子问题的情况,分治算法会做许多不必要的工作,因为它会反复求解那些公共子子问题。而动态规划算法对每个子问题只求解一次,将解保存在一个表格中,如果随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算。但是动态规划算法是付出额外的内存空间来节省计算时间的,是典型的以时间换空间的算法。

二:动态规划算法的适用情况:

动态规划问题通常用来求解最优化问题,但这类最优化问题应该具备两个要素:

  • 最优子结构:一个问题的最优解包含其子问题的最优解。此时我们用子问题的最优解来构造原问题的最优解,因此我们必须要小心确保考察了最优解中用到的所有子问题,但注意这些子问题应该是无关的,也就是一个子问题的求解不影响另一个子问题的解

  • 重叠子问题: 子问题空间必须足够小,即问题的递归算法会反复求解相同的子问题,而不是一直生成新的子问题。

因此一个问题是否适合用动态规划求解要同时依赖于其子问题的无关性重叠性

三:动态规划的实现方法:

动态规划有两种实现方法,一个是带备忘的自顶向下法,另一个是自底向上法

  • 带备忘的自顶向下法:此方法仍然按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在数组或散列表中)。当需要一个子问题的解时,过程会首先检查是否已经保存过此解,如果是,则直接返回保存的值,从而节省了计算时间,否则,按通常方式计算这个子问题。

  • 自底向上法:这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小”子问题的求解。因而我们可以将子问题按规模排序,按从小至大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小子问题都已求解完毕,结果已保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成。

一般来说,如果每个子问题都必须至少求解一次,自底向上动态规划算法会比自顶向下备忘算法快,因为自底向上算法没有递归调用的开销。相反,如果子问题空间的某些子问题完全不必求解,可以用自顶向下的备忘方法,因为它只会求解绝对必要的子问题,然而自底向上法是求解出所有的子问题。

四:设计动态规划算法的四个步骤:

  • 刻画一个最优解的结构特征,也就是原问题的最优解用其子问题的最优解的形式表达出来;
  • 递归地定义最优解的值;
  • 计算最优解的值,通常采用自底向上法;
  • 利用计算出的信息构造一个最优解。

五:动态规划求解最优化问题的事例:

书中共列举了四个例子,分别是钢条切割矩阵链乘法最长公共子序列最优二叉搜索树。下面给出这四个问题的代码。

钢条切割:

下面给出的代码和书中的代码有点不一样,书中表示的是一段长度为n的钢条可以切割成任意长度。但下面的代码表示的是长度为n的钢条只能切割成规定长度的钢条,不难发现这种情况包括了书中的情况。

无备忘的递归带备忘的递归自底向上法这三个版本的代码分别如下:

无备忘的递归:
//在数组price中,下标i对应(i+1)米,price[i]对应(i+1)米钢条的价格,比如下标0对应1米,price[0]表示1米钢条的价格;
//length表示的是要切割钢条的长度;
int cutRodRec(const vector<int>& price,int length)
{
        if(length==0)
                return 0;

        int revenue=-1;
        if(price.size()<=length)//要切割的钢条长度大于等于市场上能够卖的钢条长度
                for(int i=1;i<=price.size();++i)
                        revenue=max(revenue,price[i-1]+cutRodRec(price,length-i));
        else
                for(int i=1;i<=length;++i)
                        revenue=max(revenue,price[i-1]+cutRodRec(price,length-i));

        return revenue;
}
带备忘的递归:
//在数组revenueArray中,下标i对应i米,revenueArray[i]对应i米钢条的最大收益,比如revenueArray[1]表示1米钢条的最大收益。
int memorizedCutRodAux(const vector<int>& price,int length,vector<int>& revenueArray)
{
        if(revenueArray[length]>=0)
                return revenueArray[length];

        if(length==0)
                return 0;

        int revenue=-1;
        if(price.size()<=length)
                for(int i=1;i<=price.size();++i)
                        revenue=max(revenue,price[i-1]+memorizedCutRodAux(price,length-i,revenueArray));
        else
                for(int i=1;i<=length;++i)
                        revenue=max(revenue,price[i-1]+memorizedCutRodAux(price,length-i,revenueArray));

        revenueArray[length]=revenue;
        return revenue;
}


int memorizedCutRod(const vector<int>& price,int length)
{
        vector<int> revenueArray(length+1,-1);
        return memorizedCutRodAux(price,length,revenueArray);
}
自底向上法:

与上面的代码相比,下面的代码中不仅求解了一个最优解的收益值,还返回了解本身(一个长度列表,给出切割后每段钢条的长度)。

//在数组solution中,下标i对应i米,solution[i]存储的是i米钢条最优的第一段切割长度。
void bottomUpCutRod(const vector<int>& price,int length,vector<int>& revenueArray,vector<int>& solution)
{
        for(int j=1;j<=length;++j)
        {
                int revenue=-1;
                if(price.size()<=j){
                        for(int i=1;i<=price.size();++i)
                                if(revenue<price[i-1]+revenueArray[j-i]){
                                        revenue=price[i-1]+revenueArray[j-i];
                                        solution[j]=i;
                                }
                }
                else{
                        for(int i=1;i<=j;++i)
                                if(revenue<price[i-1]+revenueArray[j-i]){
                                        revenue=price[i-1]+revenueArray[j-i];
                                        solution[j]=i;
                                }
                }
                revenueArray[j]=revenue;
        }
}

void printCutRodSolution(const vector<int>& price,int length)
{
        vector<int> revenueArray(length+1);
        revenueArray[0]=0;

        vector<int> solution(length+1);

        bottomUpCutRod(price,length,revenueArray,solution);

        cout<<"maximum revenue: "<<revenueArray[length]<<endl;
        cout<<"the concrete solution:"<<endl;
        while(length>0){
                cout<<solution[length]<<" ";
                length-=solution[length];
        }
}

矩阵链乘法:

下面的代码给出了备忘的递归自底向上方法这两个版本去求解最少的乘法数,还给出了构造最优解的代码。

备忘的递归:
// minCount矩阵存储的是最小的矩阵乘法次数;
// breakPoint矩阵存储的是一个矩阵链划分成两个矩阵链的最优划分点。
int matrixChainOrderRec(int i,int j,const vector<int>& dimension,vector< vector<int> >& minCount,vector< vector<int> >& breakPoint)
{
        if(minCount[i][j]>=0)
                return minCount[i][j];

        if(i==j)
                minCount[i][j]=0;
        else{
                int tmp=INT_MAX;
                for(int k=i;k<j;k++)
                {
                        int count=matrixChainOrderRec(i,k,dimension,minCount,breakPoint)+matrixChainOrderRec(k+1,j,dimension,minCount,breakPoint)+dimension[i-1]*dimension[k]*dimension[j];
                        if(count<tmp){
                                tmp=count;
                                breakPoint[i][j]=k;
                        }
                }

                minCount[i][j]=tmp;
        }

        return minCount[i][j];
}
自底向上方法:
void matrixChainOrder(const vector<int>& dimension,vector< vector<int> >& minCount,vector< vector<int> >& breakPoint)
{

        for(int i=1;i!=minCount.size();++i)
                minCount[i][i]=0;

        int matrixChainLength=dimension.size()-1;

        for(int subChainLength=2;subChainLength<=matrixChainLength;subChainLength++)
                for(int i=1;i<=matrixChainLength-subChainLength+1;++i)
                {
                        int j=i+subChainLength-1;

                        minCount[i][j]=INT_MAX;
                        for(int k=i;k!=j;++k)
                        {
                                int count=minCount[i][k]+minCount[k+1][j]+dimension[i-1]*dimension[k]*dimension[j];

                                if(count<minCount[i][j]){
                                        minCount[i][j]=count;
                                        breakPoint[i][j]=k;
                                }
                        }
                }
}
构造最优解:
void printMatrixChainOrder(const vector< vector<int> >& breakPoint,int i,int j)
{
        if(i==j)
                cout<<"A"<<i;
        else{
                cout<<"(";
                printMatrixChainOrder(breakPoint,i,breakPoint[i][j]);
                printMatrixChainOrder(breakPoint,breakPoint[i][j]+1,j);
                cout<<")";
        }
}

void matrixChainOrder(const vector<int>& dimension)
{
        vector< vector<int> > minCount;
        minCount.resize(dimension.size());
        for(int i=0;i!=minCount.size();++i)
                minCount[i].resize(dimension.size(),-1);

        vector< vector<int> > breakPoint;
        breakPoint.resize(dimension.size());
        for(int i=0;i!=minCount.size();++i)
                breakPoint[i].resize(dimension.size());

        matrixChainOrder(dimension,minCount,breakPoint);
//      matrixChainOrderRec(1,dimension.size()-1,dimension,minCount,breakPoint);

        cout<<"the optimal matrix order:"<<endl;
        printMatrixChainOrder(breakPoint,1,dimension.size()-1);
        cout<<endl;
        cout<<"the corresponding minimum multiply count: "<<minCount[1][dimension.size()-1]<<endl;
}

最长公共子序列:

下面的代码只给出了自底向上方法这一个版本的,同时也给出了构造最优解的代码。它们的代码在此一并给出:

int max(int a,int b)
{
        return a>=b?a:b;
}

//自底向上方法求出array1,array2这两个序列的最长公共子序列,用length这个矩阵保存。
template<class Type>
void lcsLength(const vector<Type>& array1,const vector<Type>& array2,vector< vector<int> >& length )
{
        length.resize(array1.size()+1);
        for(int i=0;i!=length.size();++i)
                length[i].resize(array2.size()+1);

        for(int i=0;i!=length.size();++i)
        {
                length[0][i]=0;
                length[i][0]=0;
        }

        for(int i=1;i!=length.size();++i)
                for(int j=1;j!=length.size();++j)
                {
                        if(array1[i-1]==array2[j-1])
                                length[i][j]=length[i-1][j-1]+1;
                        else
                                length[i][j]=max(length[i-1][j],length[i][j-1]);
                }
}

//构造最优解:
template<class Type>
void printLcs(const vector<Type>& array1,const vector<Type>& array2,int i,int j,vector< vector<int> >& length )
{
        if(i==0||j==0)
                return;

        if(array1[i-1]==array2[j-1]){
                printLcs(array1,array2,i-1,j-1,length);
                cout<<array1[i-1]<<" ";
        }
        else if(length[i-1][j]>=length[i][j-1])
                printLcs(array1,array2,i-1,j,length);
        else
                printLcs(array1,array2,i,j-1,length);
}

template<class Type>
void lcs(const vector<Type>& array1,const vector<Type>& array2)
{
        vector< vector<int> > length;

        lcsLength(array1,array2,length);

        cout<<"the length of lcs: "<<length[array1.size()][array2.size()]<<endl;
        cout<<"the corresponding lcs: "<<endl;
        printLcs(array1,array2,array1.size(),array2.size(),length);
}

最优二叉搜索树:

和最长公共子序列问题一样,下面的代码只给出了自底向上方法这一个版本的,同时也给出了构造最优解的代码。它们的代码在此一并给出:

//自底向上方法:
void optimalBST(const vector<double>& actualKeyPro,const vector<double>& fakeKeyPro,vector< vector<double> >& minCost,vector< vector<int> >& root)
{
        int actualKeySize=actualKeyPro.size()-1;

        minCost.resize(actualKeySize+2);
        for(int i=0;i!=minCost.size();++i)
                minCost[i].resize(actualKeySize+2);

        root.resize(actualKeySize+1);
        for(int i=0;i!=root.size();++i)
                root[i].resize(actualKeySize+1);

        vector< vector<double> > weight;
        weight.resize(actualKeySize+2);
        for(int i=0;i!=weight.size();++i)
                weight[i].resize(actualKeySize+2);

        for(int i=1;i<=actualKeySize+1;++i)
        {
                minCost[i][i-1]=fakeKeyPro[i-1];
                weight[i][i-1]=fakeKeyPro[i-1];
        }

        for(int keySize=1;keySize<=actualKeySize;++keySize)
                for(int i=1;i<=actualKeySize-keySize+1;++i)
                {
                        int j=keySize+i-1;

                        minCost[i][j]=DBL_MAX;
                        weight[i][j]=weight[i][j-1]+actualKeyPro[j]+fakeKeyPro[j];
                        for(int r=i;r<=j;++r)
                        {
                                double cost=minCost[i][r-1]+minCost[r+1][j]+weight[i][j];
                                if(cost<minCost[i][j]){
                                        minCost[i][j]=cost;
                                        root[i][j]=r;
                                }
                        }
                }
}

//构造最优解:

void printOBST(const vector< vector<int> >& root,int i,int j)
{
        if(i<=j){
                int r=root[i][j];

                if(i<j)
                        cout<<"k"<<root[i][r-1]<<" is the left child of k"<<r<<endl;
                else if(i==j)
                        cout<<"d"<<i-1<<" is the left child of k"<<i<<endl;
                printOBST(root,i,r-1);

                if(i<j)
                        cout<<"k"<<root[r+1][j]<<" is the right child of k"<<r<<endl;
                else if(i==j)
                        cout<<"d"<<i<<" is the right child of k"<<r<<endl;
                printOBST(root,r+1,j);
        }
}

void OBST(const vector<double>& actualKeyPro,const vector<double>& fakeKeyPro)
{
        vector< vector<double> > minCost;
        vector< vector<int> > root;

        optimalBST(actualKeyPro,fakeKeyPro,minCost,root);

        int actualKeySize=actualKeyPro.size()-1;
        cout<<"the minimum average cost: "<<minCost[1][actualKeySize]<<endl;

        cout<<"the correspongding optimal binary search tree:"<<endl;
        cout<<"k"<<root[1][actualKeySize]<<" is root"<<endl;
        printOBST(root,1,actualKeySize);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值