算法分析与设计:动态规划

1、动态规划

动态规划算法通常用于求解具有某种最优性质的问题。这类问题通常会有许多可行解,每个解对应一个值,而我们希望在可行解中找到最优解

(1) 基本思想

动态规划的基本思想是将待求问题分解为若干个子问题,先求解子问题,再从子问题得到原问题的解。与分治法不同,动态规划适合于分解后的子问题非独立的情况。动态规划用一个记录已解决子问题的答案,可以避免大量的重复计算

在LeetCode做题时碰到很多”自上而下“型的动态规划算法。这种算法的基本思路与“自下而上”的动态规划是一致的,区别在于分析问题的角度与得出答案的方式。自上而下型从原问题出发,逐级向下计算所需子问题的解,通常使用递归;自下而上型从最小的子问题出发,逐级向上构造较大问题的解,通常使用迭代或递推。

(2) 基本步骤

  1. 找出最优解的性质,并刻画其结构特征
  2. 递归地定义最优值(动态规划方程);
  3. 自下而上的方式计算出最优值;
  4. 根据计算最优值过程得到的信息,构造最优解

(3) 问题特征

  1. 最优子结构:问题的最优解包含了其子问题的最优解。
  2. 重叠子问题:每次产生的子问题并不总是新问题。

2、动态规划典型问题

(1) 矩阵连乘问题

● 问题描述

普通方法下,矩阵连续相乘的运算次数与矩阵相乘的顺序有关。
假设有n个矩阵,为A1A2…An,其中Ai维数为 Pi-1 × Pi;试确定这n个矩阵相乘所需的最少相乘次数。

● 问题分析

m[i,j] 表示对从Ai到Aj的最少相乘次数,那么该次数必然可以分解为两个部分相乘次数之和,再加上两个部分合并的代价。设k为将矩阵Ai到Aj的切分位置,其中i ≤ k ≤ j。遍历所有的k并找到最少的相乘次数,就是m[i,j]的值。而每一个k所切分出的Ai到Ak与Ak+1到Aj,也应当是这部分的最少相乘次数m[i,k]m[k+1,j]
由此,该问题具有如上的最优子结构性质,根据该性质可以对问题求解。

● 建立方程

由上面的分析,可以写出动态规划方程
矩阵连乘问题的动态规划方程

● 计算最优值

根据动态规划方程,可以设计对应算法计算最优值。
大致步骤为:首先将i=j,也即单个矩阵的情况最优值设为0;然后逐渐向上计算多个矩阵的情况,最后得到n个矩阵的情况。
对于n个矩阵A1A2…An,其最优值为m[1][n]。

//p储存矩阵的维数;m存储矩阵Ai到Aj的最小乘积次数;s存储最小乘积次数对应的切断点。
void minMetrixChain(int n,int* p,int** m,int** s)  //  动态规划,自下而上求所有子集的乘积最小次数
{
    for(int i = 0;i <= n;i++){
        m[i][i] = 0;    //令对角线元素为0
    }
    for(int r = 1;r <= n - 1;r++){          //i与j的差,最大为n-1
        for(int i = 1;i <= n - r;i++){      //i从1开始求m和s的元素
            int j = i + r;
            m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j]; //初值,切在i位置,即第一项单独计算,后面r-i项一起算
            s[i][j] = i;    //初值记录s
            for(int k = i + 1;k <j;k++){    //切在k位置,i~k为一组,k+1~j为一组
                int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
                if(t < m[i][j]){
                    m[i][j] = t;
                    s[i][j] = k;
                }
            }
        }
    }
}
● 构造最优解

上一步计算最优值的过程已经得到了构造最优解的所需信息。由此设计算法构造最优解:

//s中储存了分割点的信息
void traceBack(int i,int j,int **s)
{
    if(i == j)
        return;
    traceBack(i,s[i][j],s);
    traceBack(s[i][j]+1,j,s);
    cout <<"Multiply A" << i << "," << s[i][j] << " and A" << s[i][j] + 1 << "," << j << endl;
}

(2) 最长公共子序列(LCS)

● 问题描述

子序列:对于X1X2…Xn,有一个序列Xi…Xj的下标严格递增,则称后一序列为前一序列的子序列。
子序列不要求连续,但要求子序列内的元素顺序必须在原序列中有序。
试求两个序列X=x1x2…xm和Y=y1y2…yn最长公共子序列。这一问题又称为LCS问题

● 问题分析

设Z=z1z2…zk是X和Y的最长公共子序列,则LCS具有如下最优子结构性质

  1. 若xm=yn,则Zk-1是Xm-1和Yn-1的最长公共子序列;
  2. 若xm≠yn,且zk≠xm,则Z是Xm-1和Y的最长公共子序列;
  3. 若xm≠yn,且zk≠yn,则Z是X和Yn-1的最长公共子序列。
