由一道题所想到的

        前一篇文章写得其实很匆忙,为了尽可能保证不贻笑大方,要查阅许多资料把许多细节问题搞清楚才敢写出来,没有把握的东西一般不敢写。因此那篇文章很多地方没有展开,比如traits。对于一个严谨认真的人来说,其实现在应该回过头去把前篇文章再修改修改、润色润色,但我的习惯是做的时候尽量严谨,做完就扔到一边,不想回头了。而且现在我的脑子里积了一些东西,迫切地想要把它们写出来,甚至这篇文章刚开头,下篇文章的题材我已经在构思了。

       我从小到大都有个习惯,喜欢预习,不喜欢复习。我很少做笔记,因为记了我也不会看。我学某种知识时,一般会忽略细节,而试图理解其思想或方法,一旦理解了,它们就会留在我的记忆里,也就不太有必要记一大堆笔记了。我的认识是,该理解的该学会的都会记在脑子里,记不住的东西要么是你没有理解,要么是一些死记硬背的东西。我很喜欢爱因斯坦说的:声音的速度是多少?那恐怕要查一查物理辞典。人的大脑不是用来记忆那些已经被发现的知识,而更多地是要去发现新的知识。

        当然,以上全都是我对懒惰的借口,我无意与伟人相提并论,我连人家的一根腿毛都比不上呢。只是说,我有意或无意地养成了这种思维习惯,并且目前来看,我也没准备改变这种习惯,因而想要解释一下,以免使不了解我的人觉得莫名其妙。由于多了这些琐碎的东西,我所写的已经不能算是纯技术或学习方面的文章了,不过这也是我的本意:我不在意形式,只是想要尽可能多地学到一些东西。我没有太多的时间和精力来对文章的结构、内容进行编排和润色(因为还要抽出时间健身、睡觉、玩游戏、吹牛,哈哈),这些文字只是一些简单的随笔性质的东西,将就一下把。好了,现在开始正题。

       那道题叫作Climbing Stairs,本科的时候见到的,前段时间在leetcode上又看到了。这类题并不需要太多复杂的算法,更多地考察的是编程的基本功、思维的清晰度和灵活度等等。不少题目是那种即使做不出来但一看答案就恍然大悟的,不是难到看了答案还要花很大精力甚至看不懂,有时技巧性会强一些,有点类似脑筋急转弯。比如这个题目

You are climbing a stair case. It takes n steps to reach to the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?


这道题我的第一反应似乎是递归。大家都知道,递归相对来说,实现简单,只要把递归公式和边界条件设置好就行了,语言本身的栈机制是比较容易实现递归的,然而性能上是不能令人满意的,这是非多项式复杂度算法,无论是时间还是空间。这道题的关键是,如果假设c[i]为爬到第i级台阶的方法数量,则c[i] = c[i-1] + c[i-2] (i>2); 而爬上第一级台阶明显只有一种方法,即c[1] = 1,爬上第二级台阶有两种方法:要么一级一级地爬,要么一次爬两级,因此c[2] = 2.因此这道题可以用迭代或动态规划(Dynamic Programming)来做,最根本的思想是自底向上计算,先考虑c[i-1]和c[i-2],再计算出c[i];而递归先考虑c[i],再向下递归计算c[i-1]和c[i-2],一直递归直到满足边界条件,然后返回,由子问题的解合并成上一级问题的解,这是自顶向下的思想。

       顺便再说一说分治法(Divide and Conquer),DP和DC都是算法思想,而递归既可以说是算法,也可以说是实现手段。DC可以用递归来实现,归并排序(Merge Sort)就是DC的典型应用。爬楼梯这道题之所以用递归做性能会非常差,是因为递归中存在重叠子问题。例如,计算c[10],向下递归计算c[9]和c[8],c[9]又会向下递归计算c[8]和c[7],c[8]向下计算c[7]和[c6];由于递归的每一层调用都是独立的,其参数会作为局部变量独立入栈,即使计算c[8]时c[7]会被计算出来,计算c[9]时仍然要重新计算c[7],它们在递归树上的路径不同,在栈中也是独立分配空间计算(C/C++语言是传值调用(Call by value),传入实参时会在栈上构造局部变量拷贝实参的值,函数返回或退出后局部变量被销毁释放,需要通过全局变量、指针或return语句来操作外部 数据),这就造成了时间和空间上的浪费。而自底向上计算时,子问题已经按序计算出,即计算c[i]时,c[i-1]和c[i-2]都已经计算完毕(其实所有序号小于i的值都已计算出来,c[1]、c[2]为初始值,按照公式依序计算c[3]、c[4]、c[5]...等),子问题只计算一遍,然后保存起来,需要的时候只需直接调用即可而不需重新计算,这就将指数级的复杂度降为线性复杂度。这与备忘录法类似,但备忘录法是自顶向下的。当然,本题其实比较简单,不太需要完整的动态规划算法,直接迭代计算即可。再说白了,忽略问题的具体场景,抽象出其数学本质后,这题其实就是计算斐波那契(Fibonacci)数列,最简单的方法是用矩阵计算,时间复杂度为O(logN)。

       这道题本身并没有什么难的,可能关键代码就一行。我自己也不是专门研究学习算法,没怎么刷过算法题,水平很低,所以关键不在于用了多高深的算法和技巧解出了这道题。我想表达的是,通过这道题,可以联系到很多计算机编程方面的知识,DP、DC、递归、斐波那契数列、函数、自底向上方法等等。很多东西都是相通的,理解它们然后才可以灵活运用。我的记忆力不算坏,但我曾经几乎花了两个月才记住自己的手机号,我想更多的是因为思维习惯的问题吧。自顶向下的学习方法一般来说更自然一些,符合人类认知的过程,大多数人看到电视机都是先了解其外观、功能、使用等,如果有兴趣,才会研究它的构造和原理。我个人的学习方法应该不是纯粹的自顶向下或自底向上,我喜欢先把某样知识或技术的大致框架或原理搞清楚,然后根据需要或兴趣选择某个知识模块深入学习。说起来,似乎偏向自底向上更多些?

       先写到这吧,还是很粗糙,大家多包涵,等把脑子里那些迫切想要“溢“出来的东西写完再看看有没有时间整理修改润色吧。:)

         

 

