0
点赞
收藏
分享

微信扫一扫

蓝桥杯十大常见天阶功法——水之呼吸.壹之型.递归

🔔聚齐七道典型习题,召唤递归神龙🐲

水之呼吸.壹之型.递归

💓前序

🌟一、由数据范围反推算法复杂度以及算法内容

一般算法题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在 1 0 7 ∼ 1 0 8 10^7 \sim 10^8 107108 ,大概就是一亿次运算的样子为最佳。

下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:

将这里的总结的信息类比作化学的酸碱指示剂吧,可以帮助我们明确什么能用,什么不能用。数据范围可以当做出题人给咱们的一个提示喔~

恍然大悟

🌟二、简述状态空间

一个实际问题的各种可能情况构成的集合通常称为状态空间,程序的运行可以看做对这个集合的处理,也就是对状态空间的遍历。
算法和数据结构的主要目的就是通过划分、归纳、提取、抽象来帮助提高程序遍历状态空间的效率。
递归递推就是程序遍历状态空间的两种基本方式。

🌟三、递推与递归的宏观描述

对于一个待求解问题,当它局限在某处的边界某个小范围或者某种特殊情形下时,其答案往往是已知的了。如果能够将当前这个解答的应用场景扩大到原问题的状态空间,并且拓展过程中每个步骤具有相似性,也就是着重考虑当前步骤的逻辑怎么实现,下一步的逻辑和当前这步的一样。那么此时就可以考虑递归和递推。

新的知识增加了

🌟四、递归和递推的简单应用

递归和递推的简单应用

💓递归

递归的火柴人演示🐳
在这里插入图片描述
对火柴人演示的具体流程图如下:
具体流程图

通过这张流程图可以看到,递归程序的基本单位是由缩小求解扩展组成的一种变换步骤,只是在求解时候,因为问题的"相似性,不断重复使用这样一种变换步骤,直至在已知的问题边界上直接确定答案。对于其中任意一条从原问题问题边界的变换路线,例如图中左边我用实线圈出来的部分。
为了保证每层的缩小与扩展能够衔接在同一形式的问题上,求解操作自然要保证在执行前后程序面对同一个问题的状态是相同的,也就是还原现场的必要性。

对还原现场举个栗子吧
举个栗子

我现在面前有一个双叉路口,我可以向左走,也可以向右走,我现在向右走,走呀走呀走呀,发现到达这条路的边界了,我慢慢退回去(也就是代码中的回溯),当我退回原来的双叉路口的时候,这里是空空荡荡的,和我最初进行选择的时候一模一样。我可以选择右边那条路,这个岔口没有被什么大石头堵住。

接下来就通过以下这些常见的题型具体的进入递归的世界吧

🌟例1、试题 算法提高 判断水仙花数

🌻题目描述

判断水仙数
原题传送门

🌻解题报告

判水仙花数应该是才接触C语言时候。老师布置的实验报告中的常客了吧。
茂密

🌻参考代码(C++版本)

#include <cstdio>
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>

using namespace std;
int n,res,cnt;


void f(int x)
{
	//递归:基线\边界条件+递归条件
	
	//基线条件
	if(x < 10)
	{
		res += (int)pow(x,cnt);
		return;//记得及时退出递归
	}
	
	
	//递归部分
	res += (int)pow((x%10),cnt);
	f(x /= 10);
	
}

int main()
{
	cin >> n;
	
	int t = n,r = n;
	
	//求当前这个数的位数
	while(t)
	{
		cnt ++;
		t /= 10;
	}
	
    f(r);
    
	if(res == n) cout << "YES" << endl;
	else cout << "NO" << endl;
	
    return 0;
}

🌟例2、试题 算法提高 逆向输出各位数字

🌻题目描述

逆向输出
原题传送门

🌻解题报告

这道题喃,难度不大的,只是有点考验细节。
机智

获取低位的数字可以用模运算实现。

对于递归,我想再提一下我在分享深搜dfs的时候说的一句话,叫做只考虑当前这步的逻辑,下一步的逻辑和当前的一致。

那么我们就考虑好基线条件也就是递归结束的边界,然后再编写递归条件中的逻辑。

🌻参考代码(C++版本)

	if(n == 0) return;
	printf("%d ",n%10);
	dfs(n /= 10);