● 建立方程

c[i,j]为Xi与Yj的最长公共子序列长度,根据上面的性质,可以得到动态规划方程
LCS的动态规划方程

● 计算最优值

根据动态规划方程,可以设计算法计算最优值。
大致步骤为:令c[i][0]和c[0][j]均为0,再根据方程,用两循环嵌套逐步向上计算LCS值。

void longestCommonString(int *X,int n,int *Y,int m,int **lcsNum,int **lcsWay)
{
    //初始化将空序列与任意序列的最大子序列设为无
    for(int i = 1;i <= n;i++)
        lcsNum[i][0] = 0;
    for(int i = 1;i <= m;i++)
        lcsNum[0][i] = 0;
    //根据递推公式构造
    for(int i = 1;i <=n;i++){
        for(int j = 1;j <= m;j++){
            if(X[i] == Y[j]){
                lcsNum[i][j] = lcsNum[i-1][j-1] + 1;
                lcsWay[i][j] = 1;
            }
            else if(lcsNum[i-1][j] > lcsNum[i][j-1]){
                lcsNum[i][j] = lcsNum[i-1][j];
                lcsWay[i][j] = 2;
            }
            else{
                lcsNum[i][j] = lcsNum[i][j-1];
                lcsWay[i][j] = 3;
            }
        }
    }
}
● 构造最优解

上面计算最优值的过程已经得到了构造最优解的所需信息。

//lcsWay存有LCS的构造路径。
void printLCS(int n,int m,int **lcsNum,int **lcsWay,int *X)
{
    if(n == 0 || m == 0)    return;
    if(lcsWay[n][m] == 1){
        printLCS(n-1,m-1,lcsNum,lcsWay,X);
        cout << X[n];
    }
    else if(lcsWay[n][m] == 2)
        printLCS(n-1,m,lcsNum,lcsWay,X);
    else if(lcsWay[n][m] == 3)
        printLCS(n,m-1,lcsNum,lcsWay,X);
}

(3) 0-1背包问题

● 问题描述

给定一个物品集合s ={1,2,3,…,n},物品i的重量是wi,价值是vi,背包的容量为W。在限定的重量内,怎么装物品才能使物品的总价最大。
当物品允许拆分时,称为背包问题,适用贪心算法
当物品不允许拆分时,称为0-1背包问题,需要使用动态规划
下面讨论0-1背包问题的求解。

● 问题分析

用数学语言描述该问题,就是找到一个s的子集s‘满足:
0-1背包问题数学表达
前式称为目标函数,后式称为约束方程

● 建立方程

p[i,j] 表示在可选物品为s’={i,i+1…n}和剩余容量为j的情况下,对应的最大价值即最优值。
对于任意的p[i,j],都有如下关系:

  1. 当wi>j时,无法装入物品i,因此最优值一定为p[i+1,j];
  2. 当wi<j时,可以装入物品,此时最优值可能为p[i+1,j-wi]+vi,需要取较大值。

由此得到动态规划方程
0-1背包问题的动态规划方程

● 计算最优值

由动态规划方程,可以设计算法计算最优值:

//n为物品总数,c为最大容量,w储存物品重量、v储存物品价值、p用于递推
void knapSack(int n,int c,int *w,int *v,int **p)
{
    //jMax表示物品能否存入的边界情况;防止出现c < w[i]的情况造成越界
    int jMax = min(w[n]-1,c);
    //对小于w[n]的j
    for(int j = 0;j <= jMax;j++)
        p[n][j] = 0;
    //对大于等于w[n]的j
    for(int j = w[n];j <= c;j++)
        p[n][j] = v[n];
    
    //由动态规划方程递推
    for(int i = n - 1;i > 0;i--){
        jMax = min(w[i]-1,c);
        for(int j = 0;j <= jMax;j++)
            p[i][j] = p[i+1][j];
        for(int j = w[i];j <= c;j++)
            p[i][j] = max(p[i+1][j],p[i+1][j-w[i]]+v[i]);
    }
}
● 构造最优解

计算最优值的过程已经获得了构造最优解的所需信息。

//x储存物品的选择情况;0为未选择,1为已选择
void traceBack(int c,int n,int* w,int *v,int **p,int *x)
{
    for(int i = 1;i < n;i++){
        if(p[i][c] == p[i+1][c])
            x[i] = 0;
        else{
            x[i] = 1;
            c -= w[i];
        }
    }
    x[n] = p[n][c] == 0 ? 0 : 1;
}

动态规划在不同的问题中,难度也有变化。在刷题时,发现难题的动态规划简直看答案都看不明白。这个算法还需要多多体会、多多学习。

发布了39 篇原创文章 · 获赞 4 · 访问量 2046

猜你喜欢

转载自blog.csdn.net/weixin_44712386/article/details/104827580