算法导论-第15章-动态规划

动态规划(dynamic programming)的思想是分治思想解决冗余

  • 与分治法相似的是
    • 将原问题分解为若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
  • 与分治法不同的是
    • 经分解的子问题往往不是相互独立的。若用分治法来解,有些共同部分(子问题或子子问题)被重复计算了很多次。
    • 而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都需重新计算。

动态规划方法通常用来求解最优化问题(optimization problem)。这类问题可以有很多可行的解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都是最优值。

我们通常按如下4步骤来设计一个动态规划算法:

  1. 刻画一个最优解的结构特征。
  2. 递归地定义最优解的值(写出动态规划方程)。
  3. 计算最优解的值,通常采用自底向上的方法。
  4. 利用计算出的信息构造一个最优解。

步骤1~3是动态规划求解问题的基础。如果我们仅仅需要最优解的一个值,而不是最优解本身,可以忽略步骤4。如果要构造最优解,则必须执行步骤4,步骤3中维护的一些额外信息是构造最优解的基础。

下面将讨论如何用动态规划求解最优解问题。15.1节将讨论如何将长钢条切割成短钢条,使其总价值最高。15.2节将讨论如何使用最少次数的标量乘法计算矩阵链乘。15.3节将讨论适合用动态规划求解的问题应该具备的两个关键特征。15.4节将讨论如何使用动态规划求解两个序列的最长公共子序列。15.5节用动态规划方法解决在已知关键字分布的前提下,如何构造最优二叉搜索树。

15.1 钢条切割

钢条切割问题:给定一段长度为 n n n 英寸的钢条和一个价格表 p i ( i = 1 , 2 , 3 , ⋯   , n ) p_i(i=1, 2, 3, \cdots, n) pi(i=1,2,3,,n)。求切割钢条的方案,使得销售收益 r n r_n rn 最大。注意,如果长度为 n n n 英寸的钢条的价格 p n p_n pn 足够大,最优解可能就是完全不需要切割。

假设Serling公司出售一段长度为 i i i 英寸的钢条的价格为 p i p_i pi。钢条的长度均为整英寸,价格表如下:

长度 i i i12345678910
价格 p i p_i pi1589101717202430

考虑 n = 4 n=4 n=4 的情况。下图给出了4英寸钢条所有可能的切割方案,包括根本不切割的方案。最优解为方案c。

https://typora-markdown-oss.oss-cn-beijing.aliyuncs.com/Figure%2014.2.png

长度为 n n n 的钢条有 2 n − 1 2^{n-1} 2n1 种不同的切割方案,因为在距离钢条左端 i ( i = 1 , 2 , ⋯   , n − 1 ) i(i=1,2,\cdots,n-1) i(i=1,2,,n1)英寸处,都有切割或不切割两种选择。

如果一个最优解将钢条切割为 k ( 1 ≤ k ≤ n ) k(1 \le k \le n) k(1kn) 段,那么最优切割方案
n = i 1 + i 2 + ⋯ + i k n=i_1 + i_2 + \cdots + i_k n=i1+i2++ik
最大收益为
r n = p i 1 + p i 2 + ⋯ + p i k r_n=p_{i_1}+p_{i_2}+\cdots+p_{i_k} rn=pi1+pi2++pik
一般地,对于 r n ( n ≥ 1 ) r_n(n \ge1) rn(n1),可以用更短的钢条的最优切割收益来描述它:
r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , ⋯   , r n − 1 + r 1 ) r_n=max(p_n, r_1+r_{n-1}, r_2+r_{n-2},\cdots,r_{n-1}+r_1) rn=max(pn,r1+rn1,r2+rn2,,rn1+r1)
其中 p n p_n pn 表示不切割,其他参数对应切割为长度为 i i i n − i n-i ni 的两段,接着求这两段短钢条的最优收益 r i r_i ri r n − i r_{n-i} rni。由于无法预知哪种划分方案会获得最大收益,需要遍历所有可能的 i i i,选取其中收益最大者。

15.4 最长公共子序列

[轻松掌握动态规划]5.最长公共子序列 LCS_哔哩哔哩_bilibili

子序列(subsequence):给定一个序列 X = < x 1 , x 2 , ⋯   , x m > X=<x_1, x_2, \cdots, x_m> X=<x1,x2,,xm>,另一个序列 Z = < z 1 , z 2 , ⋯   , z k > Z=<z_1, z_2, \cdots, z_k> Z=<z1,z2,,zk>,存在一个严格递增的 X X X 的下标序列 < i 1 , i 2 , ⋯   , i k > <i_1, i_2, \cdots, i_k> <i1,i2,,ik>,对所有 j = 1 , 2 , ⋯   , k j=1,2,\cdots,k j=1,2,,k,满足 x i j = z j x_{i_j}=z_j xij=zj。例如, Z = < B , C , D , B > Z=<B, C, D, B> Z=<B,C,D,B> X = < A , B , C , B , D , A , B > X=<A, B, C, B, D, A, B> X=<A,B,C,B,D,A,B>的子序列,对应的下标序列为 < 2 , 3 , 5 , 7 > <2, 3, 5, 7> <2,3,5,7>

