递归算法

          递归就是函数间接的调用自己, 它的实现基于函数参数传递的栈机制, 每次递归递归调用都会多一个栈帧——和简单的函数调用并没有什么不同 (都是使用了调用栈)。调用自己和调用其它函数并没有本质的区别, 都是建立新栈帧, 传递参数并修改当前代码行。在函数体执行完毕后删除栈帧, 处理返回值并修改当前代码行。

          递归在数据结构中占有很重要的,特别是, 树和图的建立, 和遍历。 使用递归能使代码更清晰, 更简洁, 但是调试时会很麻烦, 所以, 对于递归要熟练应用且小心应用。

 

下面是几个简单的应用举例:

<1>

Fibonacci数列                           1                            n = 0, 1

                          F(n) =   

                                              F(n-1)  + F(n-2)     n>=2

 

 1 #include<iostream>
 2 using namespace std;
 3 
 4 int fibonacci(int n)
 5 {
 6     if(n<=1) return 1;
 7     return fibonacci(n-1) + fibonacci(n-2);
 8 } 
 9 
10 int main()
11 {
12     int n;
13     while(scanf("%d", &n)!=EOF)
14     {
15         n = fibonacci(n);
16         printf("%d\n", n);
17     }
18     return 0;
19 }
View Code
//较优化的Fibonacci数列。 
#include<cstdio>
using namespace std;
int a[100];

int Fib(int n)
{
    if (n <= 1) 
        return a[1]=1;
      else {
        a[n] = Fib(n - 1) + Fib(n - 2);
        return a[n];
    }
}

int main()
{
    int n;
    while(scanf("%d", &n)!=EOF)
    {
        n = Fib(n); 
        printf("%d\n", n);
    }
    return 0;
    
}
View Code

 

此题 有更快的解法, 用矩阵快速幂求解 构造2*2 的矩阵{ 1, 1, 1, 0}; 这个代码后面在矩阵的应用中会给出。

其实斐波那契数列是一个十分有趣的数列, a[n+2] - a[n+1] - a[n] = 0 。 如果我们令 q^(n+2) - q^(n+1) - q^n = 0。

可以解出 q 的两个值, 然后 利用这两个值,和前两项列出一个二元一次方程,解出系数。这样就求出了 斐波那契数列的通项!

斐波那契数列还具有周期性, 具有一些循环性的性质。

<2>. 汉诺塔问题, 有三个钢针, A, B, C, A 上有n个圆盘, 大的在下面, 小的在上面, 你可以借借助B针, 把A针上的圆盘移到C盘上,(任何时候较大盘都不能放在较小盘上), 对于一个不太大的n(据说n等于64时, 每秒移动一次, 宇宙会在完成的那一瞬间毁灭, 嘿嘿!!), 请给出移动次数最少的解决方案!

 

 1 #include<stdio.h>
 2 
 3 void Move(int n, char A, char B)
 4 {
 5     printf("Move disk %d from %c to %c\n", n, A, B);
 6 } 
 7 
 8 void hanoi(int n, char A, char B, char C)
 9 {
10     if(n==1) Move(1, A, C);
11     else
12     {
13         hanoi(n-1, A, C, B);
14         Move(n, A, C);
15         hanoi(n-1, B, A, C);
16     }
17 }
18 
19 int main()
20 {
21     int n;
22     while(scanf("%d", &n)!=EOF, n)
23     {
24         hanoi(n, 'A', 'B', 'C');
25     }
26     return 0;
27 }
View Code

 程序解释: 当n==1时, 很显然, 直接把它从A针上移动到C针上就好了。即: if(n==1) Move(1, A, C);

      当 n > 1时, 我们可以这样考虑, 我们可以把A针上面的 n-1个盘借助C针, 移动到B针上,然后把A针上的盘移动到C盘上。

     然后再把B盘上的n-1个盘, 借助于A盘, 移动到C盘。

很显然, 移动n个盘的问题就转化成了移动n-1个盘的问题。依次类推, 此问题最终变成求解移动一个盘的问题。

 

 

 

<3>并非一切递归函数都能用非递归方式定义, 也就是说, 有些函数只能用递归的形式进行定义或描述。 例如 双递归函数——Ackerman函数。当一个函数以及它的一个变量是由函数自身定义时, 称这个函数是双递归函数。 Acerman函数 A(n, m)有两个独立的整变量m>=0和n>=0, 其定义如下:

 

                                                                        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 。 这个函数增长快的惊人。

 1 #include<cstdio>
 2 #include<cstring>
 3 using namespace std;
 4 
 5 typedef long long LL;
 6 LL ans = 0;
 7 
 8 LL A(LL n, LL m)
 9 {   
10     if(n==1&&m==0) return 2;
11     if(n==0&&m>=0) return 1;
12     if(n>=2&&m==0) return  n+2;
13     return A(A(n-1, m), m-1);
14 } 
15 
16 int main()
17 {
18     LL a, b;
19     while(scanf("%lld%lld", &a, &b)!=EOF)
20     {
21         ans = 0;
22         ans=A(a, b);
23         printf("%lld\n", ans);    
24     }
25     return 0;
26 }
27  
View Code


