导读

信息学能够有助于孩子未来工作发展,提升孩子的综合能力。


之前,我们讲过了递归的基本概念,简单了解了递归,但是递归在竞赛中考的并没有那么简单,我们今天一起来看看信息学初赛中,比较难的递归算法。掌握相关的做题方法,为接下来的信息学竞赛打下基础!


1 梦中梦

不知道大家有没有这样的经历,在自己做梦的时候,梦到了自己做梦,然后做的梦也是自己做梦……


这是一种很神奇的经历,和我们之前讲过的递归算法很像。

2 递归算法回顾

在我们讲函数的时候,我们讲了一种情况,函数调用自己!我们把函数调用自己的这种函数叫做递归

1 什么是递归算法

递归算法是自己调用自己的算法,通过函数来实现。


使用递归算法的场景要满足几条非常重要的特点,我们按照它的名字给它分开:


可递
可归


所谓的可递就是能够递去。也就是能实现自己调用自己,这样就要满足子问题要和父问题是同类型问题。


所谓的可归就是能够归来。也就是到达某个特定情况,子问题能够执行结束,然后退回到上层问题。

2 递归与栈

递归的过程会自动生成栈结构。递归有递去和归来两部分,递去的部分就是入栈的部分,归来的部分就是出栈的部分。


所以,递归的过程也可以使用循环和栈结构实现。但是递归的写法会更简单。


例如我们求1到n所有整数的和:


int sum(int n) {
if(n==1) return 1;
return n+sum(n-1);
}


3 竞赛中的递归

在竞赛中,经常会用到递归。


在初赛的过程中,选择题中可能会出现递归的相关知识。程序分析题一般都会有一道递归题目。一般都是第三题。这道题目的特点就是,流程不难,但是需要认认真真去分析。


在复赛过程中,有些算法会用递归实现,如深度优先搜索算法。所以我们要自己多多做相关的练习。

3 信息学初赛中的递归详解

我们重点来看一下信息学初赛中的递归吧!


首先我们先说一下初赛中的递归的做题方法。一般来说,程序分析题目有四道,并且有一道是用到递归算法的。而且很多情况下,这道题目是初赛中最耗费精力的一道题目。


这道题不一定是最难的,因为它的逻辑非常简单。但是这道题目需要多次递归,在每一次递归的时候,都容易分析出错


所以,一般来说,这道题目,都是放在最后做当初赛的其他题目,已经没有问题了(会做的,保证对的,已经检查完毕,没问题了。不会做的或者模棱两可的,也得到了最终确定的答案)。最后的时间,不要考虑其他任何事情,专心到这道题目中,按照递归的过程,一步一步认真分析。我们以下面的题目为例。

1 【NOIP2014 普及组】3-2

阅读程序写结果


#include<iostream>
using namespace std;
int fun(n) {
if (n == 1) return 1;
if (n == 2) return 2;
return fun(n - 2) - fun(n - 1);
}
int main() {
int n;
cin >> n ;
cout << fun(n) << endl;
return 0;
}


输入为7,输出为:______________。


【分析】


我们找到演算纸。在演算纸上去一步一步分析,做一步操作,我们就写一步操作结果。


如果递去,那我们就增加一个缩进,提醒自己,这是子操作的过程。


例如本题,当n为7时,带入到递归函数中。


//f(7),7不是1也不是2,所以f(7) = f(5)-f(6)
f(7) { //f(5) - f(6)
f(5)

f(6)

}


对于f(5)和f(6):


f(7) { //f(5) - f(6)
f(5) { //f(3) - f(4)
f(3)

f(4)
}

f(6) { //f(4) - f(5)
f(4)

f(5)
}

}


对于f(3)、f(4)和f(5):


f(7) { //f(5) - f(6)
f(5) { //f(3) - f(4)
f(3) { //f(1) - f(2) = -1
f(1) = 1
f(2) = 2
}
f(4) { //f(2) - f(3) = 3
f(2) = 2
f(3) = -1
}
}

f(6) { //f(4) - f(5)
f(4) = 3

f(5) { //f(3) - f(4) = -4
f(3) = -1
f(4) = 3
}
}
}


这个时候,我们发现,我们可以从上往下依次求出f(3)、f(4)和f(5),然后我们就可以求出f(5)和f(6),最后求出f(7):


f(7) { //f(5) - f(6) = -11
f(5) { //f(3) - f(4) = -4
f(3) { //f(1) - f(2) = -1
f(1) = 1
f(2) = 2
}
f(4) { //f(2) - f(3) = 3
f(2) = 2
f(3) = -1
}
}

f(6) { //f(4) - f(5) = 7
f(4) = 3

f(5) { //f(3) - f(4) = -4
f(3) = -1
f(4) = 3
}
}
}


