第3章 递归与分治策略
任何一个可以用计算机求解的问题所需的计算时间都与其规模n有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。
分治法的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
如果原问题可分割成k个子问题(1<k≤n),且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。
由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。
递归算法
程序直接或间接调用自身的编程技巧称为递归算法(Recursion)。
一个过程或函数在其定义或说明中又直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归需要有边界条件、递归前进段和递归返回段。
- 当边界条件不满足时,递归前进;
- 当边界条件满足时,递归返回。
- 注意:在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口,否则将无限进行下去(死锁)。
递归的缺点:
- 递归算法解题的运行效率较低。
在递归调用过程中,系统为每一层的返回点、局部变量等开辟了堆栈来存储。递归次数过多容易造成堆栈溢出等。
Fibonacci数列
Fibonacci数列的递归算法:
int fib(int n){
if (n<=1) return 1;
return fib(n-1)+fib(n-2);
}
该算法的效率非常低,因为重复递归的次数太多。
Fibonacci数列的递推算法:
int fib[50]; //采用数组保存中间结果
void fibonacci(int n){
fib[0] = 1;
fib[1] = 1;
for (int i=2; i<=n; i++)
fib[i] = fib[i-1]+fib[i-2];
}
集合的全排列问题
设R={r1,r2… rn} 是要进行排列的n个元素,显然共有n!种排列。
令Ri=R-{ri}。集合X中元素的全排列记为perm(X),则(ri)perm(X) 表示在全
排列perm(X)的每一个排列前加上前缀ri得到的排列。
R的全排列可归纳定义如下:
当n=1时,perm(R)=(r), 其中r是集合R中唯一的元素;
当n>1时,perm(R) 由(r1)perm(R1),(r2)perm(R2), ···,(rn )perm(n)构成。
依此递归定义,可设计产生perm(R)的递归算法。
全排列问题的递归算法:
#include<bits/stdc++.h>
using namespace std;
void Perm(int list[], int k, int m){
if(k==m){
for(int i=0;i<=m;i++){
cout<<list[i]<<" ";
}
cout<<endl;
}
else{
for(int j=k;j<=m;j++){
swap(list[k],list[j]);
Perm(list,k+1,m);
swap(list[k],list[j]);
}
}
}
int main(){
int a[]={1,2,3,4,5,6,7};
Perm(a,2,4);
for(int i=2;i<=4;i++){
cout<<a[i]<<" ";
}
}
如:3、4、5的全排列
整数划分问题
整数划分问题是算法中的一个经典命题之一。把一个正整数n表示成一系列正整数之和:
正整数n的这种表示称为正整数n的划分。正整数n的不同划分个数称为正整数n的划分数,记作p(n) 。
正整数6有如下11种不同的划分,所以 。
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,n2, … ni}中的最大加数s不超过m,即s=max(n1,n2, … ni)≤m,则称它属于n的一个m划分。我们记n的m划分的个数为f(n,m)。该问题就转化为求n的所有划分个数f(n, n)。我们可以建立f(n, m)的递归关系:
1、f(1,m)=1, m≥1
当n=1时,不论m的值为多少(m>0),只有一种划分即1个1。
2、f(n,1)=1, n≥1
当m=1时,不论n的值为多少(n>0),只有一种划分即n个1。
3、f(n,m)= f(n,n), m≥n
最大加数s实际上不能超过n。例如,f(3,5)= f(3, 3)。
4、f(n,n)=1+ f(n,n-1)
正整数n的划分是由s=n的划分和s≤n-1的划分构成。例如,f(6,6)=1+ f(6,5)。
5、 f(n,m)= f(n,m-1)+ f(n-m, m),n>m> 1
正整数n的最大加数s不大于m的划分,是由s =m的划分和s≤m - 1的划分组成。
#include<bits/stdc++.h>
using namespace std;
int split(int n,int m){
if(n==1||m==1){
return 1;
}else if(n<m){
return split(n,n);
}else if(n==m){
return split(n,n-1)+1;
}else{
return split(n,m-1)+split(n-m,m);
}
}
int main(){
int n;
cout<<"输入:";
cin>>n;
cout<<"p("<<n<<")="<<split(n,n)<<endl;
}
递归算法
分治策略是对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同。
递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
基本步骤
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
- 合并:将各个子问题的解合并为原问题的解。
分治策略的算法设计模式
Divide_and_Conquer(P){
if (|P|<=n0 ) return adhoc(P);
divide P into smaller substances P1,P2,…,Pk;
for (i=1; i<=k; k++)
yi=Divide-and-Conquer(Pi) //递归解决Pi
Return merge(y1,y2,…,yk) //合并子问题
}
慕课
差消法化简高阶递推方程
快速排序:
假设 A[p…r] 的元素彼此不等
以首元素A[1]对数组 A[p…r]划分,使得: 小于x 的元素放在 A[ p…q-1]
大于 x 的元素放在 A[q+1…r]
• 递归对 A[ p…q-1]和 A[q+1…r] 排序
工作量:子问题工作量+划分工作量
有 n 种可能的输入
对每个输入,划分的比较次数都是 n-1
快速排序平均工作量
假设首元素排好序在每个位置是等概率的:
对于高阶方程应该先化简,然后迭代
利用两个方程相减,将右边的项尽可能消去,以达到降阶的目的
递归树
概念:
• 递归树是迭代计算的模型.
• 递归树的生成过程与迭代过程一致.
• 递归树上所有项恰好是迭代之后产
生和式中的项.
• 对递归树上的项求和就是迭代后方
程的解.
生成规则:
• 初始,递归树只有根结点, 其值为W(n)
• 不断继续下述过程:
将函数项叶结点的迭代式W(m)表示成二 层子树
用该子树替换该叶结点
• 继续递归树的生成,直到树中无函数项
(只有初值)为止.