递归与递推
一.递归
1.概念
在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。递归式方法可以被用于解决很多的计算机科学问题。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。
重点:重复,分解,函数,调用自身。
(PS)个人理解
通过解决一些比较复杂,循环效率广的问题
但同时要注意合理的调用次数
调用时 合理使用 int与void 两种形式
通过return的数值与边界值求解
2.例题
1)整数划分
将正整数n表示成一系列正整数之和:n=n1+n2+……+nk,其中8>=n1≥n2≥……≥nk≥1,k≥1。正整数n的这种表示称为正整数n的划分。(n<=8)
例如正整数6有如下11种不同的划分:
思路与代码
#include<bits/stdc++.h>
using namespace std;
int n,r;
int f[520],a[520];
void dfs(int k)
{
if(k==r)
{
for(int i=0;i<k;i++)
{
if(i==k-1)
{
printf("%d%c",a[i],'\n');
}
else
printf("%d%c",a[i],' ');
}
return;
}
for(int i=0;i<n;i++)
{
if(!f[i] && i+1>a[k-1])
{
f[i]=1;
a[k]=i+1;
dfs(k+1);
a[i]=0;
f[i]=0;
}
}
}
int main()
{
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
cin>>n>>r;
dfs(0);
return 0;
}
———————————————
这里明确一下题意,从n个数中选择m个
我们可以开一个数组存储数据,相应的使用记忆化搜索开一个记录状态的bool数组
当这个方程显示还没有被推过时
就可以递归
递归函数中的k表示深度
显而易见,边界值为:从0开始一直递归到m(即为选择的个数)
边界值成立后方可输出
为了避免重复的情况
在满足递归条件前记录加上一个数(sum)
表示可以扫描的数据范围级即可
注意回溯b[i]=0,a[i]=0;
2)【USACO 2020 Open】Cereal
题目描述
Farmer John 的奶牛们的早餐最爱当然是麦片了!事实上,奶牛们的胃口是如此之大,每头奶牛一顿饭可以吃掉整整一箱麦片。 最近农场收到了一份快递,内有MM种不同种类的麦片(1 ≤MM≤ 10^5)。不幸的是,每种麦片只有一箱! NN头奶牛(1 ≤ NN ≤ 10^5)中的每头都有她最爱的麦片和第二喜爱的麦片。给定一些可选的麦片,奶牛会执行如下的过程:
如果她最爱的麦片还在,取走并离开。 否则,如果她第二喜爱的麦片还在,取走并离开。 否则,她会失望地哞叫一声然后不带走一片麦片地离开。
奶牛们排队领取麦片。对于每一个0 ≤ii≤ NN−1,求如果 Farmer John 从队伍中移除前 ii 头奶牛,有多少奶牛会取走一箱麦片
输入格式 输入的第一行包含两个空格分隔的整数 N 和 M。 对于每一个 1 ≤ ii ≤ NN , 第 ii 行包含两个空格分隔的整数
fifi 和 sisi (1 ≤ fifi, sisi ≤ MM, 且 fifi ≠ sisi),为队伍中第 ii
头奶牛最喜欢和第二喜欢的麦片。输出格式 对于每一个 0 ≤ ii ≤ NN-1,输入一行,包含对于 ii 的答案 样例数据 input1 4 2 1 2 1 2 1 2
1 2 output1 2 2 2 1
数据规模与约定 1 ≤MM≤ 10^5 1 ≤ NN ≤ 10^5
思路与代码
<1>客观解法
<2>个人理解
通过老师给的解法不难总结出:
前面的牛儿选择第一喜欢吃的与不喜欢吃的后效性太大
所以顺推模拟写法是不成立的
so that 我们运用递归的思想
为什么呢?
很显然,题目中给出奶牛是喜欢两种的草的
我们就可以从后往前推剩下的奶牛喜欢的草数就行了
为什么说这种方法好用
从后往前搞牛因为i在前就可以把它的草抢走
<2>注释+代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,m,ans=0;
int a[maxn],b[maxn],f[maxn];
int cnt[maxn];
void init()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
}
void work(int x/*逆推奶牛数*/,int y/*奶牛最喜欢吃的*/)
{
//当没有奶牛喜欢时,直接取走
if(!f[y])
{
f[y]=x/*更改食物所属奶牛*/;
ans++;
return ;
}
else if (f[y]>x)
{
int temp=f[y];
f[y]=x;
if (a[temp]==y)
{
work(temp,b[temp]);
//可怜的那只奶牛去寻找第二喜爱的
}
}
}
void print()
{
for(int i=1;i<=n;i++)
{
cout<<cnt[i]<<endl;
}
}
int main()
{
freopen("cereal.in","r",stdin);
freopen("cereal.out","w",stdout);
init();
for(int i=n;i>=1;i--)
{
work(i,a[i]);
cnt[i]=ans;
}
print();
return 0;
}
二.递推
1.概念与区别
1)区别
递归:从已知问题的结果出发,用迭代表达式逐步推算出问题的开始的条件,即顺推法的逆过程,称为递归。
递推:递推算法是一种用若干步可重复运算来描述复杂问题的方法。递推是序列计算中的一种常用算法。通常是通过计算机前面的一些项来得出序列中的指定象的值。
递归与递推区别:相对于递归算法,递推算法免除了数据进出栈的过程,也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值。
2)优点
其实我们之前说递归题目的时候,我很多时候会说一句话,“递归可以形象的……”这句话不是没有道理的,因为递归其实与我们真实情况更加的想像,递归的过程更容易去模拟真实过程。
那我们为什么又要去讲解递推呢?因为在实际递归过程中,由于需要多次的调用函数,在相同的时间复杂度下,递归的速度增加很多常数,而且会使用很多栈空间,如果递归过多会发生爆栈,正如同我们在可以使用树状数组的情况下一般不喜欢使用线段树一样。
而在递推的过程中,顾名思义,大部分情况下利用循环来进行操作,可以减少一些递归的问题。
2.例题
♋♋逆序对统计
题目描述
对于一个数列{a[i]},如果有i < j且a[i] > a[j],那么我们称ai与aj为一对逆序对数。
若对于任意一个由1~n自然数组成的数列,可以很容易求出有多少个逆序对数。
那么逆序对数为k的这样自然数数列到底有多少个?
输入格式 第一行为两个整数n,k。
输出格式 一个整数,表示符合条件的数列个数,由于这个数可能很大,你只需输出该数对10000求余数后的结果。
方法与代码
<1>正确方法
先在我们已知n-1的所有内容,并需要求出n
因为之前的数列未知,我们不妨设这串数列为*******,插入的这个数为k
我们可以以此去枚举这个数字k所插入的位置(用j表示):
1.*******k
可以看出,k比每一个数字都要大,故一串星号逆序对为:f[n-1][j]
2.******k*
可以看出,k比后面的一个数字大,一串星号构成的逆序对应该是**f[n-1][j-k和后面构成的逆序对]
**,即**f[n-1][j-1]**
3*****k**
一串星号构成的逆序对为:f[n-1][j-3]
…
i-1.k*******
一串星号构成的逆序对为:f[i-1][j-(i-1)]
这样,我们就就可以得到一个状态转移方程:f[i][j]=sum(f[i-1][j-k])(0≤k≤i-1)
<2>个人思路
不难得知,在一个数列中,后面添加的数大于前面的数则无法组成逆序对
而它在哪个位置也决定了逆序对个数
<3>代码实现
#include<bits/stdc++.h>
using namespace std;
int n,m;
int f[2000][2000]={};//设f[i][j]为i个数逆序对数为j的方案总数
int main()
{
cin>>n>>m;
f[2][1]=f[2][0]=1;
for (int i=3;i<=n;i++)//枚举每一层数字
{
for (int j=0;j<=min(m,i*(i-1)/2);j++)//构成逆序对一共需要两个数,可根据n选2公式得数最多会产生i*(i-1)对逆序对
{
for (int k=0;k<=i-1&&k<=j;k++)//k枚举第i个数距离这个数的末尾所处的数列的位置或相当于原来的数列中前插的位置:
f[i][j]=(f[i][j]+f[i-1][j-k])%10000;
}
}
cout<<f[n][m];
return 0;
}
————————————————
三.总结
递归递推各有所优
递归在于更加形象,同时面对少数题目具有一些局限性
但是重在好想
而递归在于巧妙
大大节省了代码运行时间与准确性
但对于数学思想有客观的需求
但这二者基本算法在于基础
在此之上升级为一些高级的什么动态规划,线段树之类的高端
所以重于解决前期题目
不过后期我也不会