递归与分治策略
2016-07-29 00:00
260 查看
1、全排列问题: 设R(n)={r[1],r[2],…,r
}是要进行排列的n个元素。集合X中元素的全排列记为Perm(X)。求R(n)的全排列Perm(R(n))。
用递归算法求解:
(1)找出递归子结构性质:即原问题的解包含了子问题的解,且子问题的描述与原问题相同。这就可以用子问题的解来构造原问题的解。设R(i)=R(n)-{r[i]},这是一个子问题。设(r[i])Perm(X)表示在全排列Perm(X)的每一个排列前加上前缀r[i]得到的排列。则Perm(R(n))由(r[1])Perm(R(1)),(r[2])Perm(R(2)),...,(r
)Perm(R(n))构成。这样我们就用子问题的解定义出原问题的解,只不过这里用的是多个子问题的解,很多简单的问题只用一个子问题的解就可定义原问题的解,比如常见的用f(n-1)定义f(n)。
(2)找出递归终止条件:设计递归算法时必须要有递归终止条件,否则就不能使用递归算法来求解。终止条件必须是一个不再依赖于子问题的可计算出的值。这里n=1时递归终止,这时集合中只有一个元素,因此Perm(R(1))={r[1]},不再依赖于子问题了。
算法递归地产生所有前缀是r[0]r[1]...r[k-1],后缀是r[k]...r[m]的全排列。算法将r[k]~r[m]中的每一个元素与r[k]交换,然后递归地计算r[k+1]~r[m]的全排列,并将结果作为r[0]...r[k]的后缀。调用Perm(r,0,n-1)即得原问题的解。
2、整数划分问题: 将一个正整数n表示成一系列正整数之和,n=n[1]+n[2]+...+n[k],其中n[1]>=n[2]>=...>=n[k]>=1,k>=1。正整数n的一个这种表示称为n的一个划分。求n的不同划分个数。
用递归算法求解:
(1)递归子结构性质:显然n的一个划分中包含了某个子问题t(<n)的一个划分。这里问题的解是划分数,关键是要研究用子问题的划分数来定义原问题的划分数。在n的所有不同划分中,将其中最大加数n[1]不大于m的划分数个记作q(n,m),则q(n,n)为原问题的解。递归定义如下:
1)当n<1或m<1时,显然q(n,m)=0;
2)当m=1时,n只有一种划分形式,n=1+1+...+1,共n个1相加。q(n,1)=1;
3)当n=1时,显然也只有一种划分形式,q(1,m)=1;
4)当n<m时,因为n[1]不能大于n,故q(n,m)=q(n,n);
5)当n=m时,q(n,n)为n的所有划分个数,它由n[1]<=n-1的划分和n[1]=n的划分(只有一个)组成,q(n,n)=q(n,n-1)+1;
6)当n>m>1时,n[1]不大于m的划分,由n[1]<=m-1的划分和n[1]=m的划分组成,q(n,m)=q(n,m-1)+q(n-m,m)。
(2)递归终止条件:上述1),2),3)即为递归的终止条件。在分析递归终止条件时特别注意要使所有的子问题都能递归终止,否则如果某个子问题不能递归终止,会导致无穷的递归调用。
递归算法结构简洁清晰,可读性强,但运行时间和占用的存储空间一般都比非递归算法要多。系统是通过栈结构来完成递归的展开,因此若要把递归算法转化为非递归算法,通常是采用一个用户定义的栈来模拟系统的调用工作栈,以达到消除递归的目的。
3、线性时间选择问题: 给定n个元素a[0],a[1],...,a[n-1],在线性时间内找出其中第k小的元素,0<=k<=n-1。
一种方法是对n个元素进行排序,然后直接取出第k小的元素,比如用快速排序,但这需要O(nlogn)的运行时间,不符合要求。我们可以改进快速排序算法来达到O(n)的运行时间,快速排序用到了分治法的思想。
分治法的基本思想是将一个规模为n的问题分解为m个规模较小的子问题,这些子问题互相独立且与原问题相同,分别解决各个子问题后,然后将各子问题的解合并得到原问题的解。可见分治法也是用各个子问题的解来构造原问题的解,相当于一种特殊的递归算法,只不过一般的递归算法中各个子问题不一定相互独立,可能两个子问题会同时依赖于第三个子问题。比如上述的q(50,6),q(50,5)都会递归展开到q(40,3),因此这个子问题会被重复计算(要消除重复计算需要用动态规划算法)。可见,分治法程序一般是一个自顶向下的递归算法。当然对一些简单的问题,我们也可以自底向上地以非递归的方式计算问题的解,以提高运行效率。
二分搜索、快速排序、归并排序都是分治法的典型应用。
(1)问题分解:设用Select(a[],0,n-1,k)返回原问题的解,则可以把它划分成一系列子问题Select(a[],p,r,k)。从大量的实践来看,分治法中的问题分解一般应使各个子问题的规模大致相同,这样最有效率。这里我们采用与快速排序类似的思想,但需要做改进,即把原问题划分成2个规模相当的子问题,这样才能达到O(n)的运行时间。
快速排序中是以a[p]为划分基准,将a[p:r]划分成3段a[p:q-1],a[q]和a[q+1:r],前一段的任一元素都小于a[q],后一段的任一元素都大于a[q],q在划分过程确定。但这会导致O(nlogn)的平均运行时间,因为这个划分基准不对称,它不能保证子问题a[p:q-1]和a[q+1:r]的规模大致相同。如果我们从a[p:r]中随机地选择一个元素a[u](p<=u<=r)作为划分基准,这样的划分就是对称的,可以证明对称的划分能够在O(n)的平均时间内找出第k小的元素。这样原问题就分解成了两个子问题Select(a[],p,q,k)和Select(a[],q+1,r,k)。
这里Random(p,r)产生p和r之间的一个随机整数,且产生不同整数的概率相同。
(2)子问题求解:由于用两个子问题来定义原问题,完成了递归定义,因此可直接对两个子问题进行递归求解,这里的问题求解就是对数组进行排序。p=r时递归终止,直接返回a[p]。
(3)合并子问题的解:由于排序是就地进行的,在两个子问题都排好序时无需执行任何合并操作,原问题就已经排好序,然后直接返回第k小的元素即可。在归并排序中就有对子问题解的合并过程。
可以证明这个分治算法的平均运行时间为O(n),在最坏情况下则为O(n**2),例如在找最小元素时总是在最大元素处划分。
递归算法基本思想:找出递归子结构性质(原问题的解包含了子问题的解)、用子问题的解来递归定义原问题的解、找出递归终止条件。
分治法基本思想:问题分解(分解成k个规模大致相同的子问题)、子问题递归求解、合并各个子问题的解。
}是要进行排列的n个元素。集合X中元素的全排列记为Perm(X)。求R(n)的全排列Perm(R(n))。
用递归算法求解:
(1)找出递归子结构性质:即原问题的解包含了子问题的解,且子问题的描述与原问题相同。这就可以用子问题的解来构造原问题的解。设R(i)=R(n)-{r[i]},这是一个子问题。设(r[i])Perm(X)表示在全排列Perm(X)的每一个排列前加上前缀r[i]得到的排列。则Perm(R(n))由(r[1])Perm(R(1)),(r[2])Perm(R(2)),...,(r
)Perm(R(n))构成。这样我们就用子问题的解定义出原问题的解,只不过这里用的是多个子问题的解,很多简单的问题只用一个子问题的解就可定义原问题的解,比如常见的用f(n-1)定义f(n)。
(2)找出递归终止条件:设计递归算法时必须要有递归终止条件,否则就不能使用递归算法来求解。终止条件必须是一个不再依赖于子问题的可计算出的值。这里n=1时递归终止,这时集合中只有一个元素,因此Perm(R(1))={r[1]},不再依赖于子问题了。
//产生r[k]~r[m]的所有排列 template<class Type> void Perm(Type r[],int k,int m){ if(k==m){ //只剩下一个元素 for(int i=0;i<=m;i++) cout<<r[i]; cout<<endl; }else{ //还有多个元素待排列,递归产生排列 for(int i=k;i<=m;i++){ Swap(r[k],r[i]); //r[k]~r[m]中的每一个元素与r[k]交换 Perm(r,k+1,m); //然后递归地计算r[k+1]~r[m]的全排列 Swap(r[k],r[i]); } } } template<class Type> inline void Swap(Type& a,Type& b){ Type temp=a; a=b; b=temp; }
算法递归地产生所有前缀是r[0]r[1]...r[k-1],后缀是r[k]...r[m]的全排列。算法将r[k]~r[m]中的每一个元素与r[k]交换,然后递归地计算r[k+1]~r[m]的全排列,并将结果作为r[0]...r[k]的后缀。调用Perm(r,0,n-1)即得原问题的解。
2、整数划分问题: 将一个正整数n表示成一系列正整数之和,n=n[1]+n[2]+...+n[k],其中n[1]>=n[2]>=...>=n[k]>=1,k>=1。正整数n的一个这种表示称为n的一个划分。求n的不同划分个数。
用递归算法求解:
(1)递归子结构性质:显然n的一个划分中包含了某个子问题t(<n)的一个划分。这里问题的解是划分数,关键是要研究用子问题的划分数来定义原问题的划分数。在n的所有不同划分中,将其中最大加数n[1]不大于m的划分数个记作q(n,m),则q(n,n)为原问题的解。递归定义如下:
1)当n<1或m<1时,显然q(n,m)=0;
2)当m=1时,n只有一种划分形式,n=1+1+...+1,共n个1相加。q(n,1)=1;
3)当n=1时,显然也只有一种划分形式,q(1,m)=1;
4)当n<m时,因为n[1]不能大于n,故q(n,m)=q(n,n);
5)当n=m时,q(n,n)为n的所有划分个数,它由n[1]<=n-1的划分和n[1]=n的划分(只有一个)组成,q(n,n)=q(n,n-1)+1;
6)当n>m>1时,n[1]不大于m的划分,由n[1]<=m-1的划分和n[1]=m的划分组成,q(n,m)=q(n,m-1)+q(n-m,m)。
(2)递归终止条件:上述1),2),3)即为递归的终止条件。在分析递归终止条件时特别注意要使所有的子问题都能递归终止,否则如果某个子问题不能递归终止,会导致无穷的递归调用。
int q(int n,int m){ if((n<1) || (m<1)) return 0; if((n==1) || (m==1)) return 1; if(n<m) return q(n,n); if(n==m) return q(n,m-1)+1; return q(n,m-1)+q(n-m,m); }
递归算法结构简洁清晰,可读性强,但运行时间和占用的存储空间一般都比非递归算法要多。系统是通过栈结构来完成递归的展开,因此若要把递归算法转化为非递归算法,通常是采用一个用户定义的栈来模拟系统的调用工作栈,以达到消除递归的目的。
3、线性时间选择问题: 给定n个元素a[0],a[1],...,a[n-1],在线性时间内找出其中第k小的元素,0<=k<=n-1。
一种方法是对n个元素进行排序,然后直接取出第k小的元素,比如用快速排序,但这需要O(nlogn)的运行时间,不符合要求。我们可以改进快速排序算法来达到O(n)的运行时间,快速排序用到了分治法的思想。
分治法的基本思想是将一个规模为n的问题分解为m个规模较小的子问题,这些子问题互相独立且与原问题相同,分别解决各个子问题后,然后将各子问题的解合并得到原问题的解。可见分治法也是用各个子问题的解来构造原问题的解,相当于一种特殊的递归算法,只不过一般的递归算法中各个子问题不一定相互独立,可能两个子问题会同时依赖于第三个子问题。比如上述的q(50,6),q(50,5)都会递归展开到q(40,3),因此这个子问题会被重复计算(要消除重复计算需要用动态规划算法)。可见,分治法程序一般是一个自顶向下的递归算法。当然对一些简单的问题,我们也可以自底向上地以非递归的方式计算问题的解,以提高运行效率。
二分搜索、快速排序、归并排序都是分治法的典型应用。
(1)问题分解:设用Select(a[],0,n-1,k)返回原问题的解,则可以把它划分成一系列子问题Select(a[],p,r,k)。从大量的实践来看,分治法中的问题分解一般应使各个子问题的规模大致相同,这样最有效率。这里我们采用与快速排序类似的思想,但需要做改进,即把原问题划分成2个规模相当的子问题,这样才能达到O(n)的运行时间。
快速排序中是以a[p]为划分基准,将a[p:r]划分成3段a[p:q-1],a[q]和a[q+1:r],前一段的任一元素都小于a[q],后一段的任一元素都大于a[q],q在划分过程确定。但这会导致O(nlogn)的平均运行时间,因为这个划分基准不对称,它不能保证子问题a[p:q-1]和a[q+1:r]的规模大致相同。如果我们从a[p:r]中随机地选择一个元素a[u](p<=u<=r)作为划分基准,这样的划分就是对称的,可以证明对称的划分能够在O(n)的平均时间内找出第k小的元素。这样原问题就分解成了两个子问题Select(a[],p,q,k)和Select(a[],q+1,r,k)。
//用随机选择策略将a[p:r]划分成3段 template<class Type> int RandomizedPartition(Type a[],int p,int r){ int i=p,j=r+1; Type temp; int u=Random(p,r); //产生p和r之间的一个随机整数,且产生不同整数的概率相同 //交换a[u]和a[p] temp=a[u]; a[u]=a[p]; a[p]=temp; Type x=a[p]; //以原来a[u]的值为划分基准 //将a[p+1:r]中小于x的元素交换到左边区域,大于x的元素交换到右边区域 while(true){ while(a[++i]<x); //从左往右找第一个>x的值 while(a[--j]>x); //从右往左找第一个<x的值 if(i>=j) break; temp=a[i]; a[i]=a[j]; a[j]=temp; //交换这两个值 } //划分完后把基准元素的值放到相应的q(=j)位置,并返回这个位置 a[p]=a[j]; a[j]=x; return j; }
这里Random(p,r)产生p和r之间的一个随机整数,且产生不同整数的概率相同。
(2)子问题求解:由于用两个子问题来定义原问题,完成了递归定义,因此可直接对两个子问题进行递归求解,这里的问题求解就是对数组进行排序。p=r时递归终止,直接返回a[p]。
(3)合并子问题的解:由于排序是就地进行的,在两个子问题都排好序时无需执行任何合并操作,原问题就已经排好序,然后直接返回第k小的元素即可。在归并排序中就有对子问题解的合并过程。
template<class Type> Type Select(Type a[],int p,int r,int k){ if(p==r) return a[p]; //递归终止 int i= RandomizedPartition(a,p,r); //得到划分后的基准元素位置q int j=i-p+1; //a[i]是第i-p+1小的元素 if(k<=j) return Select(a,p,i,k); //在前一个子问题中寻找第k小的元素#define else return Select(a,i+1,r,k-j); //a[i+1:r]前面有j个元素,故a[i+1:r]中第k-j小的元素为 //为a[p:r]第k小的元素 }
可以证明这个分治算法的平均运行时间为O(n),在最坏情况下则为O(n**2),例如在找最小元素时总是在最大元素处划分。
递归算法基本思想:找出递归子结构性质(原问题的解包含了子问题的解)、用子问题的解来递归定义原问题的解、找出递归终止条件。
分治法基本思想:问题分解(分解成k个规模大致相同的子问题)、子问题递归求解、合并各个子问题的解。
相关文章推荐
- libxml2剖析(4):XML技术概览
- 面向Web开发人员和网站管理员的Web缓存指南
- Web负载均衡的几种实现方式
- Java集合框架官方教程(4):Set/List/Map/Queue/Deque实现
- C++ Primer学习心得和遇到的问题
- C标准库源码解剖(4):字符串处理函数string.h和wchar.h
- NoSQL数据库面面观
- 堆结构的运用
- 深入理解JavaBean(2):属性编辑器PropertyEditor
- Hadoop学习路线图
- 第1部分:Spring框架概述
- 最佳实践:更好地设计你的REST API
- C标准库源码解剖(2):错误报告errno.h
- 深入浅出REST
- Oracle官方并发教程(1)
- 豆瓣的基础架构
- 面向切面编程(1):AOP介绍
- 23种设计模式分析(7):行为型模式
- 整数集合的实现
- 服务器设计系列:定时器