从来就没有救世主  也不靠神仙皇帝  要创造人类的幸福  全靠我们自己  

动态规划

 

动态规划与分治:

  都是将较大规模的问题分解成子问题求解

  分治:问题分解-----递归求解子问题--------子问题解组合求得原问题的解

  动态规划:分治划分的子问题,会重复计算相同规模的子问题,即会有重叠的子问题求解,因此当规模很大时,效率变低

          动态规划用来处理子问题重叠的情况,将子问题求解保存,再遇到相同规模的子问题时,不再求解,避免了很多重复工作

 

动态规划相关概念:

 要使用动态规划解决问题,则该问题必须要有最优子结构性质 ,且

(1)最优子结构

  ①一个问题的最优解包含其子问题的最优解,则此问题有 最优子结构 性质

  ②对于不同的问题,最优子结构的不同体现在:

    原问题的最优解中涉及多少个子问题

    在确定最优解使用哪些子问题时,需要考察多少种选择    

(2)子问题重叠

  递归算法反复求解相同的子问题

 

动态规划问题,一般使用自底向上的最优子结构:首先求子问题最优解,在子问题最优解基础上求原问题最优解

贪心:并不首先寻找子问题最优解,而是先做出一个"贪心"选择,即当时/局部最优选择,然后求解选出的子问题(而不像动态规划去求解所有相关的子问题)

 

 

 

 -----------------------------------------------------------------------------------------------------------------------------------------------------------

1. 钢条切割问题

  出售一段长度为i的钢条的价格为pi

长度 i 1 2 3 4 5 6 7 8 9 10
价格 pi 1 5 8 9 10 17 17 20 24 30

 

 

  现在给定一个长度为 n 的钢条,如何切割可使收益 rn 最大(这个很像背包问题,背包容量为n,物品占空间为i,且各个物品无限多)

 

(1)有多少种切割方案

  假设有一段长为4的钢条如下图

  

 

   橙色为可以切割的点,则有多少种切割方案成了组合问题:切0次、切1次、切2次、切3次

  共: 中方案(里面有相同的方案,比如分成1+1+2 和 1+2+1),如果要计算不重复的方案,要用 划分函数

 

 

  则长度为n的有 2n-1 种方案

(2)模型1

  n=1,r1 = p1

  n=2,r2=max(p2,r1+r1)       

  n=3,r3=max(p3,r1+r2,r2+r1)

  n=4,r4=max(p4,r1+r3,r2+r2,r3+r1)

  ....

  长度为n,rn=max(pn,r1+rn-1,r2+rn-2,...rn-1+r1)      其实pn就相当于rn+r0 ,此公式也可写为 rn=max(r1+rn-1,....rn+r0)

 

  为求解规模为n的原问题,需要求解形式一样,但规模小一些的子问题。最终问题的解由各子问题的解组合起来,而各个子问题也可以独立求解。

(3)模型2         这个模型2还有点不太理解

  上面的模型1将一个问题分解成两个子问题,再对两个子问题继续分解求解。

  将长度为n的钢条切下长度为i的一段,这块长度为i的不再切割,只对剩下长度为n-i的继续切割(递归求解)。则可得到公式如下:

  rn=max(p1+rn-1,p2+rn-2.....pn+r0)

  按照模型2,一个问题只需要计算一个子问题的解

  下面按照模型2进行分析:

(4)自顶向下的递归求解----------这实际上是分治的做法

  伪代码:

CUT-ROD(p,n)
    if n==0
        return 0
    q = -无穷
    for i = 1 to n
        q = max(q,p[i]+CUT-ROD(p,n-i))
    return q

 

  当n=4时,调用过程如下:每个结点表示调用 CUT-ROD函数

  

 

   对于长度为n的,CUT-ROD的调用次数为 2n

  效率低,以上面图为例,要求解n为3的问题,需要求解n为2和n为1的子问题,在n为2的子问题里面又求解n为1的子问题,这里就出现了重复计算,从整个图来看,有很多的重复计算。 

------------------------------------------------------------------------- 

  简单实现:C++

  dynamicProgram.h

