动态规划
一、简介
动态规划(dynamic programming)与分治方法很像,都是通过组合子问题的解来求解原问题。
分治方法将问题划分为互不相交的子问题,递归的求解子问题,再讲它们的解组合起来,求出原问题的解。
与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治算法会做许多不必要的工作,他会反复地求解那些公共子问题。而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了这种不必要的计算工作。
动态规划解决的问题通常有两个特征:
1.最优化问题
2.子问题重叠
二、动态规划的几个例子
1、钢条切割
问题定义:给定一段长度为n英寸的钢条和一个价格表 pi(i=1,2,...,n) ,求切割钢条方案,使得销售收益 rn 最大。
价格表样例,每段长度为
i
英寸的钢条为公司带来
使用普通的自顶向上的递归算法实现的解决思路如下:
//p是价格表,n是钢条长度
int cut_rod( int *p, int n )
{
if ( n == 0 )
return 0;
int q = 0x80000000; //先给q赋值一个尽可能小的值
for ( int i = 1; i <= n; i++ )
{
q = max( q, p[i]+cut_rod( p, n-i ) );
}
return q;
}
int main( void )
{
int p[] = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30 };
int result = memoized_cut_rod( p, 10 );
cout << result << endl;
return 0;
}
递归算法因为要对每一个子问题进行重复求解,所以在输入较大的n时,时间效率太低。
使用动态规划可以解决递归算法中对一个子问题进行重复求解而导致的低效率问题。解决的主要思路有:
1.带备忘的自顶向下:在递归过程中,每求得一个子问题的解,我们就将其保存下来,当下次需要求解一个子问题的解时,先检查这个子问题是否被保存过了。如果已经保存过了,则直接返回这个子问题的解。否则,再以递归的方式求解这个子问题的解。我们称这个递归过程是带备忘的,是因为它记住了之前已经计算过的结果。
2.自底向上法:因为任何子问题的求解都只依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已经求解完成。
下面是带备忘的自顶向下解法:
int memoized_cut_rod_aux( int *p, int n, int *r )
{
if ( r[n] >= 0 )
return r[n];
int q;
if ( n == 0 )
q = 0;
else
{
q = 0x80000000;
for ( int i = 1; i <= n; i++ )
{
q = max( q, p[i]+memoized_cut_rod_aux( p, n-i, r ) );
}
}
r[n] = q;
return q;
}
int memoized_cut_rod( int *p, int n )
{
int r[n+1];
for ( int i = 0; i <= n; i++ )
{
r[i] = 0x80000000;
}
return memoized_cut_rod_aux( p, n, r );
}
下面是自顶向上版本的解法:
int bottom_up_cut_rod( int *p, int n )
{
int r[n];
r[0] = 0;
int q;
for ( int j = 1; j <= n; ++j )
{
q = 0x80000000;
for ( int i = 1; i <= j; ++i )
{
q = max( q, p[i]+r[j-i] );
}
r[j] = q;
}
return r[n];
}
2、矩阵链乘法
矩阵链乘法问题可描述如下:给定n个矩阵的链 <A1,A2,....,An> <script type="math/tex" id="MathJax-Element-60"> </script> ,矩阵 Ai 的规模为 Pi−1∗Pi(1<=i<=n) ,求完全括号化方案,使得计算乘积 A1A2...An 所需标量乘法次数最少。
分析:
1、刻画一个最优解的结构特征
为了构造一个矩阵链乘法问题实例的最优解,我们可以将问题划分为两个子问题( AiAi+1...Ak 和 Ak+1Ak+2...Aj 的最优括号化问题),求出子问题实例的最优解,然后将子问题的最优解组合起来。我们必须保证在确定分割点时,已经考察了所有可能的划分点,这样就可以保证不会遗漏最优解。
2、递归的定义最优解的值
用 m[i,j] 表示计算矩阵 Ai,j 所需标量乘法次数的最小值,原问题的最优解—计算 A1...n 所需的最低代价就是 m[1,n] 。可以得出求解原问题的递归求解公式为:
3、运用动态规划的思想优化2中的递归方案,通常采用自底向上或者带备忘的自顶向下方法。
如果直接根据上面的公式写递归算法,时间效率上并不比暴力搜索的方法好,因为递归过程中子问题有重叠,所以我们要运用动态规划的思想优化递归方案。
(1)、自底向上表格法:
来自算法导论的伪代码:
MATRIX_CHAIN_ORDER(p)
n = p.length - 1
let m[1...n, 1...n] and s[1...n-1, 2...n] be new tables
for i = 1 to n
m[i, i] = 0
for l = 2 to n
for i = 1 to n-l+1
j = i + 1 - l
m[i, j] = 0x07fffffff
for k = i to j-1
q = m[i, k] + m[k+1, j]+$p_{i-1}p_kp_j$
if q < m[i, j]
m[i, j] = q
s[i, j] = k
说实话,不太能看懂,等到以后看懂了再写对应的c++代码。
3、最长公共子序列
最长公共子序列 问题即是给定两个序列
X=(x1,x2,...,xm)
和
Y=(y1,y2,...,yn)
求
X
和
例如:X=BADCGE Y=BDRRCFE 则X和Y的最长公共子序列为BDCE.
1、刻画一个最优解的结构特征
设 X=(x1,x2,...,xn) 和 Y=(y1,y2,...,yn) 是两个序列,将X和Y的最长公共子序列记为LCS(X, Y).
找出LCS(X, Y)就是一个最优化问题。因为,我们需要找到X和Y中最长的那个公共子序列。而要找X和Y的LCS,首先考虑X的最后一个元素和Y的最后一个元素。
1) 如果 xn=ym ,即X的最后一个元素和Y的最后一个元素相同,这说明该元素一定位于公共子序列中,因此,现在只需要找: LCS(Xn−1,Ym−1)
2) 如果 xn!=ym ,那么它将产生两个子问题: LCS(Xn−1,Ym) 和 LCS(Xn,Ym−1)
因为序列X和序列Y的最后一个元素不相等,所以说明要么 xn ,要么 ym ,要么两个都 不是公共子序列中的一个元素。
LCS(Xn−1,Ym) 表示: (x1,x2,...,xn−1) 和 (y1,y2,...,yn) 的公共子序列。
LCS(Xn,Ym−1) 表示: (x1,x2,...,xn) 和 (y1,y2,...,yn−1) 的公共子序列。
求解上面两个子问题,得到的公共子序列谁最长,那谁就是 LCS(X,Y) 。用数学表示就是:
LCS(X,Y)=max(LCS(Xn,Ym−1),LCS(Xn,Ym−1))
所以,我们成功的将原问题转化为了三个规模更小的问题。
2、LCS C++ 实现
enum { e_left_up = 10, e_up, e_left };
void print_LCS( int (*result)[7], char *strx, int xLen, int yLen )
{
if ( xLen == 0 || yLen == 0 )
return ;
if ( result[xLen][yLen] == e_left_up )
{
print_LCS( result, strx, xLen-1, yLen-1 );
printf( "%c", *strx );
}
else if ( result[xLen][yLen] == e_up )
{
print_LCS( result, strx, xLen-1, yLen );
}
else
{
print_LCS( result, strx, xLen, yLen-1 );
}
}
int LCS( int (*result)[7], char *strx, char *stry )
{
int row = strlen( strx ) + 1;
int col = strlen( stry ) + 1;
int c[row][col];
for ( int i = 0; i < row; i++ )
{
c[i][0] = 0;
}
for ( int i = 0; i < col; i++ )
{
c[0][i] = 0;
}
for ( int i = 1; i < row; i++ )
{
for ( int j = 1; j < col; j++ )
{
if ( strx[i] == stry[j] )
{
c[i][j] = c[i-1][j-1] + 1;
result[i][j] = e_left_up;
}
else if ( c[i-1][j] >= c[i][j-1] )
{
c[i][j] = c[i-1][j];
result[i][j] = e_up;
}
else
{
c[i][j] = c[i][j-1];
result[i][j] = e_left;
}
}
}
return c[strlen(strx)][strlen(stry)];
}