一、动态规划概述
动态规划(Dynamic Programming),这是一种解决棘手问题的方法,它将问题分成小问题,并先着手解决这些小问题。然后存储它们的解决方案以避免重复计算。这种简单的优化将时间复杂度从指数降低到多项式。例如,如果我们为Fibonacci Numbers编写简单的递归解,我们会得到指数时间复杂度,如果我们通过存储子问题的解来优化它,时间复杂度会降低到线性。
“1+1+1+1+1+1+1+1 = ?”。
“八!”
在左边写下另一个“1+”。
“那个怎么样?”
“九!” “你怎么知道这么快就九点了?”
“因为又加了一个!”
“所以不需要重新计算,因为记得有八个!
二、动态规划的不同方法
有两种不同的方法来存储值,以便可以重用子问题的值。在这里,将讨论解决动态规划 (DP) 问题的两种模式:
记忆:自上而下
制表:自下而上
1、自上而下(记忆法)
自上而下的方法使用记忆技术。它由递归和缓存组成。“递归”表示重复调用函数的计算过程,而“缓存”表示存储中间结果。
斐波那契数列示例(普通递归)
public int CalculateFibonacci(int n) {
if(n < 2)
return n;
return CalculateFibonacci(n-1) + CalculateFibonacci(n-2);
}
斐波那契数列示例(递归记忆)
public int CalculateFibonacci(int n) {
int memoize[] = new int[n+1];
return CalculateFibonacciRecursive(memoize, n);
}
public int CalculateFibonacciRecursive(int[] memoize, int n) {
if(n < 2)
return n;
// if we have already solved this subproblem, simply return the result from the cache
if(memoize[n] != 0)
return memoize[n];
memoize[n] = CalculateFibonacciRecursive(memoize, n-1) + CalculateFibonacciRecursive(memoize, n-2);
return memoize[n];
}
2、自下而上(制表法)
这种方法使用制表技术来实现动态规划解决方案。它解决了与以前相同的问题,但没有递归。在这种方法中,递归被迭代替换。制表法是通过填写一个 n 维表来完成的。根据表中的结果,然后计算顶部/原始问题的解决方案。
斐波那契数列示例(制表法)
static int fib(int n)
{
/* Declare an array to store Fibonacci numbers. */
int f[] = new int[n+2]; // 1 extra to handle case, n = 0
int i;
/* 0th and 1st number of the series are 0 and 1*/
f[0] = 0;
f[1] = 1;
for (i = 2; i <= n; i++)
{
/* Add the previous 2 numbers in the series
and store it */
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
public static void main (String args[])
{
int n = 9;
System.out.println(fib(n));
}
斐波那契数列示例(制表法的优化方法)
我们可以通过存储前两个数字来优化上一个方法中使用的空间,因为这就是我们获得下一个斐波那契数列所需的全部内容。
static int fib(int n)
{
int a = 0, b = 1, c;
if (n == 0)
return a;
for (int i = 2; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return b;
}
public static void main (String args[])
{
int n = 9;
System.out.println(fib(n));
}
三、更多示例
1、最长公共子序列
最长公共子序列(Longest Common Subsequence),简称LCS 问题,是说给定两个序列,找出它们中存在的最长子序列的长度。子序列是以相同的相对顺序出现的序列,但不一定是连续的。例如,“abc”、“abg”、“bdf”、“aeg”、“acefg”、..等是“abcdefg”的子序列。
输入序列“ABCDGH”和“AEDFHR”的 LCS 为长度为 3 的“ADH”。
输入序列“AGGTAB”和“GXTXAYB”的 LCS 为长度为 4 的“GTAB”。
它是一个经典的计算机科学问题,是比较两个文件之间差异的程序的基础,并在生物信息学中有应用。
(1)简单递归实现
朴素递归方法的时间复杂度在最坏情况下为 O(2^n),最坏情况发生在 X 和 Y 的所有字符不匹配时,即 LCS 的长度为 0。
public class LongestCommonSubsequence
{
/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char[] X, char[] Y, int m, int n )
{
if (m == 0 || n == 0)
return 0;
if (X[m-1] == Y[n-1])
return 1 + lcs(X, Y, m-1, n-1);
else
return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n));
}
/* Utility function to get max of 2 integers */
int max(int a, int b)
{
return (a > b)? a : b;
}
public static void main(String[] args)
{
LongestCommonSubsequence lcs = new LongestCommonSubsequence();
String s1 = "AGGTAB";
String s2 = "GXTXAYB";
char[] X=s1.toCharArray();
char[] Y=s2.toCharArray();
int m = X.length;
int n = Y.length;
System.out.println("Length of LCS is" + " " +
lcs.lcs( X, Y, m, n ) );
}
}
(2)递归记忆实现(自上而下)
时间复杂度:O(mn) 忽略递归堆栈空间
/* A Top-Down DP implementation of LCS problem */
#include <bits/stdc++.h>
using namespace std;
/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs(char* X, char* Y, int m, int n,
vector<vector<int> >& dp)
{
if (m == 0 || n == 0)
return 0;
if (X[m - 1] == Y[n - 1])
return dp[m][n] = 1 + lcs(X, Y, m - 1, n - 1, dp);
if (dp[m][n] != -1) {
return dp[m][n];
}
return dp[m][n] = max(lcs(X, Y, m, n - 1, dp),
lcs(X, Y, m - 1, n, dp));
}
/* Driver code */
int main()
{
char X[] = "AGGTAB";
char Y[] = "GXTXAYB";
int m = strlen(X);
int n = strlen(Y);
vector<vector<int> > dp(m + 1, vector<int>(n + 1, -1));
cout << "Length of LCS is " << lcs(X, Y, m, n, dp);
return 0;
}
(3)制表法实现(自下而上)
上述实现的时间复杂度为 O(mn),比朴素递归实现的最坏情况时间复杂度要好得多。
/* Dynamic Programming Java implementation of LCS problem */
public class LongestCommonSubsequence
{
/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char[] X, char[] Y, int m, int n )
{
int L[][] = new int[m+1][n+1];
/* Following steps build L[m+1][n+1] in bottom up fashion. Note
that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] */
for (int i=0; i<=m; i++)
{
for (int j=0; j<=n; j++)
{
if (i == 0 || j == 0)
L[i][j] = 0;
else if (X[i-1] == Y[j-1])
L[i][j] = L[i-1][j-1] + 1;
else
L[i][j] = max(L[i-1][j], L[i][j-1]);
}
}
return L[m][n];
}
/* Utility function to get max of 2 integers */
int max(int a, int b)
{
return (a > b)? a : b;
}
public static void main(String[] args)
{
LongestCommonSubsequence lcs = new LongestCommonSubsequence();
String s1 = "AGGTAB";
String s2 = "GXTXAYB";
char[] X=s1.toCharArray();
char[] Y=s2.toCharArray();
int m = X.length;
int n = Y.length;
System.out.println("Length of LCS is" + " " +
lcs.lcs( X, Y, m, n ) );
}
}