递归 是算法竞赛比较基础的一种思想,由于其时间复杂度[1]普遍较高,且适应用广,通常把它归类为 朴素算法 ,即 暴力算法 ,对其哪怕只是有了解也会对今后学习有帮助。
本文会通过对递归问题的解析让读者理解递归问题,但由于个人知识有限,可能会有纰漏,若发现请指正。
一. 递归概述
所谓递归,就是通过一定的规则,把大问题化为小问题,再把小问题化为更小的问题,最终化为可以求解的问题的一种思想。
用数学的话来说,可以表达为
\(a_n\) 就是我们要求解的问题,\(f\) 是一种运算法则,可以通过一定的规则得出一个数。
对于初学者可能有点抽象,举个例子,小学就学过等差数列,可以表示为
其中 \(a_{n - 1}+4\) 就是 \(f\),自变量为 \(a_{n - 1}\),通过计算可以得出,此数列为 \(0\ 4\ 8\ 12\ 16\ 20\ ...\)
现在,假如我们要求解 \(a_{100}\),就需要知道 \(a_{99}\);而要知道 \(a_{99}\),就要知道 \(a_{98}\),以此类推,最后到 \(a_0\),这就是 递。
找到了 \(a_0\) 这个已知的值,就可以通过 \(a_n = a_{n - 1}+4\) 进行运算,求出 \(a_1,a_2,a_3,...,a_{99}\),最后就不难求出 \(a_{100}\)的值,这就是 归。
上面的 式(1) 就是 递归式。
二. 汉诺塔
好了,你现在已经具备了基础知识,我们来看一些例题吧,我们会在题目中继续学习。
这是一个有名的递归问题,你常会在某些地方遇见它。
题目如下
如图,有三根柱子,分别标号为A、B、C,在柱子A上有若干个圆盘,从下到上由大到小排列。
现在,你要将A上的圆盘全部移到C上去,但是,你力气太小,一次只能移动一个圆盘,且由于大的圆盘会把小圆盘压坏,大圆盘不能在小圆盘上面。
请问,移动的最少步数是多少?
有一个与此相关的传说
在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。
当我们学习完这一部分时,我们会知道这时间有多么长。
先来观察一下 小的情形,这是通向成功的重要途径。
设 \(H_n\) 为有 \(n\) 个圆盘时的最小移动步数
当 \(n = 0 \) 时,由于没有圆盘可以移动,有 \(H_1 = 1\), 即 \(A \rightarrow C\)。
当 \(n = 1 \) 时,明显有 \(H_1 = 1\), 即 \(A \rightarrow C\)。
当 \(n = 2 \) 时,有 \(H_2 = 3\),即\(A \rightarrow B,A \rightarrow C,B \rightarrow C,\)
以此类推,可得下表
看起来有
我们来看看为什么会这样
要在 \(A\) 柱上移走 \(n\) 个圆盘,首先要移走在第 \(n\) 个圆盘上的 \(n - 1\) 个圆盘,由于第 \(n\) 个圆盘是最大的,所以对其余的 \(n - 1\) 个圆盘的移动不造成影响,所以问题就分解为移动第 \(n\) 个圆盘与移动上面的 \(n - 1\) 个圆盘。
- 第一步,将 \(n - 1\) 个圆盘从 \(A\) 柱移动到 \(B\) 柱上
- 第二步,将第 \(n\) 个圆盘从 \(A\) 柱移动到 \(C\) 柱上
- 第三步,将 \(n - 1\) 个圆盘从 \(B\) 柱移动到 \(C\) 柱上
所以我们可以知道
之所以要用 \(\leqslant\) 而不是 \(=\) ,是因为我们只是知道了众多方法中的一种,并不能说明它的每一步是必须的,所以不能确定这个步数是最小的。
那么,有更好的方法吗?仔细思考一下,你会发现实际上没有。
迟早你都要移动第 \(n\) 个圆盘,但是,要移动第 \(n\) 个圆盘,就必须要移动位于第 \(n\) 个圆盘上的 \(n - 1\) 个圆盘。你的目标是把 \(n\) 个圆盘从 \(A\) 柱移动到 \(C\) 柱上,所以,你只能把 \(n - 1\) 个圆盘从 \(A\) 柱移动到 \(B\) 柱上,等到第 \(n\) 个圆盘移动完毕后在径直从 \(B\) 柱移动到 \(C\) 柱上。所以我们有
结合 \(式(2)\) 与 \(式(3)\) 容易得到
好了上式就是我们要的答案了。
整理一下,长这样
挺漂亮的。
但是有一个问题,如果你需要计算 \(H_{10}\) 还好说,可以通过暴力代值解决,但是如果要计算如之前问题中的 \(H_{64}\) 甚至于 \(H_{10000}\) ,直接代值对于人类来说肯定不现实,所以,要成为超人的存在才行,我不做人啦!!!!!要找到一种能方便计算的形式,及 封闭形式,此式只与 \(n\) 有关。
关于封闭形式,举个例子来说前面的等差数列,递归式长这样
而它的封闭形式则长这样
可以在一次找到 \(H_n\) 的值,而不需要再去找 \(H_{n - 1}\) 的值,这就是封闭形式。
回到汉诺塔问题上来,我们还是首先观察一下这些数据有什么特点,只是上一次是用递归式表达,这一次我们用封闭形式来表达。
用封闭形式来说,好像有
我们将其代回 \(式(4)\)
所以封闭形式 \(式(5)\) 对于递归式 \(式(2)\) 是成立的。
这就是著名的 数学归纳法,\(H_1 = 1\) 称为 基础,而上述证明过程称为 归纳。
以上的一切都建立在一个灵光的大脑上面,如果你没有这样一个令人羡慕的大脑,也有一种属于你的方法。
我们可以对 \(式(2)\) 进行一些变形
令 \(A_n = T_n + 1\),可以得到
可以轻松知道
所以就有
艰辛的推理与证明终于结束了!
现在我们来看一下最开始的问题,\(H_{64}\) 是多少呢?根据 \(式(5)\) 可以算出 \(H_{64} = 2^{64} - 1 = 18\ 446\ 744\ 073\ 709\ 551\ 615\),如果僧侣一秒移动一个的话,那么宇宙共有 \(584\ 942\ 417\ 355\) 年寿命,しかし,科学家预测的宇宙寿命只有 \(140\ 000\ 000\ 000\)年,相差甚远。
总结
要求解一个递归问题,有如下步骤
- 观察小的情形
- 给出递归式并证明
- 给出封闭形式并证明
这样就能成功地解决一个递归问题了!
时间复杂度:其实可以理解为是程序执行次数的平均值,是评估程序运行时间的一大标准 ↩︎