算法设计与分析 课程笔记
注:笔记摘自《计算机算法设计与分析》王晓东 第五版
第二章 递归与分治策略
2.1 递归的概念
-
递归算法:一个直接或间接地调用自身的算法
-
递归函数:使用函数自身给出定义的函数
-
递归方程:对于递归算法,一般可把时间代价表示为一个递归方程
-
解递归方程最常用的方法是进行递归扩展
阶乘函数- 边界条件
- 递归关系
n ! = { 1 , n = 1 n ( n − 1 ) ! , n > 1 n!=\begin{cases} 1,n=1\\ n(n-1)!,n>1\end{cases} n!={ 1,n=1n(n−1)!,n>1
Fibonacci数列
F ( n ) = { 1 , n = 0 1 , n = 1 F ( n − 1 ) + F ( n − 2 ) , n > 1 F(n)=\begin{cases} 1,n=0\\ 1,n=1\\F(n-1)+F(n-2),n>1\end{cases} F(n)=⎩⎪⎨⎪⎧1,n=01,n=1F(n−1)+F(n−2),n>1
初始条件和递归方程是递归函数的两个要素
Ackerman函数
{ A ( 1 , 0 ) = 2 A ( 0 , m ) = 1 , m > = 0 A ( n , 0 ) = n + 2 , n > = 2 A ( n , m ) = A ( A ( n − 1 , m ) , m − 1 ) , n 、 m > = 1 \begin{cases} A(1,0)=2\\ A(0,m)=1,m>=0\\A(n,0)=n+2,n>=2\\A(n,m)=A(A(n-1,m),m-1),n、m>=1\end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧A(1,0)=2A(0,m)=1,m>=0A(n,0)=n+2,n>=2A(n,m)=A(A(n−1,m),m−1),n、m>=1
- Ackerman函数为双递归函数
- 双递归函数:一个函数及它的一个变量由函数自身定义
- A(n,m)的自变量m的每一个值都定义了一个单变量函数
注:需推一遍证明
排列问题
设R={r1,r2,…,rn}是要进行排列的n个元素,Ri=R-{ri}。集合X中的元素的全排列记为Perm(X)。
(ri)Perm(X)表示在全排列Perm(X)的每个排列前加上前缀ri得到的排列。
R的全排列定义如下:
当n=1时,Perm®=®,其中r是集合R中唯一的元素;
当n>1时,Perm®由(r1)Perm(R1),(r2)Perm(R2),…,(rn)Perm(Rn)构成。
为便于理解,以{1,2,3,4,5,6}为例:
1 2 3 4 5 6 --> 1 2 3 4 5 6 --> 1 2 3 4 5 6 --> 1 2 3 4 5 6 --> 1 2 3 4 5 6 --> 1 2 3 4 5 6
--> 1 2 3 4 6 5 --> 1 2 3 4 6 5
template<class Type>
void Perm(Type list[],int k, int m){ //产生list[k:m]的所有排列
if(k==m){ //只剩下一个元素,到达递归的最底层
for (int i=0;i<=m;i++)
cout << list[i];
cout << endl;
}
else{ //还有多个元素待排列,递归产生排列
for (int i=k;i<=m;i++)
{
Swap(list[k],list[i]);
Perm(list,k+1,m);
Swap(list[k],list[i]); //复位,保证所有元素都能依次做前缀
}
}
}
template<class Type>
inline void Swap(Type &a, Type &b)
{
Type temp=a;
a = b;
b = temp;
}
整数划分问题
将正整数n表示成一系列正整数之和,n=n1+n2+…+nk(n1≥n2≥…≥nk,k≥1)。
正整数n的不同的划分个数称为正整数n的划分数,记为p(n)。以正整数6为例:
6;
5+1;
4+2;4+1+1;
3+3;3+2+1;3+1+1+1;
2+2+2;2+2+1+1;2+1+1+1+1;
1+1+1+1+1+1。
将最大加数n1不大于m的划分个数记作q(n,m),建立如下递归关系:
q ( n . m ) = { 1 , n = 1 、 m = 1 q ( n , n ) , n < m 1 + q ( n , n − 1 ) , n = m q ( n , m − 1 ) + q ( n − m , m ) , n > m > 1 q(n.m)=\begin{cases} 1,n=1、m=1\\q(n,n),n<m\\1+q(n,n-1),n=m\\q(n,m-1)+q(n-m,m),n>m>1\end{cases} q(n.m)=⎩⎪⎪⎪⎨⎪⎪⎪⎧1,n=1、m=1q(n,n),n<m1+q(n,n−1),n=mq(n,m−1)+q(n−m,m),n>m>1
- q(n,1)=1,n≥1。当最大加数n1不大于1时,任何正整数n只有一种划分形式,即n=1+1+…+1
- q(n,m)=q(n,n),m≥n。最大加数n1不能大于n。
- q(n,n)=1+q(n,n-1)。正整数n的划分由n1=n的划分和n1≤n-1的划分组成;n1=n时,划分仅有一种。
- q(n,m-1)+q(n-m,m),n>m>1。正整数n的最大加数n1不大于m的划分由n1=m的划分和n1≤m-1的划分组成。
Hanoi塔问题
设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘时遵守:
- 每次只移动1个圆盘
- 任何时刻都不允许将较大的圆盘压到较小的圆盘之上
- 在满足1和2的前提下,可将圆盘移动至a,b,c中任一塔座上
T ( n ) = { 2 T ( n − 1 ) + 1 , n > 1 1 , n = 1 T(n)=\begin{cases}2T(n-1)+1,n>1\\1,n=1\end{cases} T(n)={ 2T(n−1)+1,n>11,n=1
void hanoi(int n,a,b,c) //a上n个盘借助c移到b
{
if (n==1) //将第1个盘从a移到b
move(1,a,b);
else{
hanoi(n-1,a,c,b); //将第1至n-1个盘,从a移到c
move(n,a,b); //将第n个盘从a移到b
hanoi(n-1,c,b,a); //将第1至n-1个盘从c移到b
}
}
使用这种递归调用的关键在于建立递归调用工作栈。
递归程序逐层调用需要分配存储空间,一旦某一层被启用,就要为之开辟新的空间。当一层执行完毕,释放相应空间,退到上一层。
递归优点:结构清晰,可读性强
递归缺点:递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多
解决方法:在递归算法中消除递归调用,使其转化为非递归算法。可采用一个用户定义的栈来模拟系统的递归调用工作栈。
2.2 分治策略
基本思想
将问题分解成若干个子问题,然后求解子问题。分治策略可以递归地进行,即子问题仍然可以用分值策略来处理,但最后的问题要非常基本而简单。
divide-and-conquer(P){
if (|P| <= n0) //直接解决小规模问题
adhoc(P);
divide P into smaller subinstances P1,P2,…,Pk //分解问题
for (i = 1; i<=k; i++) //递归解各子问题
yi=divide-and-conquer(Pi); //将格子问题的解合并为原问题的解
return merge(y1,y2,…,yk)
}
代价分析
{ O ( 1 ) , n = 1 k T ( n / m ) + f ( n ) , n > 1 \begin{cases}O(1),n=1\\kT(n/m)+f(n),n>1\end{cases} {
O(1),n=1kT(n/m)+f(n),n>1
T ( N ) = a T ( N / b ) + N k , a ≥ 1 , b > 1 T(N)=aT(N/b)+N^k,a≥1,b>1 T(N)=aT(N/b)+Nk,a≥1,b>1
T ( n ) = { O ( N t ) , t = l o g b a , a > b k O ( N k l o g N ) , a = b k O ( N k ) , a < b k T(n)=\begin{cases}O(N^t),t=log_ba,a>b^k\\O(N^klogN),a=b^k\\O(N^k),a<b^k\end{cases} T(n)=⎩⎪⎨⎪⎧O(Nt),t=logba,a>bkO(NklogN),a=bkO(Nk),a<bk
2.3 二分搜索技术
给定已按升序排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x。
-
顺序搜索方法:最好时1次,最坏时n次
-
二分搜索方法
基本思想
将n个元素分成个数大致相同的两半,取a[n/2]与x作比较。
- 如果x=a[n/2],则找到x,算法终止;
- 如果x<a[n/2],则在数组a的左半部继续搜索x;
- 如果x>a[n/2],则在数组a的右半部继续搜索x。
template<class Type>
int BinarySearch(Type a[],const Type& x, int l, int r) //l=0,r=n-1
{//找到x时返回其在数组中的位置,否则返回-1
while(r >= l){
int m = (l+r)/2;
if(x==a[m])
return m;
if (x<a[m])
r=m-1;
else
l=m+1;
}
return -1;
}
T ( n ) = { T ( n / 2 ) + O ( 1 ) , n > 1 O ( 1 ) , n = 1 T(n)=\begin{cases}T(n/2)+O(1),n>1\\O(1),n=1\end{cases} T(n)={ T(n/2)+O(1),n>1O(1),n=1
求解: T ( n ) = l o g n T(n)=logn T(n)=logn
复杂性: O ( l o g n ) O(logn) O(logn)
2.4 大整数的乘法
设X和Y都是n位二进制整数,计算它们的乘积 X Y XY XY。
小学方法: O ( n 2 ) O(n^2) O(n2)
将n位二进制整数 X X X和 Y Y Y分为2段,每段的长为n/2位,由此 X = A × 2 t + B , t = n / 2 X=A×2^t+B,t=n/2 X=A×2t+B,t=n/2, Y = C × 2 t + D , t = n / 2 Y=C×2^t+D,t=n/2 Y=C×2t+D,t=n/2
由此, X Y = ( A × 2 t + B ) ( C × 2 t + D ) = A C × 2 t + ( A D + B C ) × 2 t + B D , t = n / 2 XY=(A×2^t+B)(C×2^t+D)=AC×2^t+(AD+BC)×2^t+BD,t=n/2 XY=(A×2t+B)(C×2t+D)=AC×2t+(AD+BC)×2t+BD,t=n/2
分治法: X Y = A C × 2 n + ( ( A − B ) ( D − C ) + A C + B D ) × 2 t + B D , t = n / 2 XY=AC×2^n+((A-B)(D-C)+AC+BD)×2^t+BD,t=n/2 XY=AC×2n+((A−B)(D−C)+AC+BD)×2t+BD,t=n/2
仅需做3次n/2位整数的乘法(AC、BD和(A-B)(D-C))、6次加减法和2次移位。
T ( n ) = { O ( 1 ) , n = 1 3 T ( n / 2 ) + O ( n ) , n > 1 T(n)=\begin{cases}O(1),n=1\\3T(n/2)+O(n),n>1\end{cases} T(n)={
O(1),n=13T(n/2)+O(n),n>1
易求得,其解为 T ( n ) = O