首页 > 代码库 > [MOOC笔记]第一章XA 动态规划(数据结构)

[MOOC笔记]第一章XA 动态规划(数据结构)

Fibonacci数列和动态规划


什么是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 =1014flo =105 sec = 1 day

Φ92 =1019flo = 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[n] < 0)
                   memoir[n]= (2 > n) ? n : Fib(n-1) + Fib(n-2);
         returnmemoir[n];
}


改进方法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[n] = B[m] = ‘X‘,则取LCS(A[0, n), B[0, m))(减而治之

若A[n] ≠ 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[n] == 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[n]≠ 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[n][m];
}


总结:递归虽然可以很好地找到一个可行的解,但是有时候并不是一个高效的方法,所以需要使用动态规划使之成为一个实用的算法。

 

:本笔记来自清华大学邓俊辉老师的MOOC课程《数据结构》,感兴趣的朋友点击这里选课学习。

 


[MOOC笔记]第一章XA 动态规划(数据结构)