动态规划
Posted 松子茶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划相关的知识,希望对你有一定的参考价值。
动态规划的设计思想
动态规划(DP)[1]通过分解成子问题解决了给定复杂的问题,并存储子问题的结果,以避免再次计算相同的结果。我们通过下面这个问题来说明这两个重要属性:重叠子问题和最优子结构。
重叠子问题
像分而治之,动态规划也把问题分解为子问题。动态规划主要用于:当相同的子问题的解决方案被重复利用。在动态规划中,子问题解决方案被存储在一个表中,以便这些不必重新计算。因此,如果这个问题是没有共同的(重叠)子问题, 动态规划是没有用的。例如,二分查找不具有共同的子问题。下面是一个斐波那契函数的递归函数,有些子问题被调用了很多次。
/* simple recursive program for Fibonacci numbers */
int fib(int n)
if ( n <= 1 )
return n;
return fib(n-1) + fib(n-2);
执行 fib(5) 的递归树
fib(5)
/ \\
fib(4) fib(3)
/ \\ / \\
fib(3) fib(2) fib(2) fib(1)
/ \\ / \\ / \\
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \\
fib(1) fib(0)
我们可以看到,函数f(3)被称执行2次。如果我们将存储f(3)的值,然后避免再次计算的话,我们会重新使用旧的存储值。有以下两种不同的方式来存储这些值,以便这些值可以被重复使用。
- 记忆化(自上而下)
- 打表(自下而上)
记忆化(自上而下):
记忆化存储其实是对递归程序小的修改,作为真正的DP程序的过渡。我们初始化一个数组中查找所有初始值为零。每当我们需要解决一个子问题,我们先来看看这个数组(查找表)是否有答案。如果预先计算的值是有那么我们就返回该值,否则,我们计算该值并把结果在数组(查找表),以便它可以在以后重复使用。
下面是记忆化存储程序:
/* Memoized version for nth Fibonacci number */
#include<stdio.h>
#define NIL -1
#define MAX 100
int lookup[MAX];
/* Function to initialize NIL values in lookup table */
void _initialize()
int i;
for (i = 0; i < MAX; i++)
lookup[i] = NIL;
/* function for nth Fibonacci number */
int fib(int n)
if(lookup[n] == NIL)
if ( n <= 1 )
lookup[n] = n;
else
lookup[n] = fib(n-1) + fib(n-2);
return lookup[n];
int main ()
int n = 40;
_initialize();
printf("Fibonacci number is %d ", fib(n));
getchar();
return 0;
打表(自下而上)
下面我们给出自下而上的打表方式,并返回表中的最后一项。
/* tabulated version */
#include<stdio.h>
int fib(int n)
int f[n+1];
int i;
f[0] = 0; f[1] = 1;
for (i = 2; i <= n; i++)
f[i] = f[i-1] + f[i-2];
return f[n];
int main ()
int n = 9;
printf("Fibonacci number is %d ", fib(n));
getchar();
return 0;
这两种方法都能存储子问题解决方案。在第一个版本中,记忆化存储只在查找表存储需要的答案。而第二个版本,所有子问题都会被存储到查找表中,不管是否是必须的。比如LCS问题的记忆化存储版本,并不会存储不必要的子问题答案。
最优子结构[2]
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
例如,最短路径问题有以下最优子结构性质:如果一个节点x是到源节点ü的最短路径,同时又是到目的节点V的最短路径,则最短路径从u到v是结合最短路径:u到x和x到v。解决任意两点间的最短路径的算法的Floyd-Warshall算法[3]和贝尔曼-福特[4]是动态规划的典型例子。
另一方面最长路径问题不具有最优子结构性质。这里的最长路径是指两个节点之间最长简单路径(路径不循环)。
考虑下算法导论上面的例子:
有两条最长的路径与Q到T:Q – > R – > T和Q – > S-> T。不像最短路径,这些路径最长不具有最优子属性。例如,最长路径q-> r-> t不是由q->r 和 r->t的组合 ,因为最长的路径从q至r为q-> s-> t->r
动态规划算法的设计要素
这里用一个矩阵链乘问题为例说明动态规划算法的设计要素。
例如:给定n个矩阵 A1,A2,…,An ,其中 Ai 与 Ai+1 是可乘的, i=1,2,…,n−1 。考察这n个矩阵的连乘积 A1A2…An 。由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定,则可以依此次序反复调用2个矩阵相乘的标准算法(有改进的方法,这里不考虑)计算出矩阵连乘积。若A是一个p×q矩阵,B是一个q×r矩阵,则计算其乘积C=AB的标准算法中,需要进行pqr次数乘。
例如,如果我们有四个矩阵A,B,C和D,我们将有:
不同组合得到的运算次数是不同的,例如A为 10 × 30 , B为 30 × 5 , C 为 5 × 60 ,那么,如果采用第一种次序,执行的基本运算次数是:
而采用第二种次序,执行的基本运算次数是:
很明显第一种运算更为高效。
问题:给定一个数组P[]表示矩阵的链,使得第i个矩阵Ai 的维数为 p[i-1] x p[i].。我们需要写一个函数MatrixChainOrder()返回这个矩阵连相乘最小的运算次数。
示例:
输入:P [] = 40,20,30,10,30
输出:26000
有4个矩阵维数为 40X20,20X30,30×10和10X30。
运算次数最少的计算方式为:
(A(BC))D - > 20 * 30 * 10 +40 * 20 * 10 +40 * 10 * 30
输入:P[] = 10,20,30,40,30
输出:30000
有4个矩阵维数为 10×20,20X30,30X40和40X30。
运算次数最少的计算方式为:
((AB)C)D - > 10 * 20 * 30 +10 * 30 * 40 +10 * 40 * 30
最优子结构:
一个简单的解决办法是把括号放在所有可能的地方,计算每个位置的成本,并返回最小值。对于一个长度为n的链,我们有n-1种方法放置第一组括号。
例如,如果给定的链是4个矩阵。让矩阵连为ABCD,则有3种方式放第一组括号:A(BCD),(AB)CD和(ABC)D。
所以,当我们把一组括号,我们把问题分解成更小的尺寸的子问题。因此,这个问题具有最优子结构性质,可以使用递归容易解决。
2)重叠子问题
以下是递归的实现,只需用到上面的最优子结构性质。
//直接的递归解决
#include<stdio.h>
#include<limits.h>
//矩阵 Ai 的维数为 p[i-1] x p[i] ( i = 1..n )
int MatrixChainOrder(int p[], int i, int j)
if(i == j)
return 0;
int k;
int min = INT_MAX;
int count;
// 在第一个和最后一个矩阵直接放置括号
//递归计算每个括号,并返回最小的值
for (k = i; k <j; k++)
count = MatrixChainOrder(p, i, k) +
MatrixChainOrder(p, k+1, j) +
p[i-1]*p[k]*p[j];
if (count < min)
min = count;
return min;
// 测试
int main()
int arr[] = 1, 2, 3, 4, 3;
int n = sizeof(arr)/sizeof(arr[0]);
printf("Minimum number of multiplications is %d ",
MatrixChainOrder(arr, 1, n-1));
getchar();
return 0;
上面直接的递归方法的复杂性是指数级。当然可以用记忆化存储优化。应当指出的是,上述函数反复计算相同的子问题。请参阅下面的递归树的大小4的矩阵链。函数MatrixChainOrder(3,4)被调用两次。我们可以看到,有许多子问题被多次调用。
动态规划解决方案
以下是C / C + +实现,使用动态规划矩阵链乘法问题。
#include<stdio.h>
#include<limits.h>
int MatrixChainOrder(int p[], int n)
/* 第0行第0列其实没用到 */
int m[n][n];
int i, j, k, L, q;
//单个矩阵相乘,所需数乘次数为0
for (i = 1; i < n; i++)
m[i][i] = 0;
//以下两个循环是关键之一,以6个矩阵为例(为描述方便,m[i][j]用ij代替)
//需按照如下次序计算
//01 12 23 34 45
//02 13 24 35
//03 14 25
//04 15
//05
//下面行的计算结果将会直接用到上面的结果。例如要计算14,就会用到12,24;或者13,34等等
for (L=2; L<n; L++)
for (i=1; i<=n-L+1; i++)
j = i+L-1;
m[i][j] = INT_MAX;
for (k=i; k<=j-1; k++)
q = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
if (q < m[i][j])
m[i][j] = q;
return m[1][n-1];
int main()
int arr[] = 1, 2, 3, 4;
int size = sizeof(arr)/sizeof(arr[0]);
printf("Minimum number of multiplications is %d ",
MatrixChainOrder(arr, size));
getchar();
return 0;
时间复杂度:
O(n3)
空间复杂度:
O(n2)
动态规划算法典型的应用
最长递增子序列LIS
最长递增子序列LIS[5]的问题可以使用动态规划要解决的问题,例如,最长递增子序列(LIS)的问题是要找到一个给定序列的最长子序列的长度,使得子序列中的所有元素被排序的顺序增加。例如,10,22,9,33,21,50,41,60,80 LIS的长度是6和 LIS为10,22,33,50,60,80。
1) 最优子结构
对于长度为N的数组 A[N]=a0,a1,a2,…,an−1 ,假设假设我们想求以aj结尾的最大递增子序列长度,设为L[j],那么L[j] = max(L[i]) + 1, where i < j && a[i] < a[j], 也就是i的范围是0到j – 1。这样,想求aj结尾的最大递增子序列的长度,我们就需要遍历j之前的所有位置i(0到j-1),找出a[i] < a[j],计算这些i中,能产生最大L[i]的i,之后就可以求出L[j]。之后我对每一个A[N]中的元素都计算以他们各自结尾的最大递增子序列的长度,这些长度的最大值,就是我们要求的问题——数组A的最大递增子序列。
2) 重叠子问题
以下是简单的递归实现LIS问题(先不说性能和好坏,后面讨论)。这个实现我们遵循上面提到的递归结构。使用 max_ending_here 返回 每一个LIS结尾的元素,结果LIS是使用指针变量返回。
/* LIS 简单的递归实现 */
#include<stdio.h>
#include<stdlib.h>
/* 要利用递归调用,此函数必须返回两件事情:
1) Length of LIS ending with element arr[n-1]. We use max_ending_here for this purpose
2) Overall maximum as the LIS may end with an element before arr[n-1] max_ref is used this purpose.
The value of LIS of full array of size n is stored in *max_ref which is our final result
*/
int _lis( int arr[], int n, int *max_ref)
/* Base case */
if(n == 1)
return 1;
int res, max_ending_here = 1; // 以arr[n-1]结尾的 LIS的长度
/* Recursively get all LIS ending with arr[0], arr[1] ... ar[n-2]. If
arr[i-1] is smaller than arr[n-1], and max ending with arr[n-1] needs
to be updated, then update it */
for(int i = 1; i < n; i++)
res = _lis(arr, i, max_ref);
if (arr[i-1] < arr[n-1] && res + 1 > max_ending_here)
max_ending_here = res + 1;
// Compare max_ending_here with the overall max. And update the
// overall max if needed
if (*max_ref < max_ending_here)
*max_ref = max_ending_here;
// Return length of LIS ending with arr[n-1]
return max_ending_here;
// The wrapper function for _lis()
int lis(int arr[], int n)
// The max variable holds the result
int max = 1;
// The function _lis() stores its result in max
_lis( arr, n, &max );
// returns max
return max;
/* 测试上面的函数 */
int main()
int arr[] = 10, 22, 9, 33, 21, 50, 41, 60 ;
int n = sizeof(arr)/sizeof(arr[0]);
printf("Length of LIS is %d\\n", lis( arr, n ));
getchar();
return 0;
根据上面的实现方式,以下是递归树大小4的调用。LIS(N)为我们返回arr[]数组的LIS长度。
lis(4)
/ | \\
lis(3) lis(2) lis(1)
/ \\ /
lis(2) lis(1) lis(1)
/
lis(1)
我们可以看到,有些重复的子问题被多次计算。所以我们可以使用memoization (记忆化存储)的或打表 来避免同一子问题的重新计算。以下是打表方式实现的LIS。
/* LIS 的动态规划方式实现*/
#include<stdio.h>
#include<stdlib.h>
/* lis() returns the length of the longest increasing subsequence in
arr[] of size n */
int lis( int arr[], int n )
int *lis, i, j, max = 0;
lis = (int*) malloc ( sizeof( int ) * n );
/* Initialize LIS values for all indexes */
for ( i = 0; i < n; i++ )
lis[i] = 1;
/* Compute optimized LIS values in bottom up manner */
for ( i = 1; i < n; i++ )
for ( j = 0; j < i; j++ )
if ( arr[i] > arr[j] && lis[i] < lis[j] + 1)
lis[i] = lis[j] + 1;
/* Pick maximum of all LIS values */
for ( i = 0; i < n; i++ )
if ( max < lis[i] )
max = lis[i];
/* Free memory to avoid memory leak */
free( lis );
return max;
/* 测试程序 */
int main()
int arr[] = 10, 22, 9, 33, 21, 50, 41, 60 ;
int n = sizeof(arr)/sizeof(arr[0]);
printf("Length of LIS is %d\\n", lis( arr, n ) );
getchar();
return 0;
注意,上面动态的DP解决方案的时间复杂度为 O(n2) ,其实较好的解决方案是 O(nlogn)
最长公共子序列LCS
LCS问题[6] [7]描述:给定两个序列,找出在两个序列中同时出现的最长子序列的长度。一个子序列是出现在相对顺序的序列,但不一定是连续的。例如,“ABC”,“ABG”,“BDF”,“AEG”,“acefg“,..等都是”ABCDEFG“ 序列。因此,长度为n的字符串有 2n 个不同的可能的序列。
注意:
最长公共子串(Longest CommonSubstring)和最长公共子序列(LongestCommon Subsequence, LCS)的区别:子串(Substring)是串的一个连续的部分,子序列(Subsequence)则是从不改变序列的顺序,而从序列中去掉任意的元素而获得的新序列;更简略地说,前者(子串)的字符的位置必须连续,后者(子序列LCS)则不必。比如字符串acdfg同akdfc的最长公共子串为df,而他们的最长公共子序列是adf。LCS可以使用动态规划法解决。
这是一个典型的计算机科学问题,基础差异(即输出两个文件之间的差异文件比较程序),并在生物信息学有较多应用。例如:输入序列“ABCDGH”和“AEDFHR” 的LCS是“ADH”长度为3;输入序列“AGGTAB”和“GXTXAYB”的LCS是“GTAB”长度为4。
这个问题的直观的解决方案是同时生成给定序列的所有子序列,找到最长匹配的子序列。此解决方案的复杂性是指数的。让我们来看看如何这个问题 (拥有动态规划(DP)问题的两个重要特性):
1)最优子结构:
设输入序列是
X[0..m−1]
和
Y[0..n−1]
,长度分别为
m
和 以上是关于动态规划的主要内容,如果未能解决你的问题,请参考以下文章