#ifndef TESTCPLUS_DYNAMICPROGRAM_H
#define TESTCPLUS_DYNAMICPROGRAM_H
int cut_rod(int p[],int n);
extern int pi[22];
extern int cutCount;
#endif 

  dynamicOrogram.cpp

#include <iostream>
#include "dynamicProgram.h"
using namespace std;

//1.钢条切割
int pi[22] = {0,1,5,8,9,10,17,17,20,24,30,31,32,33,34,35,36,37,38,39,40,41};//长度为i的钢条价值
int cutCount = 0;
static int getMax(int a,int b)
{
    return a>b?a:b;
}

int cut_rod(int p[],int n)
{
    ++cutCount;
    if(n == 0) { //递归出口
        return 0;
    }
    int tmp = -1;
    for(int i=1;i<=n;i++) {
        tmp = getMax(tmp,p[i]+cut_rod(p,n-i));
    }
    return tmp;
}

  main.cpp

#include <iostream>
#include "dynamicProgram.h"
using namespace std;
int main(int argc, char** argv) 
{
    for(int i=1;i<=21;i++) {
        int maxValue = cut_rod(pi,i);
        cout<<"length "<<i<<":maxValue="<<maxValue<<"  cutCount= "<<cutCount<<endl;
        cutCount = 0;
    }
    return 0;
}

  计算结果:获得长度为1~21时,最大收益maxValue,以及调用cut_rod函数的次数

  

 

  

 

  可见,调用次数与问题规模n成指数性增长 

 

(5)动态规划求解

  自顶向下的递归求解效率低的原因在于重复计算了相同的子问题,动态规划通过合理安排求解顺序,对每个相同规模的子问题只求解一次,保存该规模的子问题解,在后面如果又要求解同等规模的子问题,则不需要再次计算,而直接从之前保存的结果里面取值。

  ①带备忘的自顶向下求解

  类似(3)里面用递归方式计算,但将各子问题的解保存在数组或散列表中,后面需要求解一个子问题时,先检查是否已经保存了这个规模问题的解,如果已经有了则直接返回保存的该值,否则就要计算。

  伪代码:

CUT-ROD-MEM(p,n)
    r[n+1] = 0,-无穷,-无穷....
    return CUT-ROD-MEM-(p,n,r)
CUT-ROD-MEM-(p,n,r)
    if r[n]>=0
        return r[n]
    if n == 0
        return 0
    q = -无穷
    for i = 1 to n
        q = max(q,p[i]+CUT-ROD-MEM-(p,n-i,r))
    r[n] = q
    return q

 

  实现:

  

int cut_rod_mem(int p[],int n)
{
    int *r  = (int *)malloc((n+1)*sizeof(int));
    if(NULL == r) {
        return -1;
    }
    r[0] = 0;
    for(int i=1;i<=n;i++) {
        r[i] = -1;
    }
    int res = cut_rod_mem_(p,n,r);
    free(r);
    return res;
}

int cut_rod_mem_(int p[],int n,int r[])
{
    ++cutCount;
    if(r[n] >= 0) {
        return r[n];
    }
    if(n == 0) {
        return 0;
    }
    int tmp = -1;
    for(int i=1;i<=n;i++) {
        tmp = getMax(tmp,p[i]+cut_rod_mem_(p,n-i,r));
    }
    r[n] = tmp;
    return tmp;
}

 

  用(4)的main去测试:查看调用cut_rod_mem_的次数

  

 

  

 

  当n较小时,差别不大;但当n变大后,调用次数出现很明显的差别(比如n为21时,不带备忘的递归调用了2097152次,而带备忘的调用了232次) 

 

 

  ②自底向上的版本

  先计算出n为1的最大收益并保存;计算n为2时最大收益:从p1+r1 和p+r0中取出最大值作为r2  ...

  n为3时,则从 p1+r2  p2+r1 p3 中取最大值.....

  由于先计算n较小时的最大收益,后面计算较大的n时,基于已经求得的较小的n的最大收益进行计算

  伪代码:

BOTTOM-UP_CUT-ROD(p,n)
    r[n+1] 
    r[0]=0
    for i = 1 to n
        q = -无穷
        for j = 1 to i
            q = max(q,p[j]+r[i-j])
        r[i] = q
    return r[n]
    

 

  实现:

int bottom_up_cut_rod(int p[],int n)
{
    int *r  = (int *)malloc((n+1)*sizeof(int));
    if(NULL == r) {
        return -1;
    }
    r[0] = 0;
    for(int i=1;i<=n;i++) {
        int tmp = -1;
        for(int j=1;j<=i;j++) {
       ++cutCount; tmp
= getMax(tmp,p[j]+r[i-j]); } r[i] = tmp; } int res = r[n]; free(r); return res; }

  用(4)的main测试:

  

 

  

 

  

  自底向上的特点:

    for执行次数是以1为公差等差数列的前n项和 次  O(n2)   ,上面的带备忘的递归也是O(n2)

   

(6)完全的解

  上面不管是分治还是动态规划都只计算出了最大收益,没有输出实际的方案

  ①带备忘的自顶向下的递归

 

  ②自底向上的方法

  伪代码:

EXTENDED-BOTTOM-UP_CUT-ROD(p,n)
    r[n+1] s[n+1]
    r[0] = 0
    for i = 1 to n
        q = -无穷
        for j = 1 to i
            if q<p[j]+r[i-j]
                q = p[j]+r[i-j]
                s[i] = j
        r[i] = q
    return r s

  s[i]的值:长度为i的切出来第一部分(这部分不再切割)的长度,剩下 i - s[i] 怎么切割呢:s[ i - s[i] ] 递推即可

  输出切割方案的伪代码:

PRINT(p,n)
    r,s = EXTENDED-BOTTOM-UP-CUT-ROD(p,n)
    while n>0
        print s[n]
        n = n-s[n]

 

  C++实现:

int extended_bottom_up_cut_rod(int p[],int n,int s[])
{
    int *r  = (int *)malloc((n+1)*sizeof(int));
    if(NULL == r) {
        return -1;
    }
    r[0] = 0;
    s[0] = 0;
    for(int i=1;i<=n;i++) {
        int tmp = -1;
        for(int j=1;j<=i;j++) {
            if(tmp<p[j]+r[i-j]) {
                tmp = p[j] + r[i-j];
                s[i] = j;
            }
        }
        r[i] = tmp;
    }
    int res = r[n];
    free(r);
    return res;
}

 

  调用:

int value4[22] = {0};
int s[22] = {0};
for(int i=0;i<=21;i++) {
    int maxValue = extended_bottom_up_cut_rod(pi,i,s);
    value4[i] = maxValue;
}
for(int i=1;i<22;i++) {
    cout<<"length "<<i<<":maxValue="<<value4[i]<<" solution:";
    int j = i;
    while(j>0) {
        if((j-s[j])<=0) {
            cout<<s[j];
        }
        else {
            cout<<s[j]<<"+";
        }
        j = j - s[j];
    }
    cout<<endl;
}

  

 

 

 

2.  矩阵链乘

 

 

3. 最长不下降子序列

  给定一个乱序整型数组,求最长不下降子序列、最长上升子序列,并输出序列(如果有多个可行的序列呢)

  upper_bound:返回给定区间[first,last)中第一个比给定参数大的元素的迭代器

  lower_bound:返回给定区间[first,last)中第一个比给定参数大或相等的元素的迭代器

(1)最长不下降

  从前往后遍历原序列:

    来第一个数字,则认为其是当前最长序列,长度为1

    来第二个数字:①比前一个大或相等(不下降,则可以相等),则可以接在前面的数字后面,最长序列长度加1

             ②比前一个小,则在已经确定的最长序列里面往前找,找到第一个大于它的数,并替换它(为什么要用这个数替换?因为它比那找到的第一个大于它的数更有前途)  

              例:已知前面的是1,2,3,5  下一个数字是4,我们不知道后面还有哪些数字,但4肯定比5更有前途使序列变得更长

    后面的步骤是一样的

  首先,需要一个跟原序列同样长度的数组d保存相关元素:d[i]表示长度为i的不下降序列的最小的末尾元素

  例:原序列1,2,3,4,8,9,5,6 则根据规则①和②最后d数组的保存结果为:1,2,3,4,5,6

 

  输出最长序列:

    在遍历原序列时,标记该元素在最长序列中的位置。比如上面的1,2,3,4,8,9,5,6,用一个数组mark依次标记为1,2,3,4,5,6,5,6,当然数组下标从0开始,所以依次还要小1 

    找完最长子序列后,从原序列最后往前找:先找mark[i]为len的,再找mark[i]为len-1的,直到找完