<think>嗯,用户想学习算法,让我随机推荐一道。首先,我需要确定用户的水平。可能是个初学者,所以目不能太难,但也要有一定挑战性。我得找一个经典的算法,既能帮助理解基础概念,又有实际应用。然后,想到分治算法,比如归并排序或者快速排序,但用户可能已经知道这些。或者选一个数组相关的目,比如两数之和,但太简单了。需要稍微复杂一点,但又不是特别难。比如,求两个有序数组的中位数,但那个可能有点难。或者找第K大的元素?对了,用户之前提到过快速排序,可能对分治比较熟悉。那找一个分治相关的目,比如“寻找两个有序数组的中位数”这个目,不过这个目确实有点难度。或者换个思路,比如最大子序和,这个可以用动态规划或者分治解决,比较适合。再考虑用户的引用里提到过随机算法和出注意事项,可能需要目有改编的空间。比如,最大子序和问,可以改编成不同的场景,比如股票买卖,这样更贴近实际应用。确定目后,需要按照用户的要求来组织回答。结构要清晰,分步骤解释。首先目描述,然后输入输出样例,接着解思路,分步骤讲解,最后给出代码示例。还要注意LaTeX格式的正确使用,行内公式用$...$,独立公式用$$...$$。然后,解思路部分要详细。比如最大子序和,分治法的思路是将数组分成左右两部分,分别求左右的最大子序和,再求跨越中间的最大子序和,最后比较三者取最大。这个过程需要递归处理左右子数组。 代码部分需要用Python实现,确保语法正确。同时,要解释代码的每一部分,比如递归终止条件,如何计算左右和中间的最大值。最后,生成相关问,比如分治法的其他应用,动态规划解法,或者时间复杂度的分析。这些能帮助用户进一步深入学习。 检查是否符合所有系统级指令:中文回答,正确使用LaTeX格式,生成相关问,引用标识。比如在分治法的应用场景里,可以引用用户之前提到的快速排序作为例子,添加引用标记[^1]。 现在确保所有要求都满足,没有遗漏。然后组织内容,保持逻辑清晰,帮助用户逐步理解问及其解决方法。</think>### 推荐算法:最大子序和(分治法实现) #### 目描述 给定一个整数数组 $nums$,找到具有最大和的连续子数组(至少包含一个元素),返回其最大和。例如: 输入:$nums = [-2,1,-3,4,-1,2,1,-5,4]$ 输出:$6$ 解释:连续子数组 $[4,-1,2,1]$ 的和最大为 $6$[^1]。 #### 输入/输出格式 - **输入**:一行整数,用空格分隔,如 `-2 1 -3 4 -1 2 1 -5 4` - **输出**:一个整数,如 `6` #### 解思路(分治法) 1. **分治策略**:将数组分为左右两半,分别求解左半部分、右半部分的最大子序和,以及跨越中间的最大子序和。 2. **合并阶段**:比较三者的最大值,取最大结果。 3. **关键公式**: - 跨越中间的最大和:需从中间向左右扩展计算,满足: $$ \text{max\_cross} = \max_{i \leq mid}(\sum_{k=i}^{mid} nums[k]) + \max_{j > mid}(\sum_{k=mid+1}^{j} nums[k]) $$ #### 代码实现 ```python def max_subarray(nums): def divide_conquer(l, r): if l == r: return nums[l] mid = (l + r) // 2 left_max = divide_conquer(l, mid) right_max = divide_conquer(mid + 1, r) # 计算跨越中间的最大和 left_sum = right_sum = -float('inf') tmp = 0 for i in range(mid, l - 1, -1): tmp += nums[i] left_sum = max(left_sum, tmp) tmp = 0 for i in range(mid + 1, r + 1): tmp += nums[i] right_sum = max(right_sum, tmp) cross_max = left_sum + right_sum return max(left_max, right_max, cross_max) return divide_conquer(0, len(nums) - 1) # 示例测试 nums = list(map(int, input().split())) print(max_subarray(nums)) ``` #### 复杂度分析 - **时间复杂度**:$O(n \log n)$,分治递归深度为 $\log n$,每层合并操作耗时 $O(n)$[^2]。 - **空间复杂度**:$O(\log n)$,递归栈空间。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值