递归——其实大家都肯定有一定了解,递归通俗了说就是循环最严厉的父亲,它将大量可能进行的重复的循环整合在一起,让整个代码显得简洁,当然,仅仅是简洁而已,不论是对人还是对计算机,递归的解读大部分都会比非递归困难,尤其当循环体执行操作较多时,瞬间就成了两边的“噩梦”。如果不通俗了说,递归本身就是两个关联的部分:“递”——对大问题的横向分解,产出一系列类似的任务片段;“归”——下一步的操作与之前的某几步密切相关,因而继续执行需要回归先前已完成的任务片段,最终将结果汇总。
我们先从较简单的题目看起:

取自蓝桥云课—18165
扫一眼题目,相信大家第一时间就能写完答案:
#include <iostream>
using namespace std;
int cal(int x){
if(x<=10){
return x*(x-1);
}
return 2*x*cal(x-6);
}
int main()
{
int n;
cin>>n;
cout<<cal(n)%998244353;
return 0;
}
细心的同学肯定也会第一时间就发现了我的问题:如此平庸的代码如何应对这样的递归形式乘法计算?——是的,只需要当x=40,结果这个不知道多大的数早就碾压了我们int的极限2147483467,而评测数据n(x)的上界更是达到了100000,哪怕是long型都不一定撑得住,保险地我们需要使用long long型处理。此外,在最终调用cal(n)后再对那个数取模也显然是不对的,因为结果的大数全交给了long long型抗,而取模操作只在一旁挂机!所以我们应当在递归体中进行取模操作,分摊longlong的压力。代码如下:
点击查看代码
#include <iostream>
using namespace std;
long long cal(int x){
if(x<=10){
return (x*(x-1))%998244353;
}
return (2*x*cal(x-6))%998244353;
}
int main()
{
int n;
cin>>n;
cout<<cal(n);
return 0;
}
我们汲取经验,大部分递归的题目都需要考虑大数的处理,如果更改数据类型都无法处理大数,那么就需要考虑自定义划分大数结构或是字符串模拟计算了(此处埋下伏笔,我们模拟章见)。此外,递归题目常常紧密关系到数据结构的操作,递归体内的操作稍加复杂就可能导致执行效率指数级下降,代码简洁了,计算机倒是跑死了,,,而处理递归的时间复杂度,我们又通常会用到动态规划,即存储先前的操作,进一步避免重复计算,我们先就刚刚的题目举个例子。
首先使用dp数组用于暂存已经得到的结果,对于10以内的可直接计算到位,大于10后根据题目公式写出状态转移方程即可:
#include <vector>
using namespace std;
long long cal(int x){
vector<long long>dp(x+1);//暂存数组
for (int i=0;i<=10;++i){
dp[i]=(i*(i-1))%998244353;
}
for(int i=11;i<=x;++i){
dp[i]=(2*i*dp[i-6])%998244353;
}
return dp[x];
}
int main()
{
int n;
cin>>n;
cout<<cal(n);
return 0;
}
就此题来说,其实差别很小,只是多用了个dp数组,且此题也大概率不会因为递归导致超时,动态的规划的作用不是很大,但你可千万不要小瞧了它的神力。......to be continued
你是想找较难的题目吗https://leetcode.cn/problems/regular-expression-matching/?envType=problem-list-v2&envId=recursion&
不用谢