递归

1. 引子:汉诺塔

1.1 汉诺塔题目

古代某寺庙中有一个梵塔,塔内有3个座A、B和C,座A上放着64个大小不等的盘,其中大盘在下,小盘在上。有一个和尚想把这64 个盘从座A搬到座B,但一次只能搬一个盘,搬动的盘只允许放在其他两个座上,且大盘不能压在小盘上。现要求用程序模拟该过程,输入一个正整数n,代表盘子的个数,编写函数 void hanoi(int n,char a,char b,char c)

其中,n为盘子个数,从a座到b座,c座作为中间过渡,该函数的功能是输出搬盘子的路径。

输入格式:输入在一行中给出1个正整数n。

输出格式:输出搬动盘子路径。

#include<stdio.h>
void hanoi(int n, char a, char b, char c);
int main()
{
	char a = 'a', b = 'b', c = 'c';
	int n;
	scanf("%d", &n);
	hanoi(n, a, b, c);
}

void hanoi(int n, char a, char b, char c)
{
	if (n == 1)
	{
		printf("%c-->%c", a, b);
	}
	else
	{
		hanoi(n - 1, a, c, b);
		printf("\n%c-->%c\n", a, b);
		hanoi(n - 1, c, b, a);
	}
}

 

2. 思考

汉诺塔的问题看了答案才知道怎么做,到底应该怎么思考,解题逻辑是什么?

关键点1:

如何把64个盘子简化成63个盘子。

 

(太难的题目,不应思考如何从1到2到3,而是明白此时n和n-1本质是一样的,不是企图从123的规律里得到答案,而是通过整体思想将n与(1到n-1)剥离,将n视为与n-1无差别,而后n→n-1→ …… →3→2→出口)

关键点2:

思考通式。

 

式1:n-1号盘子从a到c

式2:n号盘子a到b

式3:n-1号盘子c到b

关键点三:

出口/边界

 

 

3.发展:关于递归

3.1递归定义:

递归算法:一种直接或者间接调用自身函数或者方法的算法。

3.2 简单的举例(Fibonacci数列):

本题要求编写程序,利用数组计算菲波那契(Fibonacci)数列的前N项,每行输出5个,题目保证计算结果在长整型范围内。Fibonacci数列就是满足任一项数字是前两项的和(最开始两项均定义为1)的数列,例如::1,1,2,3,5,8,13,...。

输入格式:

输入在一行中给出一个整数N(1≤N≤46)。

输出格式:

输出前N个Fibonacci数,每个数占11位,每行输出5个。如果最后一行输出的个数不到5个,也需要换行。

如果输入的N不在有效范围内,则输出"Invalid."。

 

普通方法(循环):

#include<stdio.h>
int main()
{
    int i,n;
    scanf("%d", &n);
    if (n < 1 || n>46)
    {
        printf("Invalid.");
    }
    else
    {
        int a = 1, b = 1;
        int t;
        for (i = 1; i <= n; i++)
        {
            if (i == 1)
            {
                printf("%11d", 1);
            }
            else
            {
                printf("%11d", b);
                t = a;
                a = b;
                b = t + b;
                if (i % 5 == 0)
                {
                    printf("\n");
                }
            }
        }
        if (n % 5 != 0)
        {
            printf("\n");
        }
    }
}

 

 

递归方法:

#include<stdio.h>
int fibonacci(int n);

int main()
{
    int i, n;
    scanf("%d", &n);
    if (n < 1 || n>46)
    {
        printf("Invalid.");
    }
    else
    {
        for (i = 1; i <= n; i++)
        {
            printf("%11d", fibonacci(i));
            if (i % 5 == 0)
            {
                printf("\n");
            }
        }
        if (n % 5 != 0)
        {
            printf("\n");
        }
    }
}

int fibonacci(int n)
{
    if (n == 1 || n == 2)
    {
        return 1;
    }
    else
    {
        return  fibonacci(n - 1) + fibonacci(n - 2);
    }
}

 

 

递归图解:

2.3 递归名声一直不好,why?

1. 时间和空间的消耗大。

每一个线程都会有一个私有的栈。每一次方法的调用都涉及到一个桢栈的入栈到出栈。同时,会涉及到分配内存空间,保存参数,返回地址和临时变量,而且往栈里压入数据和弹出都需要时间。

2. 递归会出现重复计算。

递归的本质是把一个问题分解为多个问题,如果多个问题存在重复计算,有时候这个情况会随着n成指数增长。比如斐波那契的递归就是一个例子。

比如上面递归:当n=50,时间为244.449s

3. 栈溢出的问题。

 

 

4.高潮:改进方法

 

4.1 动态递归:

动态递归:

将原问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态递归会将每个求解过的子问题的解记录下来,这样当下一次遇到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算。

根据动态规划的特点,可以一解递归燃眉之急。

 

测试:n=50      用时:0.325s

#include<stdio.h>
#define ll long long
ll fibonacci(ll n);
ll str[100] = { 0 };
 
ll main()
{
    ll i;
    ll n;
    scanf("%lld", &n);
    printf("%lld", fibonacci(n));
}
 
