转载请声明出处:http://blog.csdn.net/zhongkelee/article/details/44901905
一、算法整体思想
对k个子问题分别求解,如果子问题的规模仍然不够小,则再划分为k个子问题。 如此递归地进行下去,直到问题规模足够小,很容易求出其解为止。
将求出的小规模问题的解合并成一个更大规模的问题的解,自底向上逐步求出原来问题的解。
分治法的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
二、递归与分治概念
1. 递归的概念
直接或者间接地调用自身的算法称为递归算法;用函数自身给出定义的函数称为递归函数。
分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。原问题与子问题的唯一区别是输入规模不同。
在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。
分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
2. 分治的基本过程
Divide:将问题的规模变小
Conquer:递归的处理小规模问题(只需递归调用即可)
Combine:将小规模问题的解合并为原始问题的解
这其中的重点是问题的分解和解的合并操作。
分解的要点是:
a.分解的条件要互斥;
b.分解后的所有条件加在一起,能够覆盖原始问题的所有情况。
3.相关易混概念
递归是算法的实现方式,分治是算法的设计思想。
迭代是不断地循环过程,递归式不断地调用自身。
4.递归的弊端
递归调用的方式,在实际代码体现上,不断地在消耗内存中栈的空间,并且有相关的值拷贝动作,函数不断地压栈,导致资源不断地消耗。
三、实例
1.阶乘函数
阶乘函数可以递归地定义为:
边界条件和递归方程是递归函数的两个要素,递归函数只有具备了这两个要素,才能在有限次计算后得出结果。
阶乘函数非递归定义为:n! = 1 x 2 x 3 x ... x (n-1) x n
代码实现:
#include <iostream>
using namespace std;
// factorial implement by loop
long factorial_loop(long n){
long result = 1;
for (int i = n; i > 0; -- i)
result *= i;
return result;
}
// factorial implement by recursive
long factorial_recursive(long n){
if (n == 0)
return 1;
return n*factorial_recursive(n-1);
}
int main(){
for (int i = 0; i < 10; i ++ ) {
cout << i << "!" << " = "
<< factorial_recursive(i)
<< ","
<< factorial_loop(i)
<< endl;
}
return 0;
}
运行结果:
2.Fibonacci数列
无穷数列1,1,2,3,5,8,13,21,34,55,...,称为Fibonacci数列。它可以递归地定义为:
非递归定义为:
代码实现:
#include <iostream>
using namespace std;
long fibonacci_recursive(long n){
if (n <= 1)
return 1;
return fibonacci_recursive(n-1)+fibonacci_recursive(n-2);
}
long fibonacci_loop(long n){
if (n <= 1)
return 1;
long f1 = 1;
long f2 = 1;
long result = 0;
for (int i = 1; i < n; i++){
result = f1 + f2;
f1 = f2;
f2 = result;
}
return result;
}
int main()
{
cout << "fibonacci implement by recursive: " << endl;
for (long i = 0; i <= 20; ++ i)
cout << fibonacci_recursive(i) << " " ;
cout << endl << endl;
cout << "fibonacci implement by loop: " << endl;
for (long i = 0; i <= 20; ++ i)
cout << fibonacci_loop(i) << " " ;
cout << endl;
return 0;
}
运行结果:
3.Ackerman函数
当一个函数及它的一个变量是由函数自身定义时,称这个函数是双递归函数。
Ackerman函数A(n, m)定义如下:
并非一切递归函数都能用非递归方式定义,比如本例中的Ackerman函数就无法找到非递归的定义。
A(n,m) 的自变量 m 的每一个值都定义了一个单变量函数:
m = 0 时,A(n,0) = 2
m = 1 时,A(n,1) = A(A(n-1,1),0)=A(n-1,1)+2 和 A(1,1)=2,故 A(n,1)=2*n
m = 2时,A(n,2)=A(A(n-1,2),1)=2A(n-1,2) 和 A(1,2)=A(A(A(0,2)),1)=A(1,1)=2,A(n,2)=2^n
m = 3时,类似的可以推出
m = 4时,A(n,4)的增长速度非常快,以至于没有适当的数学式子来表示这一函数。
代码实现:
#include <iostream>
using namespace std;
// ackerman implement
long ackerman(long n, long m){
if (n == 1 && m == 0)
return (long)2;
if (n == 0 && m >= 0)
return 1;
if (m == 0 && n >= 2)
return n + 2;
if (n >= 1 && m>= 1)
return ackerman( ackerman(n-1,m) , m-1);
}
int main()
{
cout << "m = 0 : " << endl;
cout << "ackerman(1,0) = " << ackerman(1,0) << endl;
cout << "ackerman(2,0) = " << ackerman(2,0) << endl;
cout << "ackerman(3,0) = " << ackerman(3,0) << endl;
cout << "ackerman(4,0) = " << ackerman(4,0) << endl;
cout << "m = 1 : " << endl;
cout << "ackerman(1,1) = " << ackerman(1,1) << endl;
cout << "ackerman(2,1) = " << ackerman(2,1) << endl;
cout << "ackerman(3,1) = " << ackerman(3,1) << endl;
cout << "ackerman(4,1) = " << ackerman(4,1) << endl;
cout << "m = 2 : " << endl;
cout << "ackerman(1,2) = " << ackerman(1,2) << endl;
cout << "ackerman(2,2) = " << ackerman(2,2) << endl;
cout << "ackerman(3,2) = " << ackerman(3,2) << endl;
cout << "ackerman(4,2) = " << ackerman(4,2) << endl;
cout << "m = 3 : " << endl;
cout << "ackerman(1,3) = " << ackerman(1,3) << endl;
cout << "ackerman(2,3) = " << ackerman(2,3) << endl;
cout << "ackerman(3,3) = " << ackerman(3,3) << endl;
cout << "ackerman(4,3) = " << ackerman(4,3) << endl;
return 0;
}
运行结果:
4. 排列问题
设计一个递归算法生成 n 个元素 {r1,r2,...,rn} 的全排列。
设 R = {r1,r2,...,rn} 是要进行排列的 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(Rn) 构成。
依此递归定义,可以设计产生Perm(R)的递归代码:
#include <iostream>
#include <vector>
#include <iterator>
using namespace std;
/* 使用递归实现
* 递归产生所有前缀是list[0:k-1],
* 且后缀是list[k,m]的全排列的所有排列
* 调用算法perm(list,0,n-1)则产生list[0:n-1]的全排列
*/
template <class T>
void perm_recursion(T list[],int k,int m)
{
// 产生list[k:m]的所有排列
if (k == m) {
//只剩下1个元素
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_recursion(list,k+1,m);
swap(list[k],list[i]);
}
}
}
// 非递归实现(可参照STL next_permutation源码)
template <class T>
void perm_loop(T list[],int len)
{
int i,j;
vector<int> v_temp(len);
// 初始排列
for(i = 0; i < len ; i ++)
v_temp[i] = i;
while (true) {
for (i = 0; i < len; i ++ )
cout << list[v_temp[i]] << " ";
cout << endl;
// 从后向前查找,看有没有后面的数大于前面的数的情况,若有则停在后一个数的位置。
for(i = len - 1;i > 0 && v_temp[i] < v_temp[i-1] ; i--);
if (i == 0)
break;
// 从后查到i,查找大于 v_temp[i-1]的最小的数,记入j
for(j = len - 1 ; j > i && v_temp[j] < v_temp[i-1] ; j--);
// 交换 v_temp[i-1] 和 v_temp[j]
swap(v_temp[i-1],v_temp[j]);
// 倒置v_temp[i]到v_temp[n-1]
for(i = i,j = len - 1 ; i < j;i ++,j --) {
swap(v_temp[i],v_temp[j]);
}
}
}
int main()
{
int list[] = {0,1,2};
cout << "permutation implement by recursion: " << endl;
perm_recursion(list,0,2);
cout << endl;
cout << "permutation implement by loop: " << endl;
perm_loop(list,3);
cout << endl;
return 0;
}
运行结果:
5.整数划分问题
将正整数 n 表示成一系列正整数之和:n = n1+n2+...+nk,其中n1≥n2≥...≥nk,k≥1。
正整数n的这种表示成为正整数n的划分。正整数n的不同划分个数称为正整数n的划分数,记做p(n)。
例如,正整数6有如下11种不同的划分,所以p(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;
前面几个例子中,问题本身都具有比较明显的递推关系,因而容易用递归函数直接求解。
在本例中,如果设p(n)为正整数n的划分数,则难以找到递归关系,因此考虑增加一个自变量。
在正整数n的所有不同划分中,将最大加数n1不大于m的划分个数记作q(n,m)。
可以建立q(n,m)的如下递归关系。
(1) q(n,1)=1,n >= 1;当最大加数n1不大于1时,任何正整数n只有一种划分形式,即n = 1 + 1 + … +1.
(2) q(n,m) = q(n,n),m >= n;最大加数n1实际上不能大于n。因此,q(1,m)=1。
(3) q(n,n)=1 + q(n,n-1);正整数n的划分由n1=n的划分和n1 ≤ n-1的划分组成。
(4) q(n,m)=q(n,m-1)+q(n-m,m),n > m >1;正整数n的最大加数n1不大于m的划分由n1 = m的划分和n1 ≤ m-1的划分组成。
以上的关系实际上给出了计算q(n,m)的递归式如下:
据此,可设计计算q(n,m)的递归函数如下。正整数n的划分数p(n) = q(n,n)。
#include <iostream>
using namespace std;
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 1 + q(n,m-1);
return q(n,m-1) + q(n-m,m);
}
int p(int n){
return q(n,n);
}
int main()
{
for (int i = 1; i < 7; i++){
cout<<"interger_partition("
<<i
<<") = "
<<p(i)
<<endl;
}
return 0;
}
运行结果:
6.汉诺塔问题
hanoi(n, a, b, c)表示按照游戏规则,n块圆盘从a塔移至b塔,c塔作为辅助塔。
move(a, b)表示将塔a最上面的圆盘移至塔b,并输出这一步骤。
<span style="font-size:14px;">#include <iostream>
using namespace std;
void move(char t1, char t2){
cout<<t1<<" -> "<<t2<<endl;
}
void hanoi(int n, char a, char b, char c){
if (n > 0){
hanoi(n-1, a, c, b);
move(a,b);
hanoi(n-1, c, b, a);
}
}
int main()
{
cout << "hanoi(1,'a','b','c'): " << endl;
hanoi(1,'a','b','c');
cout << endl;
cout << "hanoi(2,'a','b','c'): " << endl;
hanoi(2,'a','b','c');
cout << endl;
cout << "hanoi(3,'a','b','c'): " << endl;
hanoi(3,'a','b','c');
cout << endl;
cout << "hanoi(4,'a','b','c'): " << endl;
hanoi(4,'a','b','c');
cout << endl;
return 0;
}</span>
运行结果:
三、递归小结
优点:结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。
缺点:递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。
解决方法:在递归算法中消除递归调用,使其转化为非递归算法。
1、采用一个用户定义的栈来模拟系统的递归调用工作栈。该方法通用性强,但本质上还是递归,只不过人工做了本来由编译器做的事情,优化效果不明显。
2、用递推来实现递归函数。
3、通过变换能将一些递归转化为尾递归,从而迭代求出结果。
后两种方法在时空复杂度上均有较大改善,但其适用范围有限。
好了,递归与分治算法的第一部分内容就到这里,后一篇博客我们着重讲一下分治法的基本思想和几个经典实例。
有任何问题请和我联系,共同进步:[email protected]
转载请声明出处:http://blog.csdn.net/zhongkelee/article/details/44901905