动态规划引入
首先我们以一个最基本的例子来分析——菲波那切数列。
我们都知道,菲波那切数列的递推公式f(n) = f(n-1)+f(n-2) (这里我就说明一般情况,不列举边界条件了),很简单,如果我们用递归的方法来求解f(n),两三行代码就出来了。那么我们深入分析一下这样有什么问题?
f(2) = f(1) + f(0);
f(3) = f(2) + f(1);
f(4) = f(3) + f(2);
f(5) = f(4) + f(3);
......
计算一个f(5)我们需要计算一个f(4)和一个f(3),而一个f(4)又需要一个f(3)和一个f(2),这其中就有了一个重复的f(3),那么在继续往下推导,会发现有越来越多的重复。当我们在计算机中计算f(40)并输出时,我们会发现已经有相当长时间的延时了,为什么?因为这样的递归重复计算太多了,导致整个算法效率非常低。
由于上述过程存在着大量的重复计算,我们可以用一个数组保存所有已经计算过的项,这样便可以达到用空间换时间的目的,在这种情况下,时间复杂度为O(N),而空间复杂度也为O(N)。事实上,我们所述的这种算法就是利用了动态规划的思想。
动态规划算法的思想与分治法类似,也是通过组合子问题的解而解决整个问题。其基本思路是利用一个表来记录所有已解的子问题的答案,不管该子问题以后是否被用到,只要它被计算过,就将结果填入表中,这样就可以避免重复计算问题。
动态规划算法的设计可以分为如下几个步骤:
1)描述最优解的结构;
2)递归定义最优解的值;
3)按自底向上的方式计算最优解的值;
4)由计算出的结果构造一个最优解;
其中第1~3步构成问题的动态规划解的基础。
适合动态规划方法的最优化问题的两个要素:最优子结构,重叠子问题。
例1:求用1*2的瓷砖覆盖2*M的地板有几种方式?
分析:假设所求问题的解为F(M),有下面两种情况:
当第一块瓷砖竖着放的时候,问题转换成求用1*2的瓷砖覆盖剩下的2*(M-1)的方式,即F(M-1)。
当第一块瓷砖横着放的时候,则必有另一块瓷砖横着放在其下面,问题转换成求用1*2的瓷砖覆盖 剩下的2*(M-2)的方式,即F(M-2)。
在求F(M-1)和F(M-2)时,由于第一列地板的覆盖方式已经不同,故F(M-1)种覆盖方式和F(M-2)中覆盖方式没有重叠,故:
F(M) = F(M-1)+F(M-2)
其中,F(1) = 1,F(2) = 2。可见我们能够将问题规模缩小。
仔细看递推式,其实就和菲波那切数列是一样的,既然这样,我们就不要用上面的递归方式进行求解F(M)了,而是用动态规划的方式,建立一个表格,存储每一步骤的F(i).
//...
int a[100];
int func(int M){}
a[1] = 1;
a[2] = 2;
for(int i = 3; i <= M; ++i)
a[i] = a[i-1] + a[i-2];
return a[M];
}
//...
例2:LCS(最长公共子序列问题)
注:LCS问题不要求所求得的字符在所给的字符串中是连续的。
分析:假设X={x1,x2,...,xm}和Y={y1,y2,...,yn}的一个最长公共子序列为Z={z1,z2,...,zk},
1)若x(m) = y(n),则必然有z(k) = x(m) = y(n),且Z(k-1)是X(m-1)和Y(n-1)的最长公共子序列;
2)若x(m) != y(n)且z(k) != x(m),则Z是X(m-1)和Y的最长公共子序列;
3)若x(m) != y(n) 且z(k) != y(n),则Z是X和Y(n-1)的最长公共子序列;
也就是说,
当x(m) = y(n)时,LCS(X(m),Y(n)) = LCS(X(m-1),Y(n-1)) + 1;
当x(m) != y(n)时,LCS(X(m),Y(n)) = max{LCS(X(m-1),Y(n)), LCS(X(m),Y(n-1))};
若用一个二维表格c来存储LCS,则c[i][j]表示X和Y长度分别为i和j时的LCS,显然,当X或Y为空时,LCS为0.
下面给出动态规划算法的代码:
/* 动态规划:最长公共子序列问题LCS */
const int INF = 99999;
int c[100][100];
int LCS_Memo(string A, string B, int i, int j){
if(c[i][j] < INF)
return c[i][j];
if(i == 0 || j == 0)
c[i][j] = 0;
else if(A[i-1] == B[j-1])
c[i][j] = c[i-1][j-1] + 1;
else{
int p = LCS_Memo(A, B, i-1, j);
int q = LCS_Memo(A, B, i, j-1);
if(p >= q)
c[i][j] = p;
else
c[i][j] = q;
}
return c[i][j];
}
int LCS_Length(string A, string B){
int m = A.length();
int n = B.length();
memset(c, INF, sizeof(c));
return LCS_Memo(A, B, m, n);
}
//dp[i][j]存放的是长度分别为i、j的字符串A、B的LCS
int LCS(string A, string B, int m, int n){
int dp[300][300];
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= m; ++i){
for(int j = 1; j <= n; ++j){
if(A[i-1] == B[j-1])
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[m][n];
}
分析:这里以行n,列cap建立二维表格dp[n+1][cap+1],其中dp[i][j]表示重量不超过j时的最大价值。那么这里就有两种情况:
1)选择第i件物品,则前i-1件物品的重量不能超过j-w[i];
2)不选择第i件物品,则前i-1件物品的重量不能超过j;
即dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
int getMaxValue(vector<int> w, vector<int> v, int n, int cap){
int dp[n+1][cap+1];
for(int i = 0; i <= n; ++i)
dp[i][0] = 0;
for(int j = 0; j <= cap; ++j)
dp[0][j] = 0;
for(int i = 1; i <= n; ++i){
for(int j = 1;j <= cap; ++j){
if(j >= w[i-1])
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]]+v[i-1]);
else
dp[i][j] = dp[i-1][j];
}
}
}