写在前面: 嗨,大家好。欢迎各位有缘人来到我的博客,这里是Easy Algorithms (EA) 系列第一弹。之所以写这个专栏,完全是为了回顾和整理自己的算法知识。本科四年,我大概搞了三年半的算法竞赛,通过自己的不懈努力,还算取得了一丢丢丢丢小小的成就。实话实说,我是从大一下学期才接触算法竞赛的,再加上自己的智力远不如那些OI大佬,所以自认为自己也就是一个中下水平,也只是将将够应付基础的算法面试罢了。后来,有幸保送到中国科学院大学读研究生,慢慢发现之前的竞赛经历无形之中给我很多帮助和启发,并且它锻炼了我有较为不错的coding能力和工程能力,学什么新东西速度也都还算可以,思维也得到了很好的锻炼。最近,偶尔发现AI中的很多场景其实都还是需要传统的算法的,比如物体跟踪领域,就常常需要图论的知识,比如二分图,匈牙利算法等。于是,决定开个专栏回顾下这下知识,希望对自己未来的科研之路有所帮助。此外,我深知在一个弱校一个人钻研算法的头痛之处,所以也希望可以用简单的语言帮助那些需要帮助的同学们。所有的文章,我尽量用简单易懂的原创作品呈现给大家,当然如果发现有高手比我写的好,我也会加以转载并注明出处,侵删,谢谢!
由于本人水平有限,博文中如果有什么错误的地方,恳请大家可以批评指正,不胜感激!
最后,用一句话送给那些犹豫不决想要放弃的有缘人,“锲而舍之,朽木不折。锲而不舍,金石可镂”!
递归与分治
今天我们就来一起学习一下递归和分治算法。
递归与分治关系
提起递归与分治,很多人总会冒出一个疑问,递归与分治到底是什么关系!??? 包括我自己在内,很长时间对于二者区分不开。那么今天,我便告诉大家,递归和分治半毛钱关系都没有,他们根本就没有任何联系! 为什么还有很多人把他们放在一起讲呢?只不过分治法可以通过递归高效的来解决,其实你也完全可以用循环暴力枚举的方法来实现,只是比较麻烦罢了。
递归
一句话概括递归就是: 某个函数重复的自己调用自己直到满足某种返回条件是停止调用,并逐层返回。 这个返回条件一般就是我们从实际问题中发现并定义的。通常情况下,通过递归我们把原问题分解成了更小的子问题,直到最后被解决。
int func(传入数值) {
if (终止条件) return 最小子问题解;
return func(缩小规模);
}
递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。
递归本质上是一种编程技巧,或者说一种解决问题的思维方式,其更像是我们常说的数学归纳法的思想。
在数学归纳法中,有时候我们很难证明一个递推公式的有效性。于是,我们便从一个小的数开始,比如 n = 1 n=1 n=1,然后证明该公式对第 k k k个数成立,那么此时在证明对第 k + 1 k+1 k+1个数也成立,进而就可以证明我们的公式正确。详细过程见百度百科
递归和数学归纳法的过程类似,但是其是一个相反的过程。递归存在所谓的结束条件,我们从一个大规模开始,逐渐缩小问题的规模,子问题得到解决,我们的大问题自然就得到了解决。而数学归纳法,这个数值是无穷无尽的,不存在所谓的结束条件,我们通过k和k+1将结论延伸到无穷,就证明了正确性。
递归的过程
很多人搞不明白递归的过程,甚至一搞到递归的过程就蒙蔽。我的建议是,不要试图想明白每一步递归到底做了什么子问题是如何解决的,而是理解函数的作用,并确信最后问题能被解决即可。
实际上,递归的过程可以看成是一个压栈和出栈的过程(这里假装大家都了解基本的数据结构队列和栈,栈就是 “先进后出” 的结构罢了),所以如果没有返回条件,当内存耗尽递归也就爆栈了。下面是一个简单的递归压栈出栈的过程图片来源,感谢:
调用Age(5)
相信看了上面这个图,我们已经可以很好的理解递归的整个过程了。那么我们为什么要写递归呢?
为什么要写递归
- 首先,递归的可解释强,它程序的结构更清晰、简洁、更容易让人理解。
- 便于求解,而不需要探究子问题到底怎么被解决
- 再就是锻炼思维能力,因为他是一个从后往前看的过程
- 有时候高效,有时候低效(因为需要消耗内存,当数据量大不建议)
递归的例子
- 递归的一个经典的例子就是求n的阶乘。很显然,求n的阶乘f(n)等价于求n-1的阶乘f(n-1)在乘以n,即 f ( n ) = n ∗ ( n − 1 ) f(n) = n * (n-1) f(n)=n∗(n−1),返回条件就为n=1时 f ( n ) = 1 f(n)=1 f(n)=1
- 斐波那契数列。
(虽然递归并不是一个高效求解的办法,由此引申出对递归的优化,比如剪枝,记忆化搜索等等,后面的系列会详细介绍)
// 递归
int Fib(int i) {
if (i < 2) {
return i == 0 ? 0 : 1;
}
return Fib(i - 1) + Fib(i - 2);
}
- 再比如,求二叉树的最大深度等等
- 递归经典问题——汉诺塔等等,感兴趣的同学可以自行学习。
递归问题的一般步骤:
1. 推导问题能否通过求解其子问题得到答案
2. 思考递归结束条件
3. 编写递归函数,调用并返回结果
分治
分治,英文为divide and conquer,一句话概括分治的思想就是:当问题的规模很大不易求解时,我们可以将一个问题分解为k规模相同且较小的子问题,子问题相互独立且与原问题相同,然后递归求解这些子问题,从而合并得到原问题的解。
如下图,
这里就验证了我们上面说的,递归和分治真没半毛钱关系。只不过分治也是将问题分解为多个规模相同且独立的子问题,而从子问题的求解到合并,恰恰是递归所擅长干的事,这也就使递归和分治往往捆绑在一起,从而得到很多高效且清晰的算法!(比如常见的归并排序)
易用分治的问题,往往具有如下性质:
- 该问题的规模缩小到一定性质就可以很容易解决。 比如归并排序,当规模缩小到只有两个元素,我们很容易就比较出谁大谁小了。
- 问题需具有最优子结构的性质。 也就是说,原问题的最优解一定包含子问题的最优解。(或者通俗的说,子问题每一步最优,合并得到的原问题一定也是最优的)
- 子问题可以合并得到原问题的解
- 子问题之间最好是独立的
分治举例
简单罗列下想到的几个例子,就不给出代码了,建议自己实现,帮助理解。
- 计算n次方。很容易想到考虑下奇偶,然后n划分为n/2 * n/2, n/2 分为n /4 * n/4…
- 归并排序,经典的分治思想的算法
- 棋盘覆盖
- 大整数乘法
- 汉诺塔
- etc…