递归与递推
特点
递归(recursive)
运行过程中自我调用,求解过程分为回溯和递推两个过程,占用内存多(栈数先积累后收缩,有可能爆栈),代码简洁但低效。
尾递归和递归区别⬇
function story() {
从前有座山,山上有座庙,庙里有个老和尚,一天老和尚对小和尚讲故事:story() // 尾递归,进入下一个函数不再需要上一个函数的环境了,得出结果以后直接返回。
}
function story() {
从前有座山,山上有座庙,庙里有个老和尚,一天老和尚对小和尚讲故事:story(),小和尚听了,找了块豆腐撞死了 // 非尾递归,下一个函数结束以后此函数还有后续,所以必须保存本身的环境以供处理返回值。
}
尾递归省内存、高效(相当于迭代(或者说递推?))
Python无法在语言中实现尾递归优化(Tail Call Optimization, TCO),所以采用了for, while等特殊结构代替recursive的表述(优化是编译器干的,发现尾递归结构就给优化了)。
递推(iterative)
效率比递归高,尽量递推,除非只能递归。
例题
递推例子
一般都是数学题。。重点是找递推公式(也太好偷懒了吧)
平面分割问题
直线分割平面(基本结论)
如果当前有 n 条直线,新增加一条直线(第 n+1 条直线),可以多出来 n 个交点(新的直线和之前所有的直线都有交点),而多出来 n 个交点对应到可以多出 n+1 个平面(比如从两条线,又新增一条线时,新的线和两条线都相交,作用在三个区域上,对这三个区域切分,增加三个平面)。也即:
$S_{n+1}=S_{n}+(n+1)=1+\frac{(n+1)(n+2)}{2}$
线圈分割平面
设有n条封闭曲线画在平面上,而任何两条封闭曲线恰好相交于两点,且任何三条封闭曲线不相交于同一点,问这些封闭曲线把平面分割成的区域个数。
当平面上已有n-1条曲线将平面分割成an-1个区域后,第n-1条曲线每与曲线相交一次,就会增加一个区域,因为平面上已有了n-1条封闭曲线,且第n条曲线与已有的每一条闭曲线恰好相交于两点,且不会与任两条曲线交于同一点,故平面上一共增加2(n-1)个区域,加上已有的an-1个区域,一共有an-1+2(n-1)个区域。所以本题的递推关系是an=an-1+2(n-1),边界条件a1=2。
平面分割变体很多,在短时间内建立递推关系是难点。
题目见链接⬆
对于折线:设第n-1条折线,把空间划分的区域为f(n-1),为了增加的区域最多,新增的折线要和之前的n-1条折线的2(n-1)条边都相交(因为是折线所以每条边交了两次),交点数为4(n-1),新增的线段数为4(n-1),射线数为2。但要注意的是,折线本身相邻的两线段只能增加一个区域。新增区域数目为4(n-1)。
新增区域图示⬇
(多一个线段或射线都产生一个新区域,除了最左边那两条合起来只产生一个新区域。)
所以有递推公式:
f(n)=f(n-1)+4(n-1) + 1=2n2-n+1
偷懒方法
这类问题一般都有固定的公式,二维的一般是an2+bn+c,三维的一般是an3+bn2+cn+d。
用待定系数法求出各个系数就OK了。
$C_{n}=\frac{1}{n+1}\left(\begin{array}{c}
2 n \\
n
\end{array}\right)=\frac{(2 n) !}{(n+1) ! n !}$
递归例子
斐波那契数列(Fibonacci)应用
得到的是递推式,但只能用递归写,或者优化为DP
兔子繁殖问题
有雌雄一对兔子,每过两个月便可繁殖雌雄各一的一对小兔子。问过n个月后共有多少对兔子?
设满x个月时共有兔子F(x)对,满x-1个月时共有兔子F(x-1)对,第x月当月新生的兔子数目为n对。
Fx=n+F(x-1)
n=F(x-2)❗ (即第x-2个月的所有兔子到第x个月都有繁殖能力了)
得Fx=F(x-1)+F(x-2)
边界条件:F0=0,F1=1
由上面的递推关系可依次得到:
F2=F1+F0=1,F3=F2+F1=2,F4=F3+F2=3,F5=F4+F3=5…
繁殖问题通常与斐波那契数列有联系。类似题:母牛的故事。
可得递推式:
$f(n)=\left\{\begin{array}{ll}
n & n<=4 \\
f(n-1)+f(n-3) & n>4
\end{array}\right.$
蜜蜂爬行
一只蜜蜂只能爬向右侧相邻的蜂房,不能反向爬行,如图所示。
求出蜜蜂从蜂房a爬到蜂房b的可能路线数。
蜜蜂只能通过b-2或b-1点到达b点,显然有
$f_{n}=f_{n-1}+f_{n-2} \quad(a+2<=n<=b)$
边界条件为fa=fa+1=1。
汉诺塔问题
法国数学家爱德华·卢卡斯曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。
这里用递归更好理解(仅此而已,递推效率更高),也能求通项公式
以Y杆为中介,将前n-1个圆盘从X杆移到Z杆(本身就是一个n-1的汉诺塔问题了!)
将第n个圆盘移到Y杆
以X杆为中介,将Z杆上的n-1个圆盘移到Y杆(本身就是一个n-1的汉诺塔问题了!)
设f(n)为将n片圆盘所在塔全部移动到另一塔最少总次数;由递归算法可知:f(1) = 1;当n>1时,f(n) = f(n-1) + 1 + f(n-1)。计算可得通项公式:f(n)=2n-1. (n>0)
有了递推公式f(n)=2*f(n-1)+1,递归就完了(数太大了,注意数据类型是ull)
关于ULL:
unsigned long long类型是目前C语言中精度最高的数据类型,可以用来表示20以内的阶乘数据,20以外的自测。
unsigned long long的精度是64位,double或者long double 虽然也占有8个字节,但是他们的实际精度只有53位。
使用 %llu 就可以打印一个unsigned long long数据了
#include
unsigned long long Hanoi(int n)
{
if (n==1) return 1;
return 2*Hanoi(n-1)+1;
}
int main()
{
printf("%llu",Hanoi(64));
}
倒序输出正整数
先进后出,可以用栈实现(并不会)
例:给出正整数 n=12345,以各位数的逆序形式输出,即输出54321。
递归思想:首先输出这个数的个位数,然后再输出前面数字的个位数,直到之前没数字。
数列的递归表达式:
$f(n)=\left\{\begin{array}{ll}
\operatorname{print}(n \% 10), & \mathrm{n}<10 \\
\operatorname{print}(n \% 10) ; f(n / 10), & \mathrm{n}>=10
\end{array}\right.$
C代码如下:
void rev(int n)
{
printf("%d",n%10);
if (n > 10)
rev(n/10);
}
python最省事的就是切片:
print(str(s)[: : -1])