🌟例3、试题 算法提高 进制转换

🌻题目描述

进制转换
原题传送门

🌻解题报告

一、模拟十进制转其他进制——除基数取余法。

用十进制转二进制作为例子,其他都是类似的。

方法:除2取余法,即每次将整数部分除以2,把余数挂在一边,商继续除以2,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,倒着读回去

十进制转二进制
即(43)D=(101011)B。

对于八进制,就更改被除数为8就好。

在这里插入图片描述
即(796)D=(31C)H。

二、实现的细节

通过上文中的模拟,可以明确,我们的核心是去实现除基数取余的操作。运用 取模运算符 % \% % 和除法运算符   / \ /  / 能够实现这步。

比较棘手的是对 A ∼ F A\sim F AF分别代替 10 ∼ 15 10\sim 15 1015的表示上,这里可以玩出很多花的🌸。

最朴实的玩法是用打表的方式,提前开一个数组存放 0 ∼ F 0\sim F 0F的每个数据。

🌻参考代码(C++版本)

#include <iostream>
#include <cstdio>
#include <cmath>
#include <vector>

using namespace std;
char d[16]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
vector<char> ans;

void f(int n,int k){
	if(n == 0 ) return;
	//取模+除
	int r;
	r=n%k;
	n=n/k;
	//记录答案
	ans.push_back(d[r]);
	//递归进去
	f(n,k);
}
int main(){

	int x,m;
	cin>>x>>m;
	f(x,m);

	for(int i =ans.size() -1 ; i >= 0;i--) cout << ans[i];
	return 0;
}

🌟 例4、递归实现指数型枚举

🌻题目描述

递归实现指数型枚举
原题传送门

🌻解题报告

一、看数据范围大致确定算法的时间/空间复杂度

二、核心——顺序

假如 n n n是3,则可以获得下面这棵递归搜索树
递归实现指数型枚举

观察递归搜素树,可以发现,对于每个位置,我们是需要去记录它的状态。比如当前位置的数字,是选择它了,还是不选择它了,亦或没有考虑到这个位置。

三、细节
恢复现场
恢复现场

🌻参考代码(C++版本)

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 20;
int n;
int st[N];//用于记录状态的数组,默认是0,也就是什么都没有放,假如是1就表示不放,假如是2表示放

void dfs(int u)
{
    //处理递归的边界情况,此时还需要将到达边界的数组中存的信息输出出来
    if(u >n)
    {
        for(int i = 1; i <= n;i++)
            if(st[i] == 2) cout << i << ' ';
        
        cout << endl;
        return;
    }
    
    st[u] = 1;
    dfs(u+1);
    
    st[u] = 2;
    dfs(u+1);
    st[u] = 0;//恢复现场
}

int main()
{
    cin >> n;
    
    //注意数据范围是1 ~ n ,所以dfs的起点是1
    dfs(1);
    
    return 0;
}

🌟例2、递归实现排列型枚举

🌻题目描述

递归实现排列型枚举
原题传送门

🌻解题报告

一、观察数据范围

记住 9 ! 9! 9! = 326880,因为要输出 n n n个方案,那么大致应该是 n ∗ n ! n *n! nn!,心里估摸着可能是一个 d f s dfs dfs

二、字典序

如果比到最后两个单词不一样长(比如, s i g h sigh sigh s i g h t sight sight),那么把 s i g h sigh sigh排在 s i g h t sight sight的前面,那么 s i g h sigh sigh的字典序就小

三、输出字典序最小的方案

四、核心——顺序

递归搜索树
五、细节
一个萝卜一个坑,依旧是要记录当前这个坑有没有放过萝卜

🌻参考代码(C++版本)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 10;
int st[N];//记录存放的数据
bool used[N];//表示当前这个数是否用过
int n;


void dfs(int u)
{
    if(u > n)
    {
    	//抵达边界,输出存储的方案
        for(int i = 1; i <= n;i++) cout << st[i] << ' ';
        
        cout << endl;
        return;
    }
    
    for(int i = 1; i <= n;i++)
    if(!used[i])
    {
        st[u] = i;
        used[i] = true;//标记这个数字i已经用过了
        dfs(u+1);
        
        st[u] = 0;
        used[i] =false;
        
    }
}
int main()
{
    
    cin >> n;
    
    dfs(1);
    
    return 0;
}