所以我们就得到了-11。


上面这种方法,是最基本的方法,也是最通用的方法。只要是递归题目,我们都可以这样分析,虽然这样分析速度很慢,但是只要认真就不会错。


除此之外,我们还可以分析代码的功能,了解代码含义,然后自己根据含义去做。


这道题目,其实也可以看作是递推。我们给出第一项和第二项的值。后面的每一项都是前两项前项减去后项的差。(斐波那契数列是两项的和)


也就是说我们可以把上面的代码写出如下的递推公式:




a[1] = 1;
a[2] = 2;
a[n] = a[n-2] - a[n-1];


这样我们就能得出任意项。当n=7时为第7项。


a[1] = 1;
a[2] = 2;
a[3] = -1;
a[4] = 3;
a[5] = -4;
a[6] = 7;
a[7] = -11;


这种先分析代码功能,后直接根据功能带入输入推导输出的方法适合程序分析题另外的一道题目。一般程序分析题最简单的题目就是模拟得到结果。第二简单的,就是我们分析代码功能,根据代码功能推导结果

2 【NOIP2016 提高组】4-3

阅读程序写结果


#include<iostream>
using namespace std;
int lps(string seq, int i, int j) {
int len1, len2;
if (i == j) return 1;
if (i > j) return 0;
if (seq[i] == seq[j])
return lps(seq, i + 1, j - 1) + 2;
len1 = lps(seq, i, j - 1);
len2 = lps(seq, i + 1, j);
if (len1 > len2) return len1;
return len2;
}
int main()
{
string seq = "acmerandacm";
int n = seq.size();
cout << lps(seq, 0, n - 1) << endl;
return 0;
}


输出为:______________。


【分析】


1、做法1


我们还是按照上面的第一种方法做。认真分析。


首先,我们先看字符串,一共有11个字母,所以n=11,n-1就是10,索引为0和10的字母分别是a和m。所以我们进入递归函数是如下,:


012345678910
acmerandacm


lps(0,10) am


然后进入到函数后,前面的判断都不满足,我们可以得到len1和len2,然后我们对len1和len2分别递归,还是按照上面的写法:


012345678910
acmerandacm


lps(0,10) am
len1 = lps(0,9) ac

len2 = lps(1,10) cm


按照上面的做法一直写,我们就得到下面的内容:


012345678910
acmerandacm


lps(0,10) am
len1 = lps(0,9) ac
len1 = lps(0,8) aa = lps(1,8)+2
len1 = lps(1,7) cd
len1 = lps(1,6) cn
len1 = lps(1,5) ca
len1 = lps(1,4) cr
len1 = lps(1,3) ce
len1 = lps(1,2) cm 1
len1 = lps(1,1) 1
len2 = lps(2,2) 1
len2 = lps(2,3) me
len2 = lps(2,4) mr
len2 = lps(2,5) ma
len2 = lps(2,6) mn
len2 = lps(2,7) md
len2 = lps(2,8) ma
len2 = lps(1,9) cc = lps(2,8)+2
len2 = lps(1,10) cm


这个时候我们就可以得到一个结论了,如果i+1=j,并且由于两个位置上的字母一定不会相同,所以我们有下面的结论1:


lps(i,i+1) = 1


递归回到上一层,因为相隔一个字母的两个字母不同,所以:


lps(i,i+2) = max(lps(i,i+1), lps(i+1,i+2))
= max(1, 1) = 1


这个就是我们得到的结论2。


再递归到上一层,中间相隔两个字母的两个字母如果也不相同,那么有:


lps(i,i+3) = 1


如果相同,那么有:


lps(i,i+3) = lps(i+1,i+2) + 2 = 1 + 2 = 3


我们将这个总结为结论3:


如果seq[i] == seq[i+3],那么:lps(i,i+3) = 3
如果seq[i] != seq[i+3],那么:lps(i,i+3) = 1


所以剩下的部分就很好写了,我们先递去到结论3相关的层,即lps(i,i+3)层:


lps(0,10) am
len1 = lps(0,9) ac
len1 = lps(0,8) aa = lps(1,7)+2 = 3
len1 = lps(1,6) cn = 1
len1 = lps(1,5) ca 1
len1 = lps(1,4) cr 1
len2 = lps(2,5) ma 1
len2 = lps(2,6) mn 1
len1 = lps(2,5) cr 1
len2 = lps(3,6) en 1
len2 = lps(2,7) md 1
len1 = lps(2,6) mn 1
len2 = lps(3,7) ed 1
len1 = lps(3,6) en 1
len2 = lps(4,7) rd 1
len2 = lps(1,9) cc = lps(2,8)+2 = 5
len1 = lps(2,7) md 1
len2 = lps(3,8) md
len1 = lps(3,7) ed 1
len2 = lps(4,8) ra 3
len1 = lps(4,7) rd 1
len2 = lps(5,8) aa = lps(6,7)+2 = 3
len2 = lps(1,10) cm


