算法设计与分析(三)动态规划(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SakuraMashiro/article/details/78610570

基本思想
1.动态规划是一种使多阶段决策过程最优的通用方法。

2.动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。

3.动态规划中分解得到的子问题往往不是互相独立的。但不同子问题的数目常常只有多项式级。用分治法求解时,有些子问题被重复计算了许多次,从而导致分治法求解问题时间复杂度较高。
4.动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。
例如:在斐波纳契数列中,如果使用传统的分治加递归的方法,当问题规模变大时,由于大量重复计算的子问题,会导致算法的效率极其低下。如下图所示,计算斐波纳契数列F(6)时,F(4)、F(3)、F(2)、F(1)都重复计算过了。
这里写图片描述
5.所以,动态规划的实质就是分治思想和解决冗余,通过存储子问题的解而避免计算重复的子问题,以解决最优化问题。一言以蔽之,空间换时间

6.动态规划求解问题时的关键就是得到刻画子问题最优解与原问题最优解关系特征的状态转移方程,有了状态转移方程后,就可以通过自底向上或自顶向下记忆化搜索(用数组等将已经算过的东西记录下来在下一次要使用的直接用已经算出的值,避免重复运算,去掉的重复的搜索树)的方式计算最优值。

动态规划的逆向思维法
动态规划的逆向思维法是指从问题目标状态出发倒退回初始状态或边界状态的思维方式,其要点可归纳为以下三个步骤:
(1)分析最优值的结构,刻画其结构特征;
(2)递归的定义最优值;
(3)按自底向上或自顶向下记忆化的方式计算最优值。

动态规划的正向思维法
动态规划的正向思维法是从已知最优值的初始状态或边界状态开始,按照一定的次序遍历整个状态空间,递推出每个状态所对应问题的最优值。其要点可归纳为以下三个步骤:
(1)构造状态网络;
(2)根据状态转移关系和状态转移方程建立最优值的递推计算式;
(3)按阶段的先后次序计算每个状态的最优值。

典型例题

例一:走楼梯问题

描述:有一人要爬n阶的楼梯,他一次可以爬1阶或2阶,问要爬完这n阶楼梯,共有多少种方法?

解决:逆向思维法,分析问题的最优解是如何一步步得到的。
1)假设我们现在在第n阶阶梯上,显然,我们上一步是在n-1阶或者n-2阶,根据分类加法原理,我们可以知道,第n阶的方法=n-1阶的方法+n-2阶的方法
2)同样的,对于n-1阶和n-2阶我们也可以用类似的方法进行求解。
而当我们求到1阶和2阶的时候,显然方法种数分别为1、2。
3)所以如果f[i]表示爬到第i阶的方法数,那么
状态转移方程
f[1]=1 f[2] = 2
f[i] = f[i - 1]+ f[i - 2] (斐波纳契数列)

例二:最大子段和问题
问题:给定n个整数(可以为负数)的序列(a1, a2, …, an),求其最大子段和

分析:由于序列可正可负,所以子段和可能出现0或负数,当前面的子段和出现0或负数时,即使再加上一个正数,只会使子段和更小。

设b[j]表示最后一项为a[j]的序列构成的最大子段和,则
b[j] = max{ b[j-1] + a[j] , a[j] } ( 状态转移方程 )。
当最后一项为a[j-1]的最大子段和大于0时,最后一项为a[j]的最大子段和就等于b[j-1]+a[j] , 如果b[j-1]小于等于0的话,最后一项为a[j]的最大子段和就等于a[j]。

代码实现

int MaxSum (int n, int* a) {
    int sum = 0, b = 0;
    for (i = 1; i <= n; i++) {
        if (b > 0) b += a[i];
        else b = a[i];
        if (b > sum) sum = b;
    }
    return sum;
}

由于b[j]只与b[j-1]有关,所以用一个O(1)的空间来存储,一个变量b就行了,在不断计算b的过程中,用sum来保存b的最大值,即b[j]的最大值,也就是所有子段和中的的最大值,代表了原问题的最优解。

