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

友友们好(^-^)🌹🌹🌹,我是杨枝,一枚在算法领域迈步的呆萌的博主呀~
目前还是一只纯纯的菜汪🐶。 典型的又菜又爱闹那种👀,做不好很多事,说不好很多话,写题还总不Ac😅,还在努力还在前进👣。
因为了,你们对我来说都是是独一无二的呀💓。在点开这篇文章的那一刻,我相信,我们之间相互需要彼此啦🌹🌹
时刻谨记:认真写算法,用心去分享。不负算法,不误卿。 感谢相遇(^㉨^)

水之呼吸.壹之型.递归

💓前序

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

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

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

接下的内容是一个很宽泛但是也很实用的总结,不用刻意的背诵,很难暴力的记住,建议收藏,假如感觉忘记了,就翻开看一下,更加符合人类的记忆遗忘曲线,用这种方式进行迭代记忆会更轻松 ^ - ^

1. n ≤ 30 n≤30 n30, 指数级别, dfs+剪枝,状态压缩dp
2. n ≤ 100 n≤100 n100 => O ( n 3 ) O(n^3) O(n3),floyd,dp,高斯消元
3. n ≤ 1000 n≤1000 n1000 => O ( n 2 ) O(n^2) O(n2) O ( n 2 l o g n ) O(n^2logn) O(n2logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
4. n ≤ 10000 n≤10000 n10000 => O ( n ∗ n ) O(n∗\sqrt n) O(nn ),块状链表、分块、莫队
5. n ≤ 100000 n≤100000 n100000 => O ( n l o g n ) O(nlogn) O(nlogn) => 各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、Kruskal、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树
6. n ≤ 1000000 n≤1000000 n1000000 => O ( n ) O(n) O(n), 以及常数较小的 O ( n l o g n ) O(nlogn) O(nlogn) 算法 => 单调队列、 hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O ( n l o g n ) O(nlogn) O(nlogn)的做法:sort、树状数组、heap、dijkstra、spfa
7. n ≤ 10000000 n≤10000000 n10000000 => O ( n ) O(n) O(n),双指针扫描、kmp、AC自动机、线性筛素数
8. n ≤ 1 0 9 n \le 10^9 n109 => O ( n ) O(\sqrt n) O(n ),判断质数
9. n ≤ 1 0 18 n \le 10^{18} n1018 => O ( l o g n ) O(logn) O(logn),最大公约数,快速幂,数位DP
10. n ≤ 1 0 1000 n \le 10^{1000} n101000 => O ( ( l o g n ) 2 ) O((logn)^2) O((logn)2),高精度加减乘除
11. n ≤ 1 0 100000 n \le 10^{100000} n10100000 => O ( l o g k × l o g l o g k ) O(logk×loglogk) O(logk×loglogk),k表示位数,高精度加减、FFT/NTT

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

恍然大悟

🌟二、简述状态空间

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

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

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

用已知的问题边界作为起点,向原问题正向推导的拓展方式是递推。

推导的路线难以确定,这时以原问题作为起点,尝试寻找把每个状态空间缩小到已知的问题边界的路线,再通过该路线反向回溯的遍历方式是递归

新的知识增加了

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

使用递归和递推要求把原问题与问题边界之间的每个变换步骤具有相似性,这样我们才能够设计一段程序实现这个步骤,将其重复作用于问题之中。

换句话说,程序在每个步骤上应该面对相同种类的问题,这些问题都是原问题的一个子问题,可能仅在规模或者某些限制条件上有所区别,因为是子问题,那么,也就理所应当能够用求解原问题的程序进行求解

递归和递推的简单应用

💓递归

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

通过这张流程图可以看到,递归程序的基本单位是由缩小求解扩展组成的一种变换步骤,只是在求解时候,因为问题的"相似性,不断重复使用这样一种变换步骤,直至在已知的问题边界上直接确定答案。对于其中任意一条从原问题问题边界的变换路线,例如图中左边我用实线圈出来的部分。

横向来看,它的每一层是一次递归程序体的执行;

纵向来看,它的左右两边分别是寻找路线和沿这路线进行推导的流程。

为了保证每层的缩小与扩展能够衔接在同一形式的问题上,求解操作自然要保证在执行前后程序面对同一个问题的状态是相同的,也就是还原现场的必要性。

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

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

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

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

🌻题目描述

判断水仙数
原题传送门

🌻解题报告

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

解法很多,可以直接循环,也可以递归。循环的效率是要更高的,hh~。这里就提供递归的解法吧。让大家对每个递归函数都会有的基线条件和递归条件掌握得更清楚

🌻参考代码(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、试题 算法提高 逆向输出各位数字

🌻题目描述

逆向输出
原题传送门

🌻解题报告

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

题目说意思其实是让我们补全一个递归函数,可以实现从低位向高位输出。假如直接写全部代码放oj上是会 Compile Error 的

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

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

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

🌻参考代码(C++版本)
	if(n == 0) return;
	printf("%d ",n%10);
	dfs(n /= 10);

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

🌻题目描述

进制转换

原题传送门

🌻解题报告

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

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

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

十进制转二进制

即(43)D=(101011)B。

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

需要注意的是十六进制,被除数需要被修改为16,同时,倘若余数是大于等于10 ,此时就需要使用字母代替。 A ∼ F A\sim F AF分别代替 10 ∼ 15 10\sim 15 1015

在这里插入图片描述

即(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、递归实现指数型枚举

🌻题目描述

递归实现指数型枚举

原题传送门

🌻解题报告

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

记住常见的部分特殊值:
2 0 2^0 20 ~ 2 10 2^{10} 210分别是1,2,,4,8,,,1024。
2 20 2^{20} 220大致是100w。
2 16 2^{16} 216 = 65526。
2 15 2^{15} 215 = 32768
2 63 2^{63} 263大致是 1 0 18 10^{18} 1018

看这道题的数据范围,大致可以确定是指数型的时间复杂度,也就是 2 n 2^n 2n或者 n ∗ 2 n n * 2^n n2n。就是最糟糕的时候,大致就是15 *
2 15 2^{15} 215 = 15 * 32768 ,差不多就是45万次运算的样子。

二、核心——顺序

递归或者说是dfs的时候,最重要的是顺序,需要定一个顺序,可以把所有方案不重复不疏漏的找出来。对于这道题而言,就可用从1~ n n n依次考虑每个数,选或者不选。

假如 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;
}

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

🌻题目描述

递归实现排列型枚举

原题传送门

🌻解题报告

一、观察数据范围

数据范围
1≤ n n n≤9

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

二、字典序

在英文字典中,排列单词的顺序是先按照第一个字母以升序排列(即 a 、 b 、 c … … z a、b、c……z abc……z 的顺序);如果第一个字母一样,那么比较第二个、第三个乃至后面的字母,字母大的那个就是字典序大。

如果比到最后两个单词不一样长(比如, 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
第二步:确定能够不重不漏的暴搜的顺序。从前向后枚举每个数字,放置放好以后,递归处理下一个数,此时需要结合规定处理出现的相同数,继续递归处理下去。

四、细节处理

对于相同数,我们人为定序,就可以避免重复计算:我们在 d f s dfs dfs时记录一个额外的状态,记录上一个相同数存放的位置 s t a r t start start,我们在枚举当前数时,只枚举 s t a r t + 1 start+1 start+1, s t a r t + 2 start+2 start+2,…这些位置。

🌻参考代码(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、试题 算法提高 汉诺塔

🌻题目描述

汉罗塔

原题传送门

🌻解题报告

一、背景阐述

汉诺塔问题:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上。有一个和尚想把这64个盘子从A座移到B座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上。如下图

汉洛塔
二、样例模拟

谨记递归的核心是先确定好边界来跳出递归,然后对于递归部分,就是当前这步的逻辑和递归进去以后下一步的逻辑一样的。

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;
}

💓总结

对于递归的算法题,首先要明确递归的两大组成是基线条件+递归条件。分别决定了递归什么时候退出,以及递归函数中如何完成题目要求的逻辑。

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

在这里插入图片描述

进军蓝桥,冲鸭~

冲鸭

评论 93
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨枝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值