本文是仔细看完算法导论的总结,就是把算法导论中个人认为关键地方写出来,做个笔记吧,加深印象。
篇幅有限,比较概括,不明白的可以直接看书或其他博客,写的都很好。
一、 动态规划概述
动态规划(dynamicprogramming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。
动态规划是通过组合子问题的解进而解决整个问题的。动态规划适用于问题包含子问题,各子问题又包含公共的子子问题。动态规划对每个子子问题只求解一次,将其结果保存在一张表(或者说是一个二维矩阵)中,在求解子问题的过程中查表递归调用子子问题的解,从而避免每次遇到各个子子问题时重新计算。
动态规划算法的设计分为如下4个步骤:
1) 描述最优解的结构。
描述最优解的结构也就是1、判断这个问题是否具有最优子结构,如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。2、子问题是否是重叠子问题,就是说用来解原问题的递归算法可以反复地解同样的子问题,而不会产生新问题。这样的问题就适用于动态规划算法。
2) 递归定义最优解的值。
最优子结构保证了最优解可以使用子问题的最优解,逐层递归调用,所以我们可以写出问题解的递归定义。
3) 按自底向上的方式计算最优解的值。
自底向上地计算子(子)问题的最优解,并存入表中,在解父问题的最优解调用子问题最优解的过程中就可以查表得到值而无需重复计算,降低时间复杂度。这也是典型的空间换时间应用。
4) 由计算出的结构构造最优解。
跟踪规划中查表的过程,显示最优解的构造过程。
具体理论描述大家可以参考网络上资源,对于理论有个大概认识,然后就可以直接做习题加深理解了。
几个典型的适用于动态规划的习题:
二、 矩阵链乘法
矩阵相乘是满足结合律的,所以无论怎样加括号都会产生相同的结果。
但是,加括号会改变矩阵相乘的运算顺序,而运算顺序会对运算代价产生很大影响。这一点大家可以找三四个矩阵试一试,加不同括号看看运算量如何。
用动态规划解决矩阵链相乘问题并不是要算出矩阵相乘结果,而是找到一种最优的加括号方案,使得相乘运算量最小。
1. 最优子结构
假设AiAi+1…Aj的一个最优加全部括号把乘积在Ak与Ak+1处分开,则对AiAi+1…Aj的“前缀”子链AiAi+1…Ak的加全部括号必然是AiAi+1…Ak的一个最优加全部括号,同样Ak+1Ak+2…Aj的加全部括号必然是Ak+1Ak+2…Aj的最优加全部括号。(请深入理解此句话,并注意最优加全部括号和加全部括号用词区别)
2. 写出递归解
确定了问题具有最优子结构,可以写出问题的递归解。
定义m[i,j]为计算矩阵链AiAi+1…Aj相乘所需标量乘法运算次数的最小值;
如果i=j,m[i,i] = 0;如果i<j,假设最优加全部括号将AiAi+1…Aj从Ak与Ak+1处分开,其中i<=k<j,因此m[i,j]就等于计算子乘积AiAi+1…Ak和Ak+1Ak+2…Aj的代价,再加上前后两个子乘积相乘的代价的最小值。即:
由于k是未知的,所以得遍历k的取值,选取计算得到的最小值作为m[i,j]的最优解,此时k的位置是最优加括号位置。
3. 自底向上计算最优解
增加一个数组s[i,j]记录k的位置,便于跟踪最优加括号过程,构造下一步的最优解。
/**
*
* @param p
* 矩阵维度数组
* @param m
* 存放最优解表
* @param s
* 存放最优加全部括号位置
* @param n
* 矩阵个数
*/
publicvoid matrix_chain_order(int[] p, int[][] m,int[][] s,int n) {
//TODO Auto-generated method stub
for (int i = 1; i <= n; i++){
m[i][i]= 0;
}
for (int l = 2; l <= n; l++){
for (int i = 1; i <= n - l +1; i++) {
int j = i + l - 1;
m[i][j]= Integer.MAX_VALUE;
for (int k = i; k <= j - 1;k++) {
int q = m[i][k] + m[k +1][j] + p[i - 1] * p[k] * p[j];
if (q < m[i][j]) {
m[i][j]= q;
s[i][j]= k;
}
}
}
}
// System.out.println("m[1][6] = " + m[1][n]);
// System.out.println("m[2][5] = " + m[2][5]);
}
求最优解的过程就是填表的过程,大家可以自己做表按照算法过程一步一步填进去~由于i<j,最后得到的表是一个仅对角线以上被用到的二维矩阵。请亲自动手试试。
4. 构造一个最优解
根据矩阵s[i,j]可以跟踪最优加全部括号的过程,构造出最优解。
/**
* 打印最优家全部括号
*
* @params
* @param i
* @param j
*/
publicvoid print_optimal_parens(int[][]s,int i,int j) {
//TODO Auto-generated method stub
if (i == j) {
System.out.print("A" + i);
}else{
System.out.print("(");
int k =s[i][j];
print_optimal_parens(s, i, k);
print_optimal_parens(s, k + 1, j);
System.out.print(")");
}
}
完整代码:
package dynamicprogramming;
publicclassMatrixChain {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
//TODO Auto-generated method stub
int n = 6;// 6个矩阵组成的链相乘
int[] p = { 30, 35, 15, 5,10, 20, 25 };//矩阵链的维度,Ai的维度为Pi-1 * Pi;
//盛装Ai。。。j标量乘法运算次数的最小值.由于我们规定矩阵链中i<j,故m是个上三角矩阵
int[][] m =newint[7][7];
int[][] s =newint[7][7];//盛装最优分割点,便于跟踪
MatrixChaintester = newMatrixChain();
tester.matrix_chain_order(p,m, s, n);
tester.print_optimal_parens(s,1, 6);
//int count = tester.recursive_matrix_chain(p, m,1, 2);
// System.out.println(count);
}
/**
* 打印最优家全部括号
*
* @param s
* @param i
* @param j
*/
publicvoid print_optimal_parens(int[][] s, int i,int j) {
//TODO Auto-generated method stub
if (i == j) {
System.out.print("A" + i);
}else{
System.out.print("(");
int k = s[i][j];
print_optimal_parens(s,i, k);
print_optimal_parens(s,k + 1, j);
System.out.print(")");
}
}
/**
*
* @param p
* 矩阵维度数组
* @param m
* 存放最优解表
* @param s
* 存放最优加全部括号位置
* @param n
* 矩阵个数
*/
publicvoid matrix_chain_order(int[] p, int[][] m,int[][] s,int n) {
//TODO Auto-generated method stub
for (int i = 1; i <= n; i++){
m[i][i]= 0;
}
for (int l = 2; l <= n; l++){
for (int i = 1; i <= n - l +1; i++) {
int j = i + l - 1;
m[i][j]= Integer.MAX_VALUE;
for (int k = i; k <= j - 1;k++) {
intq = m[i][k] + m[k + 1][j] + p[i - 1] * p[k]* p[j];
if (q < m[i][j]) {
m[i][j]=q;
s[i][j]= k;
}
}
}
}
System.out.println("m[1][6] = "+ m[1][n]);
// System.out.println("m[2][5] = " + m[2][5]);
}
// /**
// *递归方式计算
// *
// * @param p
// * @param m
// * @param i
// * @param j
// * @return
// */
// publicint recursive_matrix_chain(int[]p,int[][] m, int i, int j) {
//int q;
// if (i == j) {
// return 0;
// } else {
// m[i][j] = Integer.MAX_VALUE;
// for (int k = 1; k <= j - 1; k++) {
// q = recursive_matrix_chain(p, m, i, k)
// + recursive_matrix_chain(p, m, k + 1, j) + p[i - 1]
// * p[k] * p[j];
// if (q < m[i][j]) {
// m[i][j] = q;
// }
//
// }
// }
// return m[i][j];
// }
}
运行结果:
m[1][6] = 15125
((A1(A2A3))((A4A5)A6))
三、 最长公共子序列
序列Z是序列X和序列Y的公共子序列,要求Z中的元素都出现在X和Y中,而且这些元素必须是以相同顺序出现的,但是不必要是连续的,其中最长的公共子序列叫做最长公共子序列(Longest-Common-Subsequence,LCS)。
1. 描述一个最长公共子序列
请深入理解如下LCS最优子结构定理:
设X={x1,x2,…,xm}和Y={y1,y2,…,yn}为两个序列,并设Z={z1,z2,…,zk}为X和Y的任意一个LCS。
1) 如果xm = yn,那么zk=xm=yn而且Zk-1是Xm-1和Yn-1的一个LCS。
2) 如果xm≠yn,并且zk≠xm,则Z是Xm-1和Y的一个LCS。
3) 如果xm≠yn,并且zk≠yn,则Z是X和Yn-1的一个LCS。
通过2)3)可以总结出,如果xm≠yn,则Z是Xm-1和Y的LCS与X和Yn-1的LCS中最长的一个。
以上定理说明两个序列的一个LCS也包含了两个序列的前缀的一个LCS。这就说明LCS问题具有最优子结构。
2. 写出递归解
记c[i,j]为X长度为i的前缀和Y长度为j的前缀的LCS的长度。则
3. 计算LCS长度
定义b[i,j]记录动态规划中选择最优解的过程。c[i,j]包含X和Y的一个LCS的长度。
/**
*
* @param x
* x序列
* @param y
* y序列
* @param c
* 存放lcs长度
* @param b
* 存放最优解选择方向
*/
publicvoid lcs_length(int[] x, int[] y,int[][] c,int[][] b) {
int m = x.length;
intn = y.length;
for (int i = 1; i <= m; i++){
c[i][0]= 0;
}
for (int j = 1; j <=n; j++) {
c[0][j]= 0;
}
for (int i = 1; i <= m; i++){
for (int j = 1; j <=n; j++) {
if (x[i - 1] == y[j - 1]){
c[i][j]= c[i - 1][j - 1] + 1;
b[i][j]= LCS.LEFT_UP;
}elseif(c[i - 1][j] >= c[i][j - 1]) {
c[i][j]= c[i - 1][j];
b[i][j]= LCS.UP;
}else{
c[i][j]= c[i][j - 1];
b[i][j]= LCS.LEFT;
}
}
}
}
4. 构造最优解LCS
根据b[i,j]表项跟踪选择过程,构造一个最优解。
/**
*
* @param x
* @param b
* @param i
* x前缀的长度
* @param j
* y前缀的长度
*/
publicvoid print_lcs(int[] x, int[][] b,int i,int j) {
if (i == 0 || j == 0) {
return;
}
if (b[i][j] == LCS.LEFT_UP) {
print_lcs(x,b, i - 1, j - 1);
System.out.print(Character.valueOf((char) x[i - 1]));
}elseif(b[i][j] == LCS.UP){
print_lcs(x,b, i - 1, j);
}else{
print_lcs(x,b, i, j - 1);
}
}
完整代码:
package dynamicprogramming;
publicclassLCS {
finalstaticintUP = 1;
finalstaticintLEFT = 2;
finalstaticintLEFT_UP = 3;
/**
* @param args
*/
publicstaticvoid main(String[] args) {
//TODO Auto-generated method stub
int[] A = {'A','B','C','B','D','A','B' };
int[] B = {'B','D','C','A','B','A' };
int[][] c =newint[A.length + 1][B.length + 1];
int[][] b =newint[A.length + 1][B.length + 1];
LCStester = newLCS();
tester.lcs_length(A,B, c, b);
System.out.println("LCS.length = "+ c[7][6]);
tester.print_lcs(A,b, 7, 6);
}
/**
*
* @param x
* x序列
* @param y
* y序列
* @param c
* 存放lcs长度
* @param b
* 存放最优解选择方向
*/
publicvoid lcs_length(int[] x, int[] y,int[][] c,int[][] b) {
int m = x.length;
int n = y.length;
for (int i = 1; i <= m; i++){
c[i][0]= 0;
}
for (int j = 1; j <= n; j++){
c[0][j]= 0;
}
for (int i = 1; i <= m; i++){
for (int j = 1; j <= n; j++){
if (x[i - 1] == y[j - 1]){
c[i][j]= c[i - 1][j - 1] + 1;
b[i][j]= LCS.LEFT_UP;
}elseif(c[i - 1][j] >= c[i][j - 1]) {
c[i][j]= c[i - 1][j];
b[i][j]= LCS.UP;
}else{
c[i][j]= c[i][j - 1];
b[i][j]= LCS.LEFT;
}
}
}
}
/**
*
* @param x
* @param b
* @param i
* x前缀的长度
* @param j
* y前缀的长度
*/
publicvoid print_lcs(int[] x, int[][] b,int i,int j) {
if (i == 0 || j == 0) {
return;
}
if (b[i][j] == LCS.LEFT_UP) {
print_lcs(x,b, i - 1, j - 1);
System.out.print(Character.valueOf((char) x[i - 1]));
}elseif(b[i][j] == LCS.UP){
print_lcs(x,b, i - 1, j);
}else{
print_lcs(x,b, i, j - 1);
}
}
}
输出结果:
LCS.length = 4
BCBA
放一张本例的配图,希望大家把这个图自己画出来,就明白差不多了~