到这里,我们又得到了一个加2,那这个时候,对应的上一层是,直到:


lps(0,10) am
len1 = lps(0,9) ac
len2 = lps(1,9) cc = lps(2,8)+2 = 5


然后我们继续往下:


012345678910
acmerandacm


lps(0,10) am
len1 = lps(0,9) ac = 5
len1 = lps(0,8) aa = lps(1,7)+2 = 3
len2 = lps(1,9) cc = lps(2,8)+2 = 5
len2 = lps(1,10) cm = 5
len1 = lps(1,9) cc = lps(2,8)+2 = 5
len2 = lps(2,10) mm = lps(3,9)+2
len1 = lps(3,8) md 1
len2 = lps(4,9) rc
len1 = lps(4,8) ra 3
len2 = lps(5,9) ac 3
len1 = lps(5,8) aa 3
len2 = lps(6,9) nc 1


所以结果为:5。


2、做法2


有了上面的分析,我们这个时候可以想一下这道题目让我们做什么?


我们看下它的操作:


if (i == j)  return 1;
if (i > j) return 0;


如果字符串到达了同一个索引,则长度记为1,如果左边的索引超过了右边的索引,长度记为0。如果左边索引在右边索引的左边,那么就正常做操作:




if (seq[i] == seq[j]) 
return lps(seq, i + 1, j - 1) + 2;


对于一个字符串,如果有两个字母相同,那么我们就去掉这两个字母,得到它的子串,然后字符串的长度为子串长度+2。


len1 = lps(seq, i, j - 1);
len2 = lps(seq, i + 1, j);
if (len1 > len2) return len1;
return len2;


如果两个字母不同,则该字符串的长度是去掉头或尾的字符串的长度的最大值。


所以,对于下面的字符串:


acmerandacm


长度和下面的子串的长度的最大值一样:


acmeranda
cmerandac
merandacm


上面这三个子串,都是头尾一样,这三个子串的长度均为去掉头尾的子串长度+2,即:


cmerand + 2
meranda + 2
erandac + 2


对于第一个串,所有字母都不同了,最终一定返回到i=j,即返回长度为1。所以上面的子串的长度是3。


对于第二个串,有相同字母,即:


meranda + 2 = anda + 2 = nd + 4


nd字母不同,返回1,所以最终返回5。


对于第三个串,得到:


erandac + 2 = anda + 2 = nd + 4


所以我们发现,他这个其实是做了一个计数操作,记录到最后的5是


1、有两个对,并且一个对包含另一个对
2、最里层的对中还包含元素,不管有几个,只记录一个。


所以这个程序的作用是,找到这个字符串的最大的对称子串(从前往后读取和从后往前读取是一致的,也是关于中间对称的)


3 总结

上面的两道题目,我们都是采用了两种方法,我给他们分别取如下两个名字:


1、带入分析法:将结果带入一步一步分析,直到得到最终结果。
2、功能分析法:分析递归能实现的功能,直接使用功能分析输入,得到最终结果。


除此之外还有第三种方法,前两种方法是通用方法,第三种方法只针对特殊题目,就是我们通过递归函数的函数名猜测递归算法的功能。但是这种方法是可遇而不可求的!


对于大多数孩子而言,第二种方法也不是很适用。很多递归题目是很难轻松分析出算法功能的。


但是第一种方法,只有一个要求,就是认真。一般来说,熟练掌握第一种方法,基本上在15-30分钟左右时间就能认真推导出正确结果。在推导过程中,我们也要不断总结一些小结论,辅助我们减少分析的情况。更快地得到最终结果。


做这道题目,一定要心无旁骛,所以,老师的建议是,先把其他题目做出来,保证没有问题之后,全心放在这道题目中。不要考虑其他的题目。认认真真分析,一定能够做出来的!

4 作业

本节课的作业,就是复习上面的所有知识,并完成下面的题目!

1 阅读程序写结果

阅读程序写结果


#include <iostream>
using namespace std;
int g(int m, int n, int x) {
int ans = 0;
int i;
if (n == 1) return 1;
for (i = x; i <= m / n; i++)
ans += g(m - i, n - 1, i);
return ans;
}
int main() {
int t, m, n;
cin >> m >> n;
cout << g(m, n, 0) << endl;
return 0;
}


输入为:7    3,输出为:______________。




AI与区块链技术

信息学赛培 | 11 必须掌握!初赛必备的递归算法答题方法详解_c++

长按二维码关注