ll fibonacci(ll n)
{
    str[1] = 1; str[2] = 1;
    if (n == 1 || n == 2)
    {
        return 1;
    }
    if (str[n] == 0)
    {
        str[n] = fibonacci(n - 1) + fibonacci(n - 2);
        return str[n];
    }
    else
    {
        return str[n];
    }
}

 

 

4.2 尾递归:

即函数调用出现在调用者函数的尾部, 所以根本没有必要去保存任何局部变量,直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。精髓:尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数。

好处:函数的堆栈耗用难以估量,尾递归省去了保存中间函数堆栈的麻烦。

 

测试:n=50      用时:1.647

#include<stdio.h>
#define ll long long
ll fibonacci(ll n, ll a, ll b);
ll a = 1, b = 1;
int main()
{
    ll n;
    scanf("%lld", &n);
    printf("%lld", fibonacci(n, a, b));
}

ll fibonacci(ll n, ll a, ll b)
{
    if (n == 1)
    {
        return a;
    }
    else
    {
        return fibonacci(n - 1, b, a + b);
    }
}

 

尾递归图解:

Fibonacci ( n, a, b );

初始值:n=5,a=1,b=1

   

Fibonacci5,1,1

=

Fibonacci4,1,2

     

Fibonacci4,1,2

=

Fibonacci3,2,3

     

Fibonacci3,2,3

=

Fibonacci2,3,5

     

Fibonacci2,3,5

=

Fibonacci1,5,8

     

Fibonacci1,5,8

result=5

 

5.拓展:广度优先搜索

广度优先搜索本质也是迭代

5.1 步骤

  1. 输入数据
  2. dfs(根节点,带有根节点的v数组),定义result数组
  3. 在根节点的孩子节点中挑选a,dfs(a,带有根节点与子节点的v数组)
  4. 每次将v数组与result进行对比,如果v更长,则将result替换为当前v
  5. 当当前节点无孩子节点,则这一条搜索完毕。
  6. 当所有的路都搜索完毕,result数组则为所求。

 

5.2 例子(病毒溯源)

病毒容易发生变异。某种病毒可以通过突变产生若干变异的毒株,而这些变异的病毒又可能被诱发突变产生第二代变异,如此继续不断变化。

现给定一些病毒之间的变异关系,要求你找出其中最长的一条变异链。

在此假设给出的变异都是由突变引起的,不考虑复杂的基因重组变异问题 —— 即每一种病毒都是由唯一的一种病毒突变而来,并且不存在循环变异的情况。

输入格式:

输入在第一行中给出一个正整数 N(≤10^4),即病毒种类的总数。于是我们将所有病毒从 0 到 N1 进行编号。

随后 N 行,每行按以下格式描述一种病毒的变异情况:

k 变异株1 …… 变异株k

其中 k 是该病毒产生的变异毒株的种类数,后面跟着每种变异株的编号。第 i 行对应编号为 i 的病毒(0≤i<N)。题目保证病毒源头有且仅有一个。

输出格式:

首先输出从源头开始最长变异链的长度。

在第二行中输出从源头开始最长的一条变异链,编号间以 1 个空格分隔,行首尾不得有多余空格。如果最长链不唯一,则输出最小序列。

注:我们称序列 { a1,,an } 比序列 { b1,,bn } "小",如果存在 1≤k≤n 满足 ai=bi 对所有 i<k 成立,且 ak<bk

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int>v[10001];
vector<int>result;
void Dfs(int now, vector<int>& p);
int main() {
    int n;
    int i, j, temp;
    int root[10001] = { 0 };
    cin >> n;
    for (i = 0; i < n; i++) {
        int m;
        cin >> m;
        for (j = 0; j < m; j++)
        {
            cin >> temp;
            root[temp] = 1;
            v[i].push_back(temp);
        }
        if (m)
        {
            sort(v[i].begin(), v[i].end());
        }
    }
    for (i = 0; i < n; i++)
    {
        if (!root[i])
        {
            break;
        }
    }
    vector<int> p;
    p.push_back(i);
    Dfs(i, p);
    cout << result.size() << endl;
    for (int i = 0; i < result.size(); i++) {
        if (i) cout << " ";
        cout << result[i];
    }
}

void Dfs(int now, vector<int>& p)
{
    if (p.size() > result.size())
    {
        result.clear();
        result = p;
    }
    for (int i = 0; i < v[now].size(); i++) {
        p.push_back(v[now][i]);
        Dfs(v[now][i], p);
        p.pop_back();
    }
}

 

 

5.3 例题解析:

  • 用两个循环输入数据到v中
  • 因为题目要求"如果最长链不唯一,则输出最小序列",所以每次内循环完毕排个序
  • root数组是为了寻根,数据中没有出现过的数据即为根节点,根节点推入p
  • dfs(根节点,p)
  • dfs中:        
    • 每次将v与result进行对比,若v长,则将result替换为v。        
    • 将当前节点替换为当前节点的孩子节点a,将a推入p,并进行递归dfs(节点a,p)
  • 输入result数组

 

 

 

参考:
https://blog.csdn.net/weixin_45962741/article/details/116227551
https://blog.csdn.net/bolite/article/details/117405368

posted @ 2021-10-23 22:40  fw_48925  阅读(75)  评论(1)    收藏  举报