<4> 全排列问题。1~n 的全排列。

 1 #include<cstdio>
 2 #include<algorithm>
 3 using namespace std;
 4 
 5 int n = 0;
 6 int list[100];
 7 void perm(int list[], int k, int m)
 8 {
 9     int i;//显示输出 
10     if(k>m)
11     {
12         for(i=0; i<=m; i++)
13         printf("%d", list[i]);
14         printf("\n");
15         n++; 
16     }
17     else 
18     {
19         for(i=k; i<=m; i++)
20         {
21             swap(list[k], list[i]); //交换第k个数和第i个数,并把后面的数全排列 
22             perm(list, k+1, m);//全排列 k+1到m 
23             swap(list[k], list[i]);//第一步的逆操作,使数列保持不变,以避免重复。 
24         }
25     } 
26 }
27 
28 int main()
29 {
30     int T;
31     while(scanf("%d", &T)!=EOF)
32     {
33         for(int i=0; i<T; i++)
34         list[i] = i+1;
35         perm(list, 2, T-1);
36         printf("total:%d\n", n);
37     }
38     return 0;
39 }
View Code

 此问题有多种解法, 大都应用了递归方法。<algorithm>里面也包含了生成全排列的算法

next_permutation(begin, end);

 

<5>整数划分问题

如 4 可以划分为

4

3 + 1

2+ 2 , 2+1+1

1+1 + 1+1

 

在整数n的所有不同 的划分中, 将最大加数n1 不大于 m 的划分记作 q(n, m)。可以建立q(n, m) 的如下递归关系。

(1)     q(n, 1) = 1 , n>=1  

加数只能全部是 1

(2)q(n, m)=q(n, n), m>=n

m>=n, n 不可能有比n自身还大的加数。

(3) q(n, n)= 1 + q(n, n - 1)

整数n的划分 分成 1 + (n-1) 和 q(n, n-1)两种情况

(4) q(n, m) =  q (n,m-1) + q(n-m, m), n>m>1

分为最大加数 n1 <=m-1时的情况, 和 n1==m时的情况。 而n1==m的情况 刚好是q(n-m,m)。(如果不太明白请结合实例理解)。

 

q(n, m) = 1 ,  n=1, m=1

q(n, m) = q(n, n)    n<m

q(n, m) = 1 + q(n, n-1)  n=m

q(n, m) = q(n, m-1) + q(n-m, m)

 

 1 #include<iostream>
 2 using namespace std;
 3 
 4 int q(int n, int m)
 5 {
 6     if((n<1)||(m<1)) return 0;
 7     if((n==1)||(m==1)) return 1;
 8     if(n<m) return q(n, n);
 9     if(n==m) return q(n, m-1) + 1;
10     return q(n, m-1) + q(n-m, m);
11 }
12 
13 int main()
14 {
15     int a, ans = 0;
16     while(scanf("%d", &a)!=EOF)
17     {
18         ans=q(a, a);
19         printf("%d\n", ans);
20     }
21     return 0;
22 }
View Code

此问题也有另外一种特别巧妙的解决办法: 生成函数。

 

《5》递归求最大公因数和最小公倍数

 1 #include<iostream>
 2 using namespace std;
 3  
 4 //递归, 求最大公因数
 5 int gcd(int a, int b)
 6 {
 7     return b ? gcd(b, a%b) : a;
 8 }
 9 
10 int main()
11 {
12     int n, m;
13     while(scanf("%d%d",  &n, &m)!=EOF)
14     {
15         printf("%d\n", gcd(n, m));
16     }
17     return 0;
18 } 
View Code
 1 #include<iostream>
 2 using namespace std;
 3  
 4 //递归, 求最小公倍数
 5 int gcd(int a, int b)
 6 {
 7     return b ? gcd(b, a%b) : a;
 8 }
 9 
10 int main()
11 {
12     int n, m;
13     while(scanf("%d%d",  &n, &m)!=EOF)
14     {
15         printf("%d\n", n/gcd(n, m)*m);
16     }
17     return 0;
18 } 
View Code

   对递归语法更详细的介绍

《6》半数集问题