公共子序列(common subsequence):给定两个序列 X X X Y Y Y,如果 Z Z Z 既是 X X X 的子序列,也是 Y Y Y 的子序列,我们称它是 X X X Y Y Y 的公共子序列。例如, X = < A , B , C , B , D , A , B > X=<A, B, C, B, D, A, B> X=<A,B,C,B,D,A,B> Y = < B , D , C , A , B , A > Y=<B, D, C, A, B, A> Y=<B,D,C,A,B,A>,那么序列 < B , C , A > <B, C, A> <B,C,A> 就是 X X X Y Y Y 的公共子序列,但不是最长的公共子序列(LCS),因为它的长度为3;而 < B , C , B , A > <B, C, B, A> <B,C,B,A> 也是 X X X Y Y Y 的公共子序列,其长度为4。 < B , C , B , A > <B, C, B, A> <B,C,B,A> X X X Y Y Y 的最长公共子序列, < B , D , A , B > <B, D, A, B> <B,D,A,B> 也是最长公共子序列,因为 X X X Y Y Y 不存在长度大于等于5的公共子序列。

最长公共子序列问题(longest-common-subsequence problem):给定两个序列 X = < x 1 , x 2 , ⋯   , x m > X=<x_1, x_2, \cdots, x_m> X=<x1,x2,,xm> Y = < y 1 , y 2 , ⋯   , y n > Y=<y_1, y_2, \cdots, y_n> Y=<y1,y2,,yn>,求 X X X Y Y Y 的最长公共子序列。

步骤1:刻画最长公共子序列的特征

定理:LCS的最优子结构

X = < x 1 , x 2 , ⋯   , x m > X=<x_1, x_2, \cdots, x_m> X=<x1,x2,,xm> Y = < y 1 , y 2 , ⋯   , y n > Y=<y_1, y_2, \cdots, y_n> Y=<y1,y2,,yn> 为两个序列, Z = < z 1 , z 2 , ⋯   , z k > Z=<z_1, z_2, \cdots, z_k> Z=<z1,z2,,zk> X X X Y Y Y 的任意LCS。

  1. 如果 x m = y n x_m=y_n xm=yn,则 z k = x m = y n z_k=x_m=y_n zk=xm=yn Z k − 1 Z_{k-1} Zk1 X m − 1 X_{m-1} Xm1 Y n − 1 Y_{n-1} Yn1 的一个 LCS。
  2. 如果 x m ≠ y n x_m \ne y_n xm=yn z k ≠ x m z_k \ne x_m zk=xm,那么 Z Z Z X m − 1 X_{m-1} Xm1 Y Y Y 的一个 LCS。
  3. 如果 x m ≠ y n x_m \ne y_n xm=yn z k ≠ y n z_k \ne y_n zk=yn,那么 Z Z Z X X X Y n − 1 Y_{n-1} Yn1 的一个 LCS。

步骤2:一个递归解

根据LCS的最优子结构定理可知,在求 X = < x 1 , x 2 , ⋯   , x m > X=<x_1, x_2, \cdots, x_m> X=<x1,x2,,xm> Y = < y 1 , y 2 , ⋯   , y n > Y=<y_1, y_2, \cdots, y_n> Y=<y1,y2,,yn> 的一个LCS时,我们需要求解一个或两个子问题。如果 x m = y n x_m=y_n xm=yn,我们应该求解 X m − 1 X_{m-1} Xm1 Y n − 1 Y_{n-1} Yn1 的一个LCS,将 x m = y n x_m=y_n xm=yn 追加到这个LCS的末尾,就得到 X X X Y Y Y 的一个LCS。如果 x m ≠ y n x_m \ne y_n xm=yn,我们必须求解两个子问题:求 X m − 1 X_{m-1} Xm1 Y Y Y 的一个 LCS 与 X X X Y n − 1 Y_{n-1} Yn1 的一个 LCS。两个LCS中较长者即为 X X X Y Y Y 的一个LCS。