例三:最长不下降子序列
问题:设有一个正整数的序列: b1,b2,…,bn ; 对于下标i1,i2,i3,… ,im;若有bi1≤bi2≤…≤bim ,则称存在一个长度为m的不下降序列。 例如,数列 { 13 , 7 , 9 , 16, 38 , 24, 37 ,18 ,44, 19 , 21 , 22 , 63 , 15 },对于下标i1=1,i2=4,i3=5,i4=9,i5=13,满足13<16<38<44<63,则存在长度为5的不下降序列。

分析:
f[i]表示以第i个数结尾的不下降子序列的最大长度,
f[i]就可以由前面任何一个满足a[j]<=a[i]的f[j]+1得到
所以,可以推导出:

f[i] = 1 (i=1)

f[i]=max{ f[j] + 1 } (a[j]<=a[i] 且 j< i ) (i>1)

计算过程如下图
这里写图片描述

代码

int increaseSub(int n){

  for(i = 0; i < n; i++){
    f[i] = 1;
    for(j = 0; j < i; j++){     
    if(a[j] <= a[i] && f[j] + 1 > f[i])
         f[i] = f[j] + 1;   
    }
  }
  return f[n-1];

}

例四:最长公共子序列
前面两个问题都是一维的规划,下面来看看个二维的

问题:给定2个序列X={x1,x2,…,xm}和Y={y1,y2,…,yn},找出X和Y的最长公共子序列。

分析:
(1)最优子结构性质
设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk} ,则
(a)若xm= yn,则zk=xm=yn,且zk-1是xm-1和yn-1的最长公共子序列。
(b)若xm≠ yn且zk≠xm,则Z是xm-1和Y的最长公共子序列。
(c)若xm≠ yn且zk≠yn,则Z是X和yn-1的最长公共子序列。
由此可见,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。

(2)建立递归关系
用c[i][j]记录序列Xi和Yj的最长公共子序列的长度。其中, Xi={x1,x2,…,xi};Yj={y1,y2,…,yj}。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列。故此时C[i][j]=0。其它情况下,由最优子结构性质可建立递归关系如下:

这里写图片描述

(3)计算最优值
子问题空间总共有θ(mn)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。

void LCSLength(int m,int n,char *x,char *y,int **c,int **b) {  
    int i,j;
    //给边界赋值
    for (i = 1; i <= m; i++)  c[i][0] = 0;
    for (i = 1; i <= n; i++)  c[0][i] = 0;
    for (i = 1; i <= m; i++)
        for (j = 1; j <= n; j++) {
            if (x[i]==y[j]) { 
                c[i][j]=c[i-1][j-1]+1; b[i][j]=1;}
             else if (c[i-1][j]>=c[i][j-1]) {
                  c[i][j]=c[i-1][j]; b[i][j]=2;}
             else { c[i][j]=c[i][j-1]; b[i][j]=3; }
        }
}

C[i][j]存储Xi和Yj的最长公共子序列的长度,
b[i][j]记录C[i][j]的值是由哪一个子问题的解得到的,后面构造最长公共子序列时需要用到。

(4)构造最优解
从b[m][n]开始,在数组b中搜索,当b[i][j]=1时,表示Xi和Yj的最长公共子序列是由Xi-1和Yj-1的最长公共子序列在尾部加上xi所得到的子序列;当b[i][j]=2时,表示表示Xi和Yj的最长公共子序列与Xi-1和Yj的最长子序列相同;当b[i][j]=3时,表示表示Xi和Yj的最长公共子序列与Xi和Yj-1的最长子序列相同。

void LCS(int i,int j,char *x,int **b) {
    if (i ==0 || j==0) return;
    if (b[i][j]== 1){ LCS(i-1,j-1,x,b); cout<<x[i]; }
    else if (b[i][j]== 2) LCS(i-1,j,x,b);
    else LCS(i,j-1,x,b);
}

求解过程图示
这里写图片描述

这里写图片描述

猜你喜欢

转载自blog.csdn.net/SakuraMashiro/article/details/78610570