刚刚被老妈拉出去健身,所以更的晚了一些。。。
今天这部分是只有视频里面有书上没有的
首先回忆一下上个话题,我们为什么要讲递归式,原因就是分治法中要用到递归式来获得其复杂度。从而评判算法的好坏。
对于一个递归式: T(n) = aT(n/b)+Theta(1)
a表示子问题的个数,n/b表示子问题的大小,Theta(1)表示合并子问题的时候的开销
这里需要注意一点,子问题不能是有重复的情况,比如说算斐波那契数列,用分治法是指数级别的开销(Theta(phi^n),其中phi为黄金分割数),用自底向上的递推则只有Theta(n)的开销。说到斐波那契数列,其实有小于O(n)的算法,一会儿总结里会提到。
接下来写个总结吧:
merge sort
basic idea是先把一个数组分成两半,分别进行merge sort(复杂度是2*O(n/2)),然后把已经merge好的两个数组merge了(复杂度是Theta(n)).
写出递归式是T(n) = 2T(n/2)+Theta(n)
显然,这里我们的两个T(n/2)虽然看上去一样,但并非重复计算,而是分别对两半进行merge,故而不存在冗余计算的情况.
这个大家应该非常熟悉,master method 告诉我们复杂度是nlogn
binary search
这个大家肯定都写过,也能脱口而出它的复杂度是logn
但是既然我们是钻研算法的人,肯定不能满足于此吧。
我先给出它的递归式:T(n) = T(n/2)+Theta(1)
T(n)是指对n个数进行二分查找的复杂度, T(n/2)是指对n/2个数进行二分查找的复杂度,Theta(1)是比较要查找的数和数组中的第n/2个数的运算复杂度
同理,master method 告诉我们上面的递归式的复杂度是logn。
powering a number
这也是很经典的问题,简单的说就是假设计算机不会算乘方,你写一个函数计算x^n。
一个最简单明了的想法就是这样写
int f(float x, int n){
if (n != 1) return x*f(x,n-1);
return x;
}
但是稍微想想就会发现我们这里有很多重复计算的地方。比如说,你算x^4,其实只需要x^2再算平方就可以了,并不需要x^3。
这个算法的复杂度是多少呢?O(n),虽然不算大,但我们有更好的。
正如我刚刚所说,算x^n只需要算x^(n/2)或者算x^(n-1)/2就可以了,这取决于n的奇偶性。
我给出它的计算式:
x^n = x^(n/2)*x^(n/2) if n is even
x^n = (x^(n-1)/2)*(x^(n-1)/2)*x if n is odd
再给出它的递归式(这里我们并不在意它的取整情况):
T(n) = T(n/2)+Theta(1)
它的复杂度是logn
给出代码
int f(float x, int n){
if(n == 1) return x;
if(n%2 == 0) {
int temp = f(x,n/2);
return temp*temp;
}
else {
int temp = f(x,(n-1)/2);
return temp*temp*x;
}
}
斐波那契数列
类似于上面的问题,简单的做法是递归的计算
f(n) = f(n-1) + f(n-2)
如果你愿意画一下递归树会发现里面重复计算的子树太多了,简直一直都在重复计算。
事实上这个复杂度是指数级别的,高达O(phi^n),其中phi = (sqrt(5)-1) / 2
但如果我们顺序递推f(n),每次要计算f(n)时f(n-1)和f(n-2)都已经计算好了。这个计算复杂度是O(n)。
事实上有更牛X的方法,但是一般人估计想不到了。斐波那契数其实有个性质,就是round(phi^n/sqrt(5))就是f(n)。这样可以用我们刚才说的计算数的power的方法计算f(n),复杂度是logn。是不是很厉害呢?
不幸的是由于种种原因,这种算法并不能真正在计算机上实现,因为现在的计算机都是浮点型的计算机,根号5和phi其实都是被一些固定位数的浮点数来表示,在计算过程中会失去一些位,结果就是取整的时候得不到正确的答案。
最后教授介绍了一种复杂度为log(n)且可行的方法。((F(n+1), F(n)) (F(n), F(n-1))) = ((1,1) (1,0))^n,这是一个用矩阵来计算的方法,我给出自己写的实例代码。
可以用一维矩阵替代二维
#include <stdio.h>
int a[2][2] = {{1,1},{1,0}};
int* matrix_multi(int *A, int *B){
int i, j, k;
int *temp = new int[4];
for(i = 0; i < 2; i ++){
for (j = 0; j < 2; j++){
*(temp+2*i+j) = 0;
for (k = 0; k < 2; k ++){
*(temp+2*i+j) = *(temp+2*i+j) + (*(A+2*i+k)) * (*(B+2*k+j));
}
}
}
return temp;
}
int* f(int n){
if(n < 1) return NULL;
if(n == 1) return *a;
if(n%2 == 0){
return matrix_multi(f(n/2), f(n/2));
}
else {
return matrix_multi(matrix_multi(f((n-1)/2), f((n-1)/2)), *a);
}
}
int main(){
printf("%d", *(f(10)+1));
return 0;
}
也可直接用二维矩阵
#include <stdio.h>
int ** matrix_multi(int** A, int** B){
int ** p = new int *[2];
for (int i =0; i<2; i++){
p[i] = new int[2];
}
for (int i =0; i<2; i++){
for(int j = 0; j < 2; j++){
for (int k = 0; k < 2; k++){
p[i][j] = p[i][j] + A[i][k]*B[k][j];
}
}
}
return p;
}
int** f(int** a, int n){
if(n < 1) return NULL;
if(n == 1) return a;
if(n%2 == 0){
return matrix_multi(f(a, n/2), f(a, n/2));
}
else {
return matrix_multi(matrix_multi(f(a, (n-1)/2), f(a, (n-1)/2)), a);
}
}
int main(){
int ** a = new int *[2];
for (int i =0; i<2; i++){
a[i] = new int[2];
}
a[0][0] = 1;
a[0][1] = 1;
a[1][0] = 1;
a[1][1] = 0;
printf("%d", *(*f(a, 10)+1));
printf("\n");
return 0;
}
上面计算出来的结果都是55,即fibo(10)的结果
计算矩阵的乘法
一般而言矩阵的乘法需要三层for循环,这是一个n^3的算法。strassen提出了一个n^(log2(7))的算法,教授也讲了,这个我就不说了。
VLSI (超大规模集成电路)的布局
其实就是求完全二叉树怎么放占的矩形面积最小,要求每个树的顶点在点阵的点上。
最后面积是O(n),长宽各sqrt(n),做一个H布局。
这个我觉得纯属智力题,老师也没说怎么推导出来的。