根据LCS的最优子结构性质,可得如下公式:
c [ i , j ] = { 0 若 i=0 或 j=0 c [ i − 1 , j − 1 ] + 1 若 i, j > 0 且  x i = y j m a x ( c [ i , j − 1 ] , c [ i − 1 , j ] ) 若 i, j > 0 且  x i ≠ y j c[i, j]=\begin{cases} 0 &\text{若 i=0 或 j=0} \\ c[i-1,j-1]+1 &\text{若 i, j > 0 且 }x_i=y_j \\ max(c[i, j-1], c[i-1, j]) &\text{若 i, j > 0 且 }x_i \ne y_j\end{cases} c[i,j]= 0c[i1,j1]+1max(c[i,j1],c[i1,j]) i=0  j=0 i, j > 0  xi=yj i, j > 0  xi=yj

步骤3:计算LCS的长度

LCS问题有 Θ ( m n ) \Theta(mn) Θ(mn) 个不同的子问题,可以采用自底向上动态规划进行计算。

LCS-LENGTH过程接受两个序列 X = < x 1 , x 2 , ⋯   , x m > X=<x_1, x_2, \cdots, x_m> X=<x1,x2,,xm> Y = < y 1 , y 2 , ⋯   , y n > Y=<y_1, y_2, \cdots, y_n> Y=<y1,y2,,yn> 作为输入。将 c [ i , j ] c[i,j] c[i,j] 的值保存在表 c [ 0.. m , 0.. n ] c[0..m, 0..n] c[0..m,0..n] 中,并按照行主次序(row-major order)计算表项(即首先从左到右计算 c 的第一行,然后计算第二行,以此类推)。过程中还维护了一个表 b [ 1.. m , 1.. n ] b[1..m, 1..n] b[1..m,1..n],帮助构造最优解。

LCS-LENGTH(X, Y, m, n)
    let b[1 : m, 1 : n] and c[0 : m, 0 : n] be new tables
    for i = 1 to m
        c[i, 0] = 0
    for j = 0 to n
        c[0, j] = 0
    for i = 1 to m   // compute table entries in row-major order
        for j = 1 to n
            if x_i == y_j
                c[i, j] = c[i - 1, j - 1] + 1
                b[i, j] = "↖"
            else if c[i - 1, j] ≥ c[i, j - 1]
                c[i, j] = c[i - 1, j]
                b[i, j] = "↑"
            else c[i, j] = c[i, j - 1]
                b[i, j] = "←"
    return c and b

下图显示了LCS-LENGTH对输入序列 X = < A , B , C , B , D , A , B > X=<A, B, C, B, D, A, B> X=<A,B,C,B,D,A,B> Y = < B , D , C , A , B , A > Y=<B, D, C, A, B, A> Y=<B,D,C,A,B,A> 计算的结果。运行时间为 Θ ( m n ) \Theta(mn) Θ(mn),因为每个表项的计算时间为 Θ ( 1 ) \Theta(1) Θ(1)

Figure 14.8

步骤4:构造LCS

要构造LCS,只需从 b [ m , n ] b[m, n] b[m,n] 开始,按箭头追踪下去即可。当在表项中遇到”↖“时,意味着 x i = y j x_i=y_j xi=yj 是LCS的一个元素。按照这种方法,就可以依次构造出LCS的所有元素。

PRINT-LCS(b, X, i, j)
    if i == 0 or j == 0
        return    // the LCS has length 0
    if b[i, j] == "↖"
        PRINT-LCS(b, X, i - 1, j - 1)
        print x_i    // same as y_i
    else if b[i, j] == "↑"
        PRINT-LCS(b, X, i - 1, j)
    else PRINT-LCS(b, X, i, j - 1)

PRINT-LCS(b, X, m, n)

也可以不依赖表 b。每个 c [ i , j ] c[i, j] c[i,j] 项只依赖于表 c 中的其他三项: c [ i − 1 , j ] 、 c [ i , j − 1 ] 、 c [ i − 1 , j − 1 ] c[i-1, j]、c[i, j-1]、c[i-1, j-1] c[i1,j]c[i,j1]c[i1,j1]。这种方法节省了 Θ ( m n ) \Theta(mn) Θ(mn) 的空间。此时的空间复杂度是 Θ ( m n ) \Theta(mn) Θ(mn)

对空间复杂度进行优化

使用滚动数组对空间复杂度进行优化。

LCS的代码实现

编程实现最长公共子序列(LCS)算法,并理解其核心思想。

  1. 时间复杂度 O ( m n ) \Omicron(mn) O(mn),空间复杂度 O ( m n ) \Omicron(mn) O(mn),求出LCS及其长度。
  2. 时间复杂度 O ( m n ) \Omicron(mn) O(mn),空间复杂度 O ( 2 ∗ m i n ( m , n ) ) \Omicron(2 * min(m, n)) O(2min(m,n)),求出LCS的长度。
  3. 时间复杂度 O ( m n ) \Omicron(mn) O(mn),空间复杂度 O ( m i n ( m , n ) ) \Omicron(min(m, n)) O(min(m,n)),求出LCS的长度。

输入:由控制台输入两个字符串text1,text2。

输出:控制台打印LCS的长度及相应的序列,如不存在公共子序列则返回0.

package ch15;

import java.util.Scanner;

public class LCS {
    /**
     * 最长公共子序列,空间复杂度O(mn)
     * @param seq1 序列1
     * @param seq2 序列2
     * @return LCS及其长度
     */
    public static String longestCommonSubsequence(String seq1, String seq2) {
        int m = seq1.length();
        int n = seq2.length();

        // 创建一个二维数组来保存子问题的解
        int[][] dp = new int[m + 1][n + 1];

        // 动态规划求解最长公共子序列
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (seq1.charAt(i - 1) == seq2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        // 从二维数组的右下角开始回溯,构造最长公共子序列
        StringBuilder lcs = new StringBuilder();
        int i = m, j = n;
        while (i > 0 && j > 0) {
            if (seq1.charAt(i - 1) == seq2.charAt(j - 1)) {
                lcs.insert(0, seq1.charAt(i - 1));
                i--;
                j--;
            } else if (dp[i - 1][j] > dp[i][j - 1]) {
                i--;
            } else {
                j--;
            }
        }
        return lcs.toString();
    }

    /**
     * 最长公共子序列,空间复杂度2 * O(min(m, n))
     * @param seq1 序列1
     * @param seq2 序列2
     * @return LCS长度
     */
    public static int longestCommonSubsequencesPro(String seq1, String seq2) {
        int m = seq1.length();
        int n = seq2.length();

        // 确保 n 是较小的值,以减小空间复杂度
        if (m < n) {
            String temp = seq1;
            seq1 = seq2;
            seq2 = temp;
            int tempLength = m;
            m = n;
            n = tempLength;
        }

        // 创建两行滚动数组来保存子问题的解
        int[][] dp = new int[2][n + 1];

        // 动态规划求解最长公共子序列的长度
        for (int i = 1; i <= m; i++) {
            int currRow = i % 2;
            int prevRow = (i - 1) % 2;
            for (int j = 1; j <= n; j++) {
                if (seq1.charAt(i - 1) == seq2.charAt(j - 1)) {
                    dp[currRow][j] = dp[prevRow][j - 1] + 1;
                } else {
                    dp[currRow][j] = Math.max(dp[prevRow][j], dp[currRow][j - 1]);
                }
            }
        }
        return dp[m % 2][n];
    }

    /**
     * 最长公共子序列,空间复杂度O(min(m, n))
     * @param seq1 序列1
     * @param seq2 序列2
     * @return LCS长度
     */
    public static int longestCommonSubsequenceLengthUltra(String seq1, String seq2) {
        int m = seq1.length();
        int n = seq2.length();

        // 确保 n 是较小的值,以减小空间复杂度
        if (m < n) {
            String temp = seq1;
            seq1 = seq2;
            seq2 = temp;
            int tempLength = m;
            m = n;
            n = tempLength;
        }

        // 创建一维数组来保存子问题的解
        int[] dp = new int[n + 1];

        // 动态规划求解最长公共子序列的长度
        for (int i = 1; i <= m; i++) {
            int prev = 0;
            for (int j = 1; j <= n; j++) {
                int current = dp[j];
                if (seq1.charAt(i - 1) == seq2.charAt(j - 1)) {
                    dp[j] = prev + 1;
                } else {
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }
                prev = current;
            }
        }
        return dp[n];
    }


    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String seq1 = scanner.next();
        String seq2 = scanner.next();


        // 最长公共子序列,空间复杂度O(mn)
        String lcs = longestCommonSubsequence(seq1, seq2);
        if (!lcs.equals("")) {
            System.out.println("LCS: " + lcs + ", 长度为: " + lcs.length());
        }else {
            System.out.println("0");
        }

        // // 最长公共子序列,空间复杂度2 * O(min(m, n))
        //int res = longestCommonSubsequencesPro(seq1, seq2);
        //System.out.println(res);


         最长公共子序列,空间复杂度O(min(m, n))
        //int res = longestCommonSubsequenceLengthUltra(seq1, seq2);
        //System.out.println(res);
    }
}

理解最优子结构

最优子结构是指一个问题具有递归结构,并且原问题的最优解可以通过一系列子问题的最优解来构建得到。换句话说,如果一个问题的最优解可以通过其子问题的最优解来推导得出,那么这个问题就具有最优子结构

最优子结构是动态规划算法中的一个关键概念。在使用动态规划解决问题时,我们将问题划分为若干个子问题,并通过递归地解决子问题来获得原问题的最优解。通过最优子结构的特性,我们可以在求解子问题时将其最优解进行存储,以便后续的计算和使用。

通过将问题划分为子问题,并利用最优子结构的特性,动态规划算法能够避免重复计算,提高效率,并且能够在多项式时间内求解复杂的问题。因此,最优子结构是动态规划算法的一个重要性质,对于设计和分析动态规划算法非常关键。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值