理解经典算法:动态规划
什么是动态规划?
先来看看《算法导论》里面的解释:
动态规划(dynamic programming)与分治法相似,都是通过组合子问题的解来求解原问题。动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。这种算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。
设计一个动态规划算法需要4步:
1. 描述最优解的结构。
2. 递归定义最优解的值
3. 按自底向上的方式计算最优解的值
4. 由计算出的结果构造一个最优解。
这4步中的前3步是动态规划的基础,即前3步用自底向上的方法,从最底层的已经有解的子子问题开始,计算出所有子问题的最优解,并将其存入表格,用以求得问题的最优解。
从求解斐波那契数理解动态规划
学习递归的时候我们都会遇到求解斐波那契数的问题,这个问题最简单的解法就是使用递归。
1 int Fibonacci(int n) 2 { 3 if (n == 1 || n == 0) 4 return 1; 5 return Fibonacci(n - 1) + Fibonacci(n - 2); 6 }
这个方法简单是简单,但却是用程序的运行时间为代价换来的。我们从n=5时,查看程序的运行过程:
F(5) = F(4) + F(3)
F(5) = (F(3) + F(2)) + (F(2) +F(1))
F(5) = ((F(2) + F(1)) + (F(1) + F(0))) +((F(1) + F(0)) + F(1))
F(5) = (((F(1) + F(0)) + F(1)) + (F(1) + F(0))) + ((F(1) + F(0)) + F(1))
这个问题中,重复的子问题很多,但却得一个个重复的求解,重复求解这些子问题的所花费的时间可想而知。
那我们试着用动态规划的想法来求解这个问题。首先得找到这个问题的最优解的描述,即一个数的斐波那契数为其前两个数的斐波那契数之和,前提条件为F(0) = 1,F(1) = 1。然后用递归表达式表示,则为:F(n) = F(n - 1) + F(n - 2)。再然后,我们从2开始,计算出2~n-1的斐波纳契数,保存在数组a[0...n]中。最后,我们便可轻易地算出F(n) = a[n - 1] + a[n - 2]。
1 int Fibonacci(int n) 2 { 3 int *a = (int *)calloc(sizeof(int), n + 1); 4 a[1] = a[0] = 1; 5 for (int i = 2; i <= n; i++) 6 { 7 a[i] = a[i - 1] + a[i - 2]; 8 } 9 return a[n]; 10 }
这个问题使用动态规划求解也同样简单,问题本身构造最优解的复杂程度决定了其使用动态规划时算法的复杂程度。
求解最长公共子序列
动态规划的难点在于问题最优解的构造。前面的斐波纳契数问题因为其最优解不难构造,所以使用动态规划求解相当容易。而使用动态规划求解最长公共子序列,其最优解的构造则复杂一些。我们使用上面的步骤来求解这个问题:
1. 最优解的描述
最长公共子序列是求解两个字符串中,共有的最长的子序列。这个问题不要求子序列在两个字符串中连续存在,也就是不考虑子序列字符间在原字符串中有无字符存在。我们可以将问题分为两种情况:
a. 两字符串最后一个字符相等。
b. 两字符串最后一个字符不相等。
对于第一种情况,我们只需将两字符串前面字符的公共子序列求出来,然后加上最后一个字符,长度加1即可。而对于第二种情况,我们则要考虑字符串1的前缀与字符2的最长公共子序列和字符串1与字符串2的前缀的最长公共子序列的长度,两者之间取最长的作为最优解。
2. 构造递归式
我们令两字符串分别表示为xm和yn,其子问题的解保存在数组c[m, n]中,则上述描述可表示为:

3. 计算子问题的最优解
通过上面的公式,代码实现为:
1 void LCS(int **a, char *str1, int len1, char *str2, int len2) 2 { 3 int i, j; 4 //定义字符数组b并初始化 5 int **b = (int **)malloc(sizeof(int) * (len1 + 1)); 6 for (i = 0; i <= len1; i++) 7 { 8 b[i] = (int *)calloc(sizeof(int), (len2 + 1)); 9 } 10 //计算最长子序列 11 for (i = 1; i <= len1; i++) 12 { 13 for (j = 1; j <= len2; j++) 14 { 15 if (str1[i - 1] == str2[j - 1]) 16 { 17 a[i][j] = a[i - 1][j - 1] + 1; 18 b[i][j] = DIAGONAL; 19 } 20 else 21 { 22 if (a[i - 1][j] > a[i][j]) 23 { 24 b[i][j] = TOP; 25 a[i][j] = a[i - 1][j]; 26 } 27 if (a[i][j - 1] > a[i][j]) 28 { 29 b[i][j] = LEFT; 30 a[i][j] = a[i][j - 1]; 31 } 32 } 33 } 34 } 35 }
代码中的DIAGONAL、TOP、LEFT可看成↖、↑、←,则通过上面代码求解字符串X={A,B,C,B,D,A,B}和Y={B,D,C,A,B,A},可获得下表:

由上表,我们知道这两个字符串的最长公共子序列的长度为4。但是问题还没完,我们需要求解的是最长公共子序列,而不是它的长度。
4. 求解公共子序列
通过上面保存的子问题的最优解,我们就可以很容易地获得原问题的解:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 #define NON 0 6 #define TOP 1 7 #define LEFT 2 8 #define DIAGONAL 3 9 10 void LCSPrint(int **b, char *str1, int i, int j, char *str) 11 { 12 if (i == 0 || j == 0) 13 { 14 return; 15 } 16 if (b[i][j] == DIAGONAL) 17 { 18 char c[2]; 19 c[0] = str1[i - 1]; c[1] = '\0'; 20 LCSPrint(b, str1, i - 1, j - 1, str); 21 strcat(str, c); 22 } 23 else if (b[i][j] == TOP) 24 { 25 LCSPrint(b, str1, i - 1, j, str); 26 } 27 else 28 { 29 LCSPrint(b, str1, i, j - 1, str); 30 } 31 } 32 33 void LCS(int **a, char *str1, int len1, char *str2, int len2, char *str) 34 { 35 int i, j; 36 //定义字符数组b并初始化 37 int **b = (int **)malloc(sizeof(int) * (len1 + 1)); 38 for (i = 0; i <= len1; i++) 39 { 40 b[i] = (int *)calloc(sizeof(int), (len2 + 1)); 41 } 42 //计算最长子序列 43 for (i = 1; i <= len1; i++) 44 { 45 for (j = 1; j <= len2; j++) 46 { 47 if (str1[i - 1] == str2[j - 1]) 48 { 49 a[i][j] = a[i - 1][j - 1] + 1; 50 b[i][j] = DIAGONAL; 51 } 52 else 53 { 54 if (a[i - 1][j] > a[i][j]) 55 { 56 b[i][j] = TOP; 57 a[i][j] = a[i - 1][j]; 58 } 59 if (a[i][j - 1] > a[i][j]) 60 { 61 b[i][j] = LEFT; 62 a[i][j] = a[i][j - 1]; 63 } 64 } 65 } 66 } 67 LCSPrint(b, str1, len1, len2, str); 68 69 free(b); 70 } 71 72 int main(int argc, char *argv[]) 73 { 74 int **a, len1, len2, i; 75 char *str = (char *)malloc(sizeof(char)); 76 char *str1 = (char *)malloc(sizeof(char)); 77 char *str2 = (char *)malloc(sizeof(char)); 78 scanf("%s", str1); 79 scanf("%s", str2); 80 len1 = strlen(str1); 81 len2 = strlen(str2); 82 a = (int **)malloc(sizeof(int *) * (len1 + 1)); 83 for (i = 0; i <= len1; i++) 84 { 85 a[i] = (int *)calloc(sizeof(int), len2 + 1); 86 } 87 LCS(a, str1, len1, str2, len2, str); 88 printf("len: %d, string: %s", a[len1][len2], str); 89 90 free(str); 91 free(str1); 92 free(str2); 93 free(a); 94 95 return 0; 96 }
同最长公共子序列相关的问题还有:最小编辑距离、0-1背包问题等,这些问题都能展现动态规划的强大之处。

浙公网安备 33010602011771号