最近,在波利亚GG的谆谆教诲下,在pongba同学的循循善诱下,在TopLanguage的今天我们思考系列的
热情引导下,我终于痛下决心开始琢磨所谓的科学思考问题的方法。对大部分人而言,解题不是终极目的,只是希望在解题中培养的思考问题的方式能够广泛的应用
到其他领域。我依然觉得,思维这个抽象的可怕的东西,本质上还是个体化的,要因人而异,很难找到一招鲜吃遍天的套路。
但其中,一些共性的东西还是可以抽取 出来,一起琢磨一起讨论一起研究,并规范化科学化。大师的书pangbo的文章和大家的讨论提供了足够的参考素材,在这里我只是写我自己这些日子来的一些
实践和思考。我混了20多年,还是一个解题白痴,可以想见,这段日子的集训也不会有质变的效果,所以写下一个beta,作为不成熟的挡箭牌。在今后的日子
里,我会继续的思考和实践,希望有一天,我可以有勇气在后面加个v1.0。。。
我一直以为,要想解决问题,除了所谓耐心勇气等人性方面的因素外,至少还需要两方面的储备。一面谓之硬件,指的是基本的知识。比如说你要设计一个算法,基
本的复杂度分析,分治法,二分法之类的知识应该需要储备起来;如果你要设计个软件架构,基本的模式,操作系统的原理,交互接口的设计也应该了解一些。不要以为自需要很小的一点知识库,就可以像原子弹似的衍生出无限的能量,自己造轮子做汽车的生产方式,已被这个高效的社会淘汰了。
牛顿GG很早就教育我们要站 在巨人的肩膀上才能站的高看得远,而如《费马大定理》中描绘的一样,怀尔斯如果不是充分了解所有前人的思路深刻掌握许多高精尖的数学方法的话,从最原始的公理开始,我想再送他100年寿命估计也搞不定费马大定理。所以,一个人只有拥有了足够丰富的知识后,才有可能混得和机器猫的口袋似的,要啥有啥。。。
另一面谓之软件,指的就是解决问题的基本思维。在《Why Programs
Fail》中我了解到, 调试这种貌似依靠经验和灵感的活也是可以有章可循的。与之类似,所有貌似只能靠灵感喷涌来搞定的问题,也不会是只有咣当咣当拍脑袋才能获得解决思路的。我
们需要信赖灵感,但很多时候,也可以不依靠灵感。我们有了足够的知识储备和运用知识的经验后,就像骑上了匹宝马良驹,有了一马平川奔向目的地的能力,但是
没有良好的驾驭,南辕北辙了,就算是汗血宝马估计也无奈了。。。
从这段时间的实践来看,波利亚GG的解题框架是极具推广性的。不但在解题上会有效果,尝试推广到软件设计甚至其他方面上,也是可以行得通的。主要实践原料的来源,除了TopLanguage里面的一些题目外,还有来自《编程之美》的习题和高大爷仙书上的一些经典算法。在我这里,波利亚GG的框架被我抽取成下述样子,不过强烈建议自己去阅读原著,抽取属于自己的框架:
1. 首先是条件的提取。
对于题目而言,就是把题设的条件一部分一部分抽取出来,然后牢牢把握住未知量,从始到终,同时,要对已有已知量是否能导出未知量有个评
估。对于其他问题而言,比如软件设计,条件就是做这个项目的一些要求和约束,未知量就是需要做到的效果,评估就是在这样的条件下能不能做到需要的效果。这
个问题说起来简单,把握起来却不容易。我在自己做题或者给别人出题的时候,我经常会发现,在做的过程中很多条件被遗忘了,或者没有被充分利用到。
pangba的一个实践方法很好,就是把所有的条件都一条条写下来,并尽可能的扩展开,毕竟,不是每个人都是欧拉。一个例子,可以看这里。。。
2. 然后是寻找解决方案。
摊开了讲,就复杂了,想了解,看书算是最好的方式。简单说,先要对题目进行归约,努力回想你做过的类似的题目。有一招就对题目转述,
我觉挺好的方式,有的东西说一下就更明晰了。比如,题目是找一组一维点中的最大距离。改下题,说找一组有序点的最大距离,问题就明晰多了。先变有序,再找
距离。而从思索的角度上看,从条件开始试错,或从未知量开始倒着做都是可以尝试的方式。有的时候,你需要把特化考虑问题,即考虑特殊的情况类推出普遍情
况,这种方式我们常用。还有一种是泛化,把特殊的问题变普遍了,不知不觉中也会常用。比如这里的第二题,把那几个恐怖的数字抹平成了N,问题就清晰了。。。
3. 做完了一定要验证。
从小到大数学考试,我从来都是做完了就算,很公平,也被无数次惩罚了。验证可以是不完备的,用基本的逻辑和特例来验证一下,也可以是完
备的,用数学方法来证明。说这个就想起测试,写代码的时候,很多自以为完美无缺的逻辑,在测试下都显得脆弱不堪,所以无论如何,验证是必需的,这个懒,在
任何时候,还是不偷的好。。。
4. 扩展。
前面说的都很简单,客观原因大牛们已经写了很多了,主观原因我很懒。因此,这一点决定多写一点,理由是感触多些。扩展,在我的定义下,有两层意思,
一个是对原有方法的改进,另一个是尝试挖掘一下为可能的问题做准备。两者在实践上是一致的,因此归为一点。当你完成并验证了某个题或某件事,你还需要进一
步的反思和改进,这就是我所谓的扩展,实践包括以下几个步骤:
4.a. 列出所有你解决方案中用到的条件。
这个条件和题设的条件可能是不一致的,因为你在具体解决过程中可能加入了有形或无形的约束。比如,打印一个树的前序遍
历。很有可能,你在解决这个问题的时候默认这个树是以最常见的左右链表的形式保存的,这个过程中,你就多添加了一个约束,有没有想过,如果你用穿线树的格
式存,解决方案就完全不一样了。这样的条件列举还是蛮不容易的事情,很多隐藏的条件(约束)不容易被挖掘出来,要努力的多想尽量的多列。比如做比较排序的
时候,你有没有发现,你可能添加了一个无形的约束,就是每次都是键和键直接比较,想一想,去除掉这个约束视野会更开阔的。
4.b. 问自己一些问题,仔细思考你现行的解决方案中有没有做重复或多余或疑似复杂了的步骤。比如,KMP算法就是在降低一般算法的重复;再比如一题,找一个N位
二进制中的1的数目,如果你写了一个O(N)的算法,仔细想想你做了什么多余的工作呢;还比如,冒泡算法作为一种交换的排序算法,有没有做什么疑似复杂的
操作呢。
4.c. 如果有重复步骤,尝试在原有条件不变的前提下,优化解决方案。
比如,上面所提,找1数目的题目,你可以想到既然是找1的数目,那么应该能做到O(M)的算
法,其中M是1的个数。再继续,能不能够用一些其他策略,比如时间换空间,把算法复杂度降到常数级别呢。想知道答案,可以查看一下《编程之美》,呵呵。
4.d. 如果在原有条件上没有继续优化的空间,那么试图改变一些条件试试。
这个条件如果是你在设计执行方案的时候添加进去的,那么这就是一个对已有的优化;如果,这个条件是题设的,那么就是对这个未知的扩展尝试。
4.e. 在改进的过程中,你可能引入了新的约束,更新约束列表,继续尝试上述步骤。
最后,写个例子,我尝试按照这个思路把高大爷仙书第三卷的插入排序部分串了一下,本书的其他部分我也实践了一下,但还没有完全一体化,如果有兴趣不妨尝试一下^_^。
a. 根据打扑克牌得来的心得,设计了一个基于顺序数组存放的最最普通的插入排序,经验证正确,至此,上述1~3步的工作完成。
b. 提取了一下约束,包括:存储空间是连续的静态分配的;在排序的过程中,数组的前半部分是有序的;每次插入一个新元素的时候,并不能保证一定把这个元素插入到了最终位置;是基于键值比较来确定大小关系的;没有使用过多的额外空间。
c. 问自己一个问题,能不能提高插入的速度?很自然我们想到了二分,于是有了二分插入。
d. 再继续,发现在目前条件下没有太大改进空间(别说用斐波纳契分...),于是考虑改变一些条件。最简单的,如果我不保证前半部分有序,我能怎么做。于是有了二路插入,将点定在中间,减少移动次数。
e. 如果你和shell一样有更多的硬件储备(前面提到的,有时候是需要硬件储备的...),你会联想到去掉前半部分有序,可以运用局部化和分治的思想,于是就有了Shell排序。
f. 如果,你开始问自己的问题是,能不能减少数据移动提高速度?一个答案是,改变的顺序存储,变链表,于是有了链表插入。
g. 能不能更快?于是,在原有基础上改变,我们添加一个哨兵指针,放在上次插入的位置。
h. 不要忘了,更新一下列表,我们发现添加了一个链式存储的约束,这个约束下,二分不能使用,查找速度降低。
i. 自然而然,我们问自己,能不能查找又快又少移动呢?继续改变条件,还是变数据结构,于是有了树插入(又见硬件储备...)。
j. 想想,还有没有什么条件没有改变过。很显然,基于键的比较没变过,不能保证将元素插入到最终的位置也是一个。联想起来,能不能不基于比较基于计算,能不能尽量一次到位这样可以减少移动次数。如果足够幸运,你可以想出基于地址的排序。
k. 其实,还有很多条件没有变过或可以继续变,有兴趣就继续下去吧:)。。。
|