[MOOC笔记]第一章XA 动态规划(数据结构)
2014-09-20 16:36
295 查看
Fibonacci数列和动态规划
什么是Fibonacci数列?
Fibonacci数列指的是这样一个数列
它的第0项是0,第1项是第一个1。从第二项开始,每一项都等于前两项之和。用C语言可以表示为:
这段代码可以计算出Fibonacci数列的第n项。但是随着n的增长,它所消耗的时间逐渐让人无法接受。可以推算出它的时间复杂度为:
T(0) = T(1) =1; T(n) = T(n-1) + T(n-2) + 1,n > 1;
令 S(n) = [T(n) + 1] / 2
则 S(0) = 1 = Fib(1); S(1)= 1 = Fib(2)
故 S(n) = S(n-1) + S(n-2)= Fib(n+1)
T(n) = 2 * S(n) – 1 = 2 * Fib(n+1) – 1 = O(Fib(n+1))= O(Φn) =O(2n)
这是一个指数级的复杂度。
O(Φn)具体有多大?
Φ43 =230
= 109 flo = 1 sec
Φ67 =1014
flo =105 sec = 1 day
Φ92 =1019
flo = 1010 sec =
105 day = 3 century
如果用以上算法求Fibonacci数列的第67项需要1天,求出第92项则需要整整三生三世!
为什么会这么大?
使用递归跟踪将Fib(5)的调用关系绘制如下图:
![](http://img.blog.csdn.net/20140920164121356?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMTk0MjU5Mjc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
可以发现其中许多项被重复调用多次,总共O(Φn)种递归实例去除重复后只有O(n)种。
该如何改进?
改进方法A 记忆法(memoization):将已经计算过实例的结果制表查备。代码如下:
改进方法B 动态规划:颠倒计算方向:自顶而下递归改为自底而上迭代。代码如下:
这两种改进后算法的时间复杂度均为O(n),记忆法的空间复杂度为O(n)、动态规划的空间复杂度为O(1)。
LCS和动态规划
什么是LCS?
LCS是Longest CommonSubsequence的缩写,即最长公共子序列。一个序列,如果是两个或多个已知序列的子序列,且是所有子序列中最长的,则为最长公共子序列。
![](http://img.blog.csdn.net/20140920164822978?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMTk0MjU5Mjc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
如何求出LCS?
对于序列A[0, n]和B[0, m],LCS(A[0, n], B[0, m])有三种情况
若n = -1 或 m = -1,则取空序列""
若A
= B[m] = 'X',则取LCS(A[0, n), B[0, m))(减而治之)
若A
≠ B[m],则在LCS(A[0, n], B[0, m))和LCS(A[0, n), B[0, m])取更长者(分而治之)
LCS的每一个解,对应于(0, 0)和(n, m)之间的一条单调通路,反之亦然。例如LCS("advantage", "educational")的求解过程如下图:
![](http://img.blog.csdn.net/20140920164649203?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMTk0MjU5Mjc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
LCS的单调性:无论如何,每经过一次比对,原问题的规模必可减少,作为输入的两个序列,至少其一的长度缩短一个单位。
LCS的性能如何?
根据上边提到的三条分支,可以构建出一个递归LCS算法,代码如下:
≠ B[m]时,原问题将变为2个子问题。更糟糕的是,它们在随后进一步导出的子问题很可能雷同。例如下图,许多的分支都会由它右边和下边的分支创建出两份:
![](http://img.blog.csdn.net/20140920165047371?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMTk0MjU5Mjc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
如何改善LCS?
跟Fibonacci数列类似,LCS算法也可以从递归转为迭代,由前缀向后推算。这样能把时间复杂度从O(2n)降为O(m*n)。为此只需要将所有子问题列成一张表,颠倒计算方向,从LCS(A[0], B[0])出发,依此计算出所有项,如下图所示:
![](http://img.blog.csdn.net/20140920165133033?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMTk0MjU5Mjc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
迭代LCS代码参考如下:
总结:递归虽然可以很好地找到一个可行的解,但是有时候并不是一个高效的方法,所以需要使用动态规划使之成为一个实用的算法。
注:本笔记来自清华大学邓俊辉老师的MOOC课程《数据结构》,感兴趣的朋友点击这里选课学习。
什么是Fibonacci数列?
Fibonacci数列指的是这样一个数列
{0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89,144, ...}
它的第0项是0,第1项是第一个1。从第二项开始,每一项都等于前两项之和。用C语言可以表示为:
//Fib(n) = Fib(n-1) + fib(n-2) int Fib(int n) { return(2 > n) ? n : Fib(n-1) + Fib(n-2); }
这段代码可以计算出Fibonacci数列的第n项。但是随着n的增长,它所消耗的时间逐渐让人无法接受。可以推算出它的时间复杂度为:
T(0) = T(1) =1; T(n) = T(n-1) + T(n-2) + 1,n > 1;
令 S(n) = [T(n) + 1] / 2
则 S(0) = 1 = Fib(1); S(1)= 1 = Fib(2)
故 S(n) = S(n-1) + S(n-2)= Fib(n+1)
T(n) = 2 * S(n) – 1 = 2 * Fib(n+1) – 1 = O(Fib(n+1))= O(Φn) =O(2n)
这是一个指数级的复杂度。
O(Φn)具体有多大?
Φ43 =230
= 109 flo = 1 sec
Φ67 =1014
flo =105 sec = 1 day
Φ92 =1019
flo = 1010 sec =
105 day = 3 century
如果用以上算法求Fibonacci数列的第67项需要1天,求出第92项则需要整整三生三世!
为什么会这么大?
使用递归跟踪将Fib(5)的调用关系绘制如下图:
可以发现其中许多项被重复调用多次,总共O(Φn)种递归实例去除重复后只有O(n)种。
该如何改进?
改进方法A 记忆法(memoization):将已经计算过实例的结果制表查备。代码如下:
int memoir[100]; int Fib(int n) { if(memoir < 0) memoir = (2 > n) ? n : Fib(n-1) + Fib(n-2); returnmemoir ; }
改进方法B 动态规划:颠倒计算方向:自顶而下递归改为自底而上迭代。代码如下:
int Fib(int n) { intf = 0; intg = 1; while(1 < n--) { g= g + f; f= g - f; } returng; }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
这两种改进后算法的时间复杂度均为O(n),记忆法的空间复杂度为O(n)、动态规划的空间复杂度为O(1)。
LCS和动态规划
什么是LCS?
LCS是Longest CommonSubsequence的缩写,即最长公共子序列。一个序列,如果是两个或多个已知序列的子序列,且是所有子序列中最长的,则为最长公共子序列。
如何求出LCS?
对于序列A[0, n]和B[0, m],LCS(A[0, n], B[0, m])有三种情况
若n = -1 或 m = -1,则取空序列""
若A
= B[m] = 'X',则取LCS(A[0, n), B[0, m))(减而治之)
若A
≠ B[m],则在LCS(A[0, n], B[0, m))和LCS(A[0, n), B[0, m])取更长者(分而治之)
LCS的每一个解,对应于(0, 0)和(n, m)之间的一条单调通路,反之亦然。例如LCS("advantage", "educational")的求解过程如下图:
LCS的单调性:无论如何,每经过一次比对,原问题的规模必可减少,作为输入的两个序列,至少其一的长度缩短一个单位。
LCS的性能如何?
根据上边提到的三条分支,可以构建出一个递归LCS算法,代码如下:
int LCS(const char *A, const char *B, int n, int m) { if (n < 0 || m < 0) return 0; if (A == B[m]) return LCS(A, B, n-1, m-1) + 1; int result1 = LCS(A, B, n-1, m); int result2 = LCS(A, B, n, m-1); return result1 > result2 ? result1 : result2; } int main() { const char *A = "aadvantage"; const char *B = "educational"; printf("%d", LCS(A, B, strlen(A)-1, strlen(B)-1)); return 0; }在最好情况下,这段代码只需要O(n+m)的时间。但在最坏情况下,它则需要O(2n)时间。原因在于:当A
≠ B[m]时,原问题将变为2个子问题。更糟糕的是,它们在随后进一步导出的子问题很可能雷同。例如下图,许多的分支都会由它右边和下边的分支创建出两份:
如何改善LCS?
跟Fibonacci数列类似,LCS算法也可以从递归转为迭代,由前缀向后推算。这样能把时间复杂度从O(2n)降为O(m*n)。为此只需要将所有子问题列成一张表,颠倒计算方向,从LCS(A[0], B[0])出发,依此计算出所有项,如下图所示:
迭代LCS代码参考如下:
int table[20][20]; int LCS(const char *A, const char *B, intn, int m) { inti, j, max; for(i=0; i<=m; i++) { if(A[0] == B[i]) table[0][i]= 1; else table[0][i]= 0; } for(j=0; j<=n; j++) { if(B[0] == A[j]) table[j][0]= 1; else table[j][0]= 0; } for(i=1; i<=m; i++) { for(j=1; j<=n; j++){ max= table[j-1][i] > table[j][i-1] ? table[j-1][i] : table[j][i-1]; if(B[i] == A[j]) table[j][i]= max + 1; else table[j][i]= max; } } return table [m]; }
总结:递归虽然可以很好地找到一个可行的解,但是有时候并不是一个高效的方法,所以需要使用动态规划使之成为一个实用的算法。
注:本笔记来自清华大学邓俊辉老师的MOOC课程《数据结构》,感兴趣的朋友点击这里选课学习。
相关文章推荐
- [MOOC笔记]第一章 绪论(数据结构)
- 数据结构笔记 第一章基础概念(二)
- 《大话数据结构》笔记之 第一章 数据结构绪论
- 数据结构编程笔记二:第一章 绪论 三元组的程序实现
- 学习数据结构的笔记(第一章)
- 数据结构 C语言版 第一章笔记 基本概念和术语
- 中国大学MOOC-陈越、何钦铭-数据结构-笔记
- 数据结构 第一章笔记整理
- 数据结构 第一章绪论 学习笔记
- 数据结构-笔记(第一章)
- 《数据结构》学习笔记--第一章绪论
- 《数据结构》学习笔记--第一章 绪论
- 《数据结构》 笔记 第一章 绪论
- [MOOC笔记]第三章 列表(数据结构)
- 菜鸟的ACM之路(5)北大MOOC算法基础笔记__第六、七周__动态规划(下)例题实战
- 数据结构学习笔记7-动态规划(最长公共子序列)
- 「笔记」《大话数据结构》第一章:数据结构绪论
- 数据结构【第一章 绪论】 个人笔记
- [MOOC笔记]排序专题(数据结构)
- 数据结构笔记 第一章基础概念(一)