🌟例6、剑指Offer 数字排列

🌻题目描述

数字排列
原题传送门

🌻解题报告

一、数据范围定算法

观察数据范围,大概率是一个暴搜了。需要注意的是,可能包含重复数字让这道题不能直接简简单单的全排列了。

二、算法实现雏形

暴搜的核心是:确定以什么样的顺序可以不重不漏的去枚举所有状态。对于本题的重复数字,通过一些人为设置的"规定"来实现"不重复"。规定:所有相同数字的相对顺序不变。即,再次出现的相同数,只能放到已经排放好的这个树后面。

三、思路模拟
比如有一串数字, 12123 12123 12123

第一步:通过排序来统计有多少个数字,每个数字有几相同的。因为排序之后,相同的数字是相邻的。排序后:11223
第二步:确定能够不重不漏的暴搜的顺序。从前向后枚举每个数字,放置放好以后,递归处理下一个数,此时需要结合规定处理出现的相同数,继续递归处理下去。

四、细节处理

🌻参考代码(C++版本)

class Solution {
public:
    vector<bool> st;
    vector<int> path;
    vector<vector<int>> ans;

    vector<vector<int>> permutation(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        st = vector<bool>(nums.size(), false);
        path = vector<int>(nums.size());
        dfs(nums, 0, 0);
        return ans;
    }

    void dfs(vector<int>& nums, int u, int start)
    {
        if (u == nums.size())
        {
            ans.push_back(path);
            return;
        }

        for (int i = start; i < nums.size(); i ++ )
            if (!st[i])//当前这个位置没有用过
            {
                st[i] = true;
                path[i] = nums[u];
                if (u + 1 < nums.size() && nums[u + 1] != nums[u])
                    dfs(nums, u + 1, 0);
                else
                    dfs(nums, u + 1, i + 1);//u+1位置的数和u位置的数一样了
                st[i] = false;//恢复现场
            }
    }

};


🌟例7、试题 算法提高 汉诺塔

🌻题目描述

汉罗塔
原题传送门

🌻解题报告

一、背景阐述

汉洛塔
二、样例模拟

1、当只有一个盘子的时候,只需要从将A塔上的一个盘子移到C塔上。

2、当A塔上有两个盘子的时候,先将A塔上的1号盘子(编号从上到下)移动到B塔上,再将A塔上的2号盘子移动的C塔上,最后将B塔上的小盘子移动到C塔上。

3、当A塔上有3个盘子的时候,先将A塔上编号1至2的盘子(共2个)移动到B塔上(需借助C塔),然后将A塔上的3号最大的盘子移动到C塔,最后将B塔上的两个盘子借助A塔移动到C塔上。

4、当A塔上有n个盘子的时候,先将A塔上编号1至n-1的盘子(共n-1个)移动到B塔上(借助C塔),然后将A塔上最大的n号盘子移动到C塔上,最后将B塔上的n-1个盘子借助A塔移动到C塔上。

🌻参考代码(C++)

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>


using namespace std;
int n,m,cnt;

void hannoi(int n, char a, char b, char c)
{
	//递归的边界条件
	if(n == 1)
	{
		cnt ++;
		if(cnt == m)
		{
			printf("#1: %c->%c\n",a,c);
			return;
		}
		
	}else//递归部分
	{
		//按照我们的模拟,先将a杆中上面n-1个移动到b,需要借助c
		hannoi(n-1,a,c,b);
		cnt++;
		if(cnt == m) printf("#%d: %c->%c\n",n,a,c);
		//依旧是实现我们的模拟,将b杆上的盘,借助a,移动到c
		hannoi(n-1,b,a,c);
	}
}


int main()
{
	cin >> n >>m;
	hannoi(n,'A','B','C');
	cout << cnt << endl;
	
	return 0;
}

💓总结

其次是谨记,想清楚当前这步的逻辑,递归以后,在未达到边界之前,下一步的逻辑和当前的逻辑一致,不然容易陷入捋不清递归过程的的死胡同。对于排列、组合类的问题,也可以通过画递归搜索树来理解喔

进军蓝桥,冲鸭~

冲鸭

举报

相关推荐

0 条评论