int longerNDSubseqRes(int src[],int n,int res[])
{
    if(NULL == src || n == 0 || NULL == res) {
        return 0;
    }
    int *d = (int *)malloc(n*sizeof(int));
    if(NULL == d) {
        return 0;
    }
    int *mark = (int *)malloc(n*sizeof(int));
    if(NULL == mark) {
        free(d);
        return 0;
    }
    d[0] = src[0];
    mark[0] = 0;
    int len = 0;
    for(int i=1;i<n;i++) {
        if(src[i]>=d[len]) {
            d[++len] = src[i];
            mark[i] = len;
        }
        else {
            int firstBiger = upper_bound(d,d+len+1,src[i])-d;
            d[firstBiger] = src[i];
            mark[i] = firstBiger;
        }
    }

    //输出序列
    stack<int> tmp;
    for(int i=n-1,j=len;i>=0;i--) {
        if(mark[i] == j) {
            tmp.push(src[i]);
            --j;
        }
        if(j<0) {
            break;
        }
    }
    int i = 0;
    while(!tmp.empty()) {
        res[i++] = tmp.top();
        tmp.pop();
    }
    free(d);
    free(mark);
    return len+1;
}

 

    

(2)最长上升

  对于要求序列严格递增,与(1)的不下降序列比较,有几个地方不同

  ①如果数字比前一个大(而不是大于等于)则,这个数字可以加入到最长序列中,序列长度加1

  ②如果数字与前一个相等,则忽略

  ③如果数字比前一个小,则在已确定的最长序列里面往前找,找到第一个大于等于(而不是大于)它的数,并替换之

 

int longerNDSubseqRes(int src[],int n,int res[])
{
    if(NULL == src || n == 0 || NULL == res) {
        return 0;
    }
    int *d = (int *)malloc(n*sizeof(int));
    if(NULL == d) {
        return 0;
    }
    int *mark = (int *)malloc(n*sizeof(int));
    if(NULL == mark) {
        free(d);
        return 0;
    }
    d[0] = src[0];
    mark[0] = 0;
    int len = 0;
    for(int i=1;i<n;i++) {
        if(src[i]>d[len]) {  //改动1
            d[++len] = src[i];
            mark[i] = len;
        }
        else {
            int firstBiger = lower_bound(d,d+len+1,src[i])-d; //改动2
            d[firstBiger] = src[i];
            mark[i] = firstBiger;
        }
    }

    //输出序列
    stack<int> tmp;
    for(int i=n-1,j=len;i>=0;i--) {
        if(mark[i] == j) {
            tmp.push(src[i]);
            --j;
        }
        if(j<0) {
            break;
        }
    }
    int i = 0;
    while(!tmp.empty()) {
        res[i++] = tmp.top();
        tmp.pop();
    }
    free(d);
    free(mark);
    return len+1;
}

 

4. 最长公共子序列

  前缀:序列X=<A,B,C,D,E,F>  X0为空;第1前缀为X1 =<A>  ;第2前缀为 X2=<A,B>     .....

  LCS最优子结构:X=<x1,x2,x3,...xm>   Y=<y1,y2,y3,...yn>   Z=<z1,z2....zk>为X和Y的任一LCS

         ①如果xm==yn,则zk==xm==yn,且Zk-1是Xm-1和Yn-1的一个LCS

           ②如果xm != yn:  

            如果zk != xm,则Z是Xm-1和Y的一个LCS

            如果zk != yn,则Z是X和Yn-1的一个LCS

           两序列的LCS包含该序列小一点规模序列的LCS

  定义 c[i,j] 为Xi和Yj的LCS长度,则有公式:

    

   对于m个元素的序列x、n个元素的序列y,需要(m+1)*(n+1)的辅助空间来计算长度:行0、列0都为0,最大长度的值就是c[m][n]

  构建出来的c数组如图:

  

 

  

  如何重构共同序列:增加一个m*n的b数组,在构建c数组的时候,对于上面的三种情况(xi==yj、xi!=yj、xi!=yj三种)分别做标记,也就是上面图里面的箭头。数组构建完成后,从最后往前找,找到一个指向左上的箭头,则此位置对应的就是xi==yj的情况,此字符就是公共序列中的一个。

  

