【算法导论】动态规划
2017-05-22 15:40
302 查看
动态规划这个算法,我一直都搞不明白,也许因为我数学能力太差的缘故,总是不得其要领,每次学习这个算法的时候,总是不知道所谓的状态转移方程到底是怎么样推导出来的。其实就在我写这篇博客的时候,我依然不清楚。
什么问题能用动态规划来解决呢?动态规划问题的特征就是 最优子结构,一个递归结构:
该问题需要求一个最优解
该问题重复包含着子问题
比如说经典的动态规划问题,最长公共子序列(如下所述),我们要求一个最长的子序列,首先这是一个最优解,其次,两个序列的最长公共序列,也同时包含着子序列的最长公共序列。
动态规划通常按以下4个步骤来设计一个算法:
刻画一个最优解的结构特征
递归地定义最优解的值
计算最优解的值(通常采用自底向上的方法)
得用计算出的信息构造一个最优解
动态规划往往还结合一些备忘方法,用于记录中间解,以免重复求解子问题。下面来看几个问题:
钢条切割问题
最长公共子序列
最大连续子序列和
假设我们切割一条长度为 4 的钢条,总共有以下几种方案:
1. 不切割,价格为 9
2. 1 和 3 :价格为 1 + 8 = 9
3. 2 和 2 :价格为 5 + 5 = 10
4. 3 和 1 :价格为 8 + 1 = 9
5. 1、2、1 :价格为 1 + 5 + 1 = 7
6. 1、1、2 :价格为 1 + 1 + 5 = 7
7. 2、1、1 :价格为 2 + 1 + 1 = 7
可知道分割一条长度为4的钢条,最优的切割方案为「方案2」。更通俗一点,我们可以令Rn为切割长度为n的钢条,可以很轻松的得到以下公式:
这样,代码就容易写了,如下所示:
输入价格表,然后输入切割的长度,输出最优方案,结果如下所示:
以上提供了两种方案,一种是自顶向下的递归,一种是自底向上的递归设计,从代码中我们可以看到两者的区别,自顶向下,是先求解最后的结果,然后递归求解至第一个结果,而自底向上则是先求解第一个结果,直至最后一个结果。
S:ABCBDAB
T:BDCABA
其中的最长公共子序列是 BDAB、BCBA、BCAB等。
令c[i, j]表示从i到j的最长公共子序列,我们可以推导出以下状态转移式:
如果i到达字符串S的长度或j到达字符串T的长度,则最长
e185
公共子序列的长度为0
如果S[i] == T[j],那么最长公共子序列即为后续子序列的最长公共子序列+当前字符长度(1)
如果S[i] != T[j],那么最长公共子序列即为取S[i]的后续最长公共子序列与取T[i]的后续最长公共子序列的较大者(最后结果中未必有S[i]或T[j])
根据上述说明,写出的代码如下所示:
输入字符串S和T,ss表示S串的起点,ts表示T串的起点,U记录当前位置的最长公共子序列长度。输出的结果如下所示:
从以上图表可以看到,我们要拿到最长的公共子序列,则需要从【0, 0】点开始遍历表,我们只能往右或往下遍历,如果【i, j】点的两个字符S[i]和T[j]相等,那么我们应该走对角线,如果【i, j】== 【i, j + 1】那么我们应该往右走,否则应该往下走。如以上代码所示。
-2 11 -4 13 -5 -2, 其最大连续子序列和为: 11 -4 + 13 = 20
定义sum[i]为最大子序列和,可以推导出以下状态转移方程:
其实,这个方程,我并不知道是怎么样推导出来的,前面一个好理解,不过为什么要和当前值做比较取较大者,我真的不明白。希望有人能给我指点迷津。根据以上方程,我写出的代码如下所示:
程序的输出结果,如下图所示:
还有一种更简单的写法:
这段代码是网上抄的,我一开始真没想到这么写。我的水平还有很大的提升空间。
以上代码本人亲自编写,经本人测试基本无误,也许编写方式和网上流传有所不同,如有错漏,请批评指正。
动态规划这个算法真的实用性很大,可惜我依然是半知半解,之所以厚脸皮写出这篇文章,也是希望有一日能被某个高人碰巧看到这篇拙劣的博文,指点一下我这个苦苦自学,身边没朋友咨询的小弟,在此感谢先。
什么问题能用动态规划来解决呢?动态规划问题的特征就是 最优子结构,一个递归结构:
该问题需要求一个最优解
该问题重复包含着子问题
比如说经典的动态规划问题,最长公共子序列(如下所述),我们要求一个最长的子序列,首先这是一个最优解,其次,两个序列的最长公共序列,也同时包含着子序列的最长公共序列。
动态规划通常按以下4个步骤来设计一个算法:
刻画一个最优解的结构特征
递归地定义最优解的值
计算最优解的值(通常采用自底向上的方法)
得用计算出的信息构造一个最优解
动态规划往往还结合一些备忘方法,用于记录中间解,以免重复求解子问题。下面来看几个问题:
钢条切割问题
最长公共子序列
最大连续子序列和
钢条切割问题
给定一个钢条切割的长度和价格,求出最佳的切割方案。且看下列:长度(i) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格(pi) | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
1. 不切割,价格为 9
2. 1 和 3 :价格为 1 + 8 = 9
3. 2 和 2 :价格为 5 + 5 = 10
4. 3 和 1 :价格为 8 + 1 = 9
5. 1、2、1 :价格为 1 + 5 + 1 = 7
6. 1、1、2 :价格为 1 + 1 + 5 = 7
7. 2、1、1 :价格为 2 + 1 + 1 = 7
可知道分割一条长度为4的钢条,最优的切割方案为「方案2」。更通俗一点,我们可以令Rn为切割长度为n的钢条,可以很轻松的得到以下公式:
这样,代码就容易写了,如下所示:
#include <stdio.h> #include <stdlib.h> #include <string.h> int rodCut_recursive_down_to_up(int* p, int* s, int* k, int m, int n) { if(s > - 1) return s ; int q = -1; if(n == 0) return 0; for(int i = 1; i <= n; ++i) { for(int j = 1; j <= i; ++j) { int r = *(p + j) + rodCut_recursive_down_to_up(p, s, k, m, i - j); if(r > q) { q = r; s[i] = q; k[i] = j; } } } return q; } int rodCut_recursive_up_to_down(int* p, int* s, int* k, int m, int n) { if(s > -1) return s ; int q = -1; if(n == 0) return 0; for(int i = n; i >= 1; --i) { int r = *(p + i) + rodCut_recursive_up_to_down(p, s, k, m, n - i); if(r >= q) { q = r; s = q; k = i; } } return q; } int main(void) { int m, n; scanf("%d", &m); if(m != 0) { int* p = (int*)malloc(sizeof(int) * (m + 1)); int* s = (int*)malloc(sizeof(int) * (m + 1)); int* k = (int*)malloc(sizeof(int) * (m + 1)); memset(p, 0, sizeof(int) * (m + 1)); memset(k, 0, sizeof(int) * (m + 1)); for(int i = 1; i != m + 1; ++i) scanf("%d", p + i); while(scanf("%d", &n) != EOF) { if(n <= m) { memset(s, -1, sizeof(int) * (m + 1)); printf("%d: ", rodCut_recursive_up_to_down(p, s, k, m, n)); while(n) printf("%d ", k ), n = n - k ; printf("\n"); } } free(p); free(k); free(s); } return 0; }
输入价格表,然后输入切割的长度,输出最优方案,结果如下所示:
以上提供了两种方案,一种是自顶向下的递归,一种是自底向上的递归设计,从代码中我们可以看到两者的区别,自顶向下,是先求解最后的结果,然后递归求解至第一个结果,而自底向上则是先求解第一个结果,直至最后一个结果。
最长公共子序列
最长公共子序列是这样一个问题:有两个字符串S和T,求出两者最长的公共子序列,如以下字符串:S:ABCBDAB
T:BDCABA
其中的最长公共子序列是 BDAB、BCBA、BCAB等。
令c[i, j]表示从i到j的最长公共子序列,我们可以推导出以下状态转移式:
如果i到达字符串S的长度或j到达字符串T的长度,则最长
e185
公共子序列的长度为0
如果S[i] == T[j],那么最长公共子序列即为后续子序列的最长公共子序列+当前字符长度(1)
如果S[i] != T[j],那么最长公共子序列即为取S[i]的后续最长公共子序列与取T[i]的后续最长公共子序列的较大者(最后结果中未必有S[i]或T[j])
根据上述说明,写出的代码如下所示:
#include <stdio.h> #include <stdlib.h> #include <string.h> int LongestCommonSubSequence(char* S, char* T, char** U, int ss, int ts) { if(ss == strlen(S) || ts == strlen(T)) return 0; else if(U[ss][ts] != 0) return U[ss][ts]; else if(S[ss] == T[ts]) { int r = LongestCommonSubSequence(S, T, U, ss + 1, ts + 1); U[ss][ts] = r + 1; return U[ss][ts]; } else { int l1 = LongestCommonSubSequence(S, T, U, ss, ts + 1); int l2 = LongestCommonSubSequence(S, T, U, ss + 1, ts); U[ss][ts] = (l1 >= l2 ? l1: l2); return U[ss][ts]; } return 0; } int main (void) { int m, n; while(scanf("%d %d", &m, &n) == 2) { char* S = (char*)malloc(sizeof(char) * (m + 1)); char* T = (char*)malloc(sizeof(char) * (n + 1)); char** U = (char**)malloc(sizeof(char*) * m); for(int i = 0; i != m + 1; ++i) { U[i] = (char*)malloc(sizeof(char) * (n + 1)); memset(U[i], (char)0, sizeof(char) * (n + 1)); } scanf("%s", S); scanf("%s", T); printf("%d\n\n", LongestCommonSubSequence(S, T, U, 0, 0)); for(int i = 0; i <= m; ++i) { for(int j = 0; j <= n; ++j) printf("%d ", U[i][j]); printf("\n"); } int l = 0, r = 0; while(U[l][r]) { if(S[l] == T[r]) { printf("%c", S[l]); ++l, ++r; }else if(U[l][r] == U[l][r + 1]) ++r; else ++l; } printf("\n"); free(S); free(T); for(int i = 0; i != m + 1; ++i) free(U[i]); free(U); } return 0; }
输入字符串S和T,ss表示S串的起点,ts表示T串的起点,U记录当前位置的最长公共子序列长度。输出的结果如下所示:
从以上图表可以看到,我们要拿到最长的公共子序列,则需要从【0, 0】点开始遍历表,我们只能往右或往下遍历,如果【i, j】点的两个字符S[i]和T[j]相等,那么我们应该走对角线,如果【i, j】== 【i, j + 1】那么我们应该往右走,否则应该往下走。如以上代码所示。
最大连续子序列和
给定一个整数序列,求其中的一个连续子序列,使其和最大。如给定以下序列:-2 11 -4 13 -5 -2, 其最大连续子序列和为: 11 -4 + 13 = 20
定义sum[i]为最大子序列和,可以推导出以下状态转移方程:
其实,这个方程,我并不知道是怎么样推导出来的,前面一个好理解,不过为什么要和当前值做比较取较大者,我真的不明白。希望有人能给我指点迷津。根据以上方程,我写出的代码如下所示:
#include <stdio.h> #include <string.h> #include <stdlib.h> int maxSum = 0; int maxSequenceSum(int* num, int s, int e) { if(s == e) return 0; int sum = maxSequenceSum(num, s + 1, e) + num[s]; if(sum > 0) { if(sum > maxSum) { maxSum = sum; return sum; } return sum; } return 0; } int main(void) { int m; while(scanf("%d", &m) != EOF) { int* n = (int*)malloc(sizeof(int) * m); for(int i = 0; i != m; ++i) scanf("%d", n + i); maxSum = 0; maxSequenceSum(n, 0, m); printf("%d\n", maxSum); free(n); } return 0; }
程序的输出结果,如下图所示:
还有一种更简单的写法:
int MaxSumSubSequence(const int A[], int N) { int ThisSum,MaxSum,j; ThisSum = MaxSum =0; for(j = 0;j < N;j++) { ThisSum += A[j]; if(ThisSum > MaxSum) MaxSum = ThisSum; else if(ThisSum < 0) ThisSum = 0; } return MaxSum; }
这段代码是网上抄的,我一开始真没想到这么写。我的水平还有很大的提升空间。
以上代码本人亲自编写,经本人测试基本无误,也许编写方式和网上流传有所不同,如有错漏,请批评指正。
动态规划这个算法真的实用性很大,可惜我依然是半知半解,之所以厚脸皮写出这篇文章,也是希望有一日能被某个高人碰巧看到这篇拙劣的博文,指点一下我这个苦苦自学,身边没朋友咨询的小弟,在此感谢先。
相关文章推荐
- 算法导论学习--动态规划--LCS算法
- LCS问题实现---------------(动态规划, 取自算法导论)
- ACM日记——算法导论.15章 动态规划(二)
- 【算法导论】动态规划之最长公共子序列
- 算法导论程序36--动态规划(矩阵链)
- 算法导论16.2-2--动态规划(0-1背包问题)
- 【算法导论】动态规划之“钢管切割”问题
- 算法导论学习-动态规划之记忆化搜索
- (动态规划) 算法导论_钢条切割问题.(补)
- 【算法导论】贪心算法,动态规划 源码解析
- 算法导论16.2-2--动态规划(0-1背包问题)
- 【算法导论】用动态规划解活动选择问题
- 钢条切割--动态规划--算法导论
- 算法导论16.2-2--动态规划(0-1背包问题)
- 【算法导论】动态规划--装配线调度问题
- 最大子数组问题(动态规划)--【算法导论】
- 算法导论16.2-2--动态规划(0-1背包问题)
- 算法导论 15章 动态规划 装配线调度算法
- 【算法导论学习-29】动态规划经典问题02:最长公共子序列问题(Longest common subsequence,LCS)
- 算法导论16.2-2--动态规划(0-1背包问题)