赞
踩
动态规划(dynamic programming)的思想是分治思想和解决冗余。
动态规划方法通常用来求解最优化问题(optimization problem)。这类问题可以有很多可行的解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都是最优值。
我们通常按如下4步骤来设计一个动态规划算法:
步骤1~3是动态规划求解问题的基础。如果我们仅仅需要最优解的一个值,而不是最优解本身,可以忽略步骤4。如果要构造最优解,则必须执行步骤4,步骤3中维护的一些额外信息是构造最优解的基础。
下面将讨论如何用动态规划求解最优解问题。15.1节将讨论如何将长钢条切割成短钢条,使其总价值最高。15.2节将讨论如何使用最少次数的标量乘法计算矩阵链乘。15.3节将讨论适合用动态规划求解的问题应该具备的两个关键特征。15.4节将讨论如何使用动态规划求解两个序列的最长公共子序列。15.5节用动态规划方法解决在已知关键字分布的前提下,如何构造最优二叉搜索树。
钢条切割问题:给定一段长度为 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 i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
价格 p i p_i pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
考虑 n = 4 n=4 n=4 的情况。下图给出了4英寸钢条所有可能的切割方案,包括根本不切割的方案。最优解为方案c。
长度为 n n n 的钢条有 2 n − 1 2^{n-1} 2n−1 种不同的切割方案,因为在距离钢条左端 i ( i = 1 , 2 , ⋯ , n − 1 ) i(i=1,2,\cdots,n-1) i(i=1,2,⋯,n−1)英寸处,都有切割或不切割两种选择。
如果一个最优解将钢条切割为
k
(
1
≤
k
≤
n
)
k(1 \le k \le n)
k(1≤k≤n) 段,那么最优切割方案
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(n≥1),可以用更短的钢条的最优切割收益来描述它:
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+rn−1,r2+rn−2,⋯,rn−1+r1)
其中
p
n
p_n
pn 表示不切割,其他参数对应切割为长度为
i
i
i 和
n
−
i
n-i
n−i 的两段,接着求这两段短钢条的最优收益
r
i
r_i
ri 和
r
n
−
i
r_{n-i}
rn−i。由于无法预知哪种划分方案会获得最大收益,需要遍历所有可能的
i
i
i,选取其中收益最大者。
[轻松掌握动态规划]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 的最长公共子序列。
定理: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。
根据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} Xm−1 和 Y n − 1 Y_{n-1} Yn−1 的一个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} Xm−1 和 Y Y Y 的一个 LCS 与 X X X 和 Y n − 1 Y_{n-1} Yn−1 的一个 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]=
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)。
要构造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[i−1,j]、c[i,j−1]、c[i−1,j−1]。这种方法节省了 Θ ( m n ) \Theta(mn) Θ(mn) 的空间。此时的空间复杂度是 Θ ( m n ) \Theta(mn) Θ(mn)。
使用滚动数组对空间复杂度进行优化。
编程实现最长公共子序列(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); } }
最优子结构是指一个问题具有递归结构,并且原问题的最优解可以通过一系列子问题的最优解来构建得到。换句话说,如果一个问题的最优解可以通过其子问题的最优解来推导得出,那么这个问题就具有最优子结构。
最优子结构是动态规划算法中的一个关键概念。在使用动态规划解决问题时,我们将问题划分为若干个子问题,并通过递归地解决子问题来获得原问题的最优解。通过最优子结构的特性,我们可以在求解子问题时将其最优解进行存储,以便后续的计算和使用。
通过将问题划分为子问题,并利用最优子结构的特性,动态规划算法能够避免重复计算,提高效率,并且能够在多项式时间内求解复杂的问题。因此,最优子结构是动态规划算法的一个重要性质,对于设计和分析动态规划算法非常关键。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。