int longestCommonSeq(char x[],int m,char y[],int n,char res[])
{
    //...参数检测
    char **c = getMNMatrix<char>(m+1,n+1);
    if(nullptr == c) {
        return 0;
    }
    char **b = getMNMatrix<char>(m,n);
    if(nullptr == b) {
        freeMatrix(c,m+1,n+1);
        return 0;
    }
    for(int i=0;i<m+1;i++) {
        c[i][0] = 0;
    }
    for(int j=0;j<n+1;j++) {
        c[0][j] = 0;
    }
    for(int i=0;i<m;i++) {
        for(int j=0;j<n;j++) {
            if(x[i] == y[j]) {
                c[i+1][j+1] = c[i][j] + 1;
                b[i][j] = '1';//左上
            }
            else if(c[i][j+1]>=c[i+1][j]) {
                c[i+1][j+1] = c[i][j+1];
                b[i][j] = '2';//
            }
            else {
                c[i+1][j+1] = c[i+1][j];
                b[i][j] = '3'; //
            }
        }
    }
    int maxLen = c[m][n];

    //构建最长公共序列  通过数组b
    int bi = m-1,bj = n-1;
    stack<char> tmp;
    while((bi >= 0) && (bj >= 0)) {
        if(b[bi][bj] == '1') {
            tmp.push(x[bi]);//或者y[bj]
            bi--;
            bj--;
        }
        else if(b[bi][bj] == '2') {
            bi--;
        }
        else {
            bj--;
        }
    }
    bi = 0;
    while(!tmp.empty()) {
        res[bi++] = tmp.top();
        tmp.pop();
    }
    freeMatrix(c,m+1,n+1);
    freeMatrix(b,m,n);
    return maxLen;
}

  时间:O(m*n)

  空间:O(m*n*2)

 

  优化:时间和空间都还可以优化..................

   

5. 最长公共子串

  与公共子序列相似,但要求更严格,要求公共部分是连续的

 

  如果两个字符不等,则c数组对应位置为0;如果相等,则对应位置的值为其左上角值加1

 

  假设两个串为:"acbcbcef"、"abcbced" ,构建c数组如下

  

 

  c数组里面最大值即为最长公共子串长度;重构公共子串就从该位置往左上方找,直到找到值为1为止。

  

int longestCommonStr(char x[],int m,char y[],int n,char res[])
{
    //...参数检测,包括m、n为0的情况暂时排除
    char **c = getMNMatrix<char>(m,n);
    if(nullptr == c) {
        return 0;
    }
    int maxIndex = 0;
    int maxLen = 0;
    for(int i=0;i<m;i++) {
        for(int j=0;j<n;j++) {
            if(x[i] == y[j]) {
                if(i==0 || j==0) {
                    c[i][j] = 1;
                }
                else {
                    c[i][j] = c[i-1][j-1] + 1;
                }
                if(c[i][j] > maxLen) {
                    maxLen = c[i][j];
                    maxIndex = i;
                }
            }
            else {
                c[i][j] = 0;
            }
        }
    }
    for(int i=maxIndex-maxLen+1,j=0;i<=maxIndex;i++) {
        res[j++] = x[i];
    }
    freeMatrix(c,m,n);
    return maxLen;
}

 

 

6. 背包

 

7. 最优二叉搜索树

 

 

  

 

posted @ 2020-03-29 23:04  T,X  阅读(300)  评论(0编辑  收藏  举报