给定一个自然数n,由n开始可以依次产生半数集set(n)中的数如下。

(1) n    set(n);

(2) 在n的左边加上一个自然数,但该自然数不能超过最近添加的数的一半;

(3) 按此规则进行处理,直到不能再添加自然数为止。

例如,set(6)={6,16,26,126,36,136}。

半数集set(6)中有6个元素。

l注意半数集是多重集。

对于给定的自然数n,编程计算半数集set(n)中的元素个数。

设set(n)中的元素个数为      F(n)    ,则显然有:

  F(n) = 1 + F(1) + F(2) + …… + F(n/2)

第一次半数集

112

212

312

412

512

612

忽略原始数字12

1

2

3

4

5

6

 

 

12

13

14

24,124

15

25,125

16

26,126

36,136

                                                                     以 n 为12 为例!

 1 #include<iostream>
 2 #include<cstring>
 3 using namespace std;
 4 
 5 int a[1005];
 6 
 7 int comp(int n)//记忆化搜索 
 8 {
 9     int ans = 1;
10     if(a[n]>0) return a[n];
11     for(int i=1; i<=n/2; i++)
12     ans += comp(i);
13     a[n] = ans;
14     return ans;
15 }
16 
17 int main()
18 {
19     memset(a, 0, sizeof(a));
20     int n;
21     cin>>n;
22     cout<<comp(n)<<endl;
23     return 0;
24 }
View Code

 

《7》http://acm.hdu.edu.cn/showproblem.php?pid=1297(递归+高精度加法)

#include<iostream>
#include<cstdio>
using namespace std;

int main()
{
    int a[1001][101] = {0};
    a[0][1] = 1;
    a[1][1] = 1;
    a[2][1] = 2;
    a[3][1] = 4;
    for(int i=4; i<1001; i++)
        for(int j=1; j<101; j++)
        {
            a[i][j] += a[i-1][j] + a[i-2][j] + a[i-4][j];
            a[i][j+1] += a[i][j]/1000000;
            a[i][j] %= 1000000;
        }
        int n;
    while(scanf("%d", &n)!=EOF)
    {
        int k = 100;
        while(!a[n][k]) k--;
        printf("%d", a[n][k]);
        for(int i=k-1; i>0; i--)
            printf("%06d", a[n][i]);
        printf("\n");
    }
    return 0;
}
View Code

递归方程: ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,(见代码和分析过程)

分析过程:

设:F(n)表示n个人的合法队列,则:

  按照最后一个人的性别分析,他要么是男,要么是女,所以可以分两大类讨论:

1、如果n个人的合法队列的最后一个人是男,则对前面n-1个人的队列没有任何限制,他只要站在最后即可,所以,这种情况一共有F(n-1);

2、如果n个人的合法队列的最后一个人是女,则要求队列的第n-1个人务必也是女生,这就是说,限定了最后两个人必须都是女生,这又可以分两种情况:

2.1、如果队列的前n-2个人是合法的队列,则显然后面再加两个女生,也一定是合法的,这种情况有F(n-2);

2.2、但是,难点在于,即使前面n-2个人不是合法的队列,加上两个女生也有可能是合法的,当然,这种长度为n-2的不合法队列,不合法的地方必须是尾巴,就是说,这里说的长        度是n-2的不合法串的形式必须是“F(n-4)+男+女”,这种情况一共有F(n-4).

怒切此题:

当草滩小恪求出这个递推方程后, 很是得意,,, 于是他便屁颠屁颠的去切这道题去啦。  然而, wang一直陪伴着他。 仔细想一下,Fibonacci数列就已增长的很快, 而且这个数列增长的比Fibonacci数列更快。 所以第1000项会非常的大。所以不用高精度是不行的。 

 

《8》Big String

设A=“^__^”(4个字符),B=“T.T”(3个字符),然后以AB为基础,构造无限长的字符串。
l重复规则如下:

把A接在B的后面构成新的字符串C。

例如,A=“^__^”,B=“T.T”,则C=BA=“T.T^__^”。

令A=B,B=C,如上例所示,则A=“T.T”,B=“T.T^__^”。

编程任务:给出此无限长字符串中的第n个字符。
 
 
 
由于n最大可达263—1,对于输入的每个n,都去计算小于n的最大斐波纳契数,显然是非常浪费时间的。
解决的办法是预先把在263—1范围内的所有斐波纳契数求出来,放到一个数组中。
经过测算,该斐波纳契数列最多为86项,第86项的斐波纳契数约是6.02×1018,而263—1约是9.22×1018。

 

posted @ 2015-05-17 19:57  草滩小恪  阅读(296)  评论(0编辑  收藏  举报