暗时间:学习的精神和方法
一直关注的博主终于要出书了,满心欢喜。
博主所关注的,大抵也都是我所关注的。同样的问题,我大学时思考过,读研时思考过,上班后也思考过,没有答案;他也思考过,有了一些答案,就写了出来,与大家分享,从大学至今。
博主要出的书名叫《暗时间》,来自所写博文的归纳,所写的东西,也不是一般登大雅之堂之书中所能见到的,它们来自平日里点点滴滴的思考和总结。这些思考的成果,谈不上什么解决什么重大的科学难题或者社会焦点问题,却很是迎合我这样一类人的“特殊”需求:关于学习和思考的态度、方法和精神。
博客中内容很多,每天看一点,看了几天。看了之后觉得似懂非懂,似是非是,有时候觉得这些想法实在太精辟了,不敢相信,于是又看了几遍,直到看出一些不认同的地方才罢手——不能盲从嘛!
一、关于书写的看法:书写是为了更好的思考。
我的经验似乎是这样的:有些不成熟的想法(构想、设计、方案等还好,特别是判断),放在心里虽然咯得慌,老惦记着,但是时不时受到启发、刺激,时不时拿出来想一想(这就是所谓的“暗时间”经历!),最后内心谨慎犹豫的拿出来试验一下,还真行!这种感觉还是挺惬意的。换一种做法,写出来,虽然只是写出来,手似乎就给了内心一个认可的ACK信号,本来一个未决的事件就这么被默默从潜意识的TODO list中清除了,本来还需要更多思考,需要更多实践、灵感来启发的想法,还没来得及得到一个合理的答案就从潜意识的Active任务中,没了。
后来,我就不再随意的把想法“书写”出来了,宁可用个小纸条,故意写一些只言片语,故意不用连贯的、自然的、合乎逻辑、便于理解方式输出思维。
书写,说成备忘更为合适一些。等到了想法基本连贯了,再书写出来,成为可供查阅、可供交流、便于回忆的形式,似乎更适合我。
思考,就像是“在黑夜中打着手电筒前行,每一步推导都将我们往前挪一小步,然而电筒的光亮能照到的范围是有限的,我们走了几步发现后面又黑了,想到后面就忘了前面的,想到某个分支上去就忘了另一个分支,意识的微光就在一个很小的范围内打转,始终无法往前走出很远。而将思维过程记录下来,则给了我们完全的回溯自己的思维轨迹的可能”。
二、如果你手里有一把锤子,所有东西看上去都像钉子:锤子和钉子。
我想,这是对于程序员“眼高手低”最生动、最精辟的解释了。一位老师也曾这样批评过我,我一直不理解、不认可,觉得自己“眼高”没错,如果有条件,我一定可以“眼高手高”。学术研究且不提,工作后对此才慢慢体会。
首先意识到的是,技术不是一切,需求大于技术。没有需求,技术必然会被废掉。后来又发现,需求也不是一切,没有平台,有需求也轮不到你上。技术的地位一再被贬低,实在是内心无法认可的。现在反思起来,这是偏见啊!心中要有爱,怎么会觉得所有东西都是钉子!既然工作了,就要务实,技术就是要为某一目标服务。如果说要为锤子找一个安身立命之所,那就是要好好认识自己,认清形势,选择一个合适的目标、平台。接下来,锤子自然如鱼得水,不必为无法施展、种种冲突而苦闷。
三、(算法)为什么(我)想不出来?:知其所以然(以算法学习为例)。
文中对现有算法书的批评,实在是大快人心:我们看到的是寥寥数行精妙绝伦的算法,然后仰天长叹自己想不出来啊想不出来。为什么想不出来,因为你不知道那短短数行算法背后经历的事怎样漫长的思考过程,如果问题求解是一部侦探小说,那么算法只是结局而已,而思考过程才是情节。
当年上学时,作为学生的我也只是敢怒不敢言呵!以为算法这样的东西,如果讲得那么清楚,那么直白,直白得如同我苦苦泡图书馆几天、费了无数张草稿纸、熬了几个夜的代码后才弄明白的那样,岂不是贬低了我们计算机科班出身人的身价?!!于是,悄悄地合上书,把草稿纸装订起来,把代码打包起来,在课本上画个小小的勾,暗示自己:这块确实弄懂了,不要声张了。
还原算法本来的诞生过程,确实很重要,也很难,它本来的面目往往是可憎的、混乱的,里面有试错,有选择,有牛B人物在关键部位依据独特知识结构和思维模式做出的构造和判断,也有牛B人物在孤立点之间迅猛的长途推理。把算法的设计与证明形容成一场与问题、不确定性、猜测的战役,丝毫不过分。这里战术关键字有:“
1、注意你的未知数:一遍遍将你置于不同的问题场景下然后在该提醒你的时候提醒你,让你醒悟到“哦,原来这个时候也应该想到这个啊。”
2、重在分析推理,而不是联想:学了一大通算法和数据结构之后的一个副作用就是,看到一个问题之后,脑袋里立即不管三七二十一冒出一堆可能相干的数据结构和算法来。
3、要将思维方法内隐化,需要不断练习,就像需要不断练习才能无意识状态下就能骑自行车一样。
”
四、思维体力:学习密度与专注力。
我不喜欢文中对于这种名词的定义,意会就好……我很喜欢体力这个词。还记得某个人跟我说起过,想不到你竟然能想出这个题的算法!其实,我也没想到。其实,我想得也很累。其实,我就是在在最堵得慌的那个点上往上又顶了一把。这就是体力;体力不支的时候,这就是精神的力量。
所以,体力很重要,多多练习吧!在思路受阻的那个点上,别说累,别停,再顶一把。
--------------- to be continued ...2011.7.19
五、为什么算法这么难?:多做题!多总结!记住未知数!(recall the unknown)
原文中说到的一个问题是,在算法推演/设计/构造时,要注意解释和转换,比如:
- 如何理解/解释最优?最优就是怎么改,都做不到更好。这样的理解,实际上就是提示了一种思路:尝试去改,怎么改似乎会更好,但是做不到,那么,原因就是最优的原因。
- 如何计算/解释代价(cost_i*level_i)树?将其中的乘法转换为加法,乘以层数,相当于遍历时每层做一次加法,有多少层,就做多少加法,达到乘上了层数的效果。
- 怎么用码元给符号编码?从集合中一个个枚举赋值吗?一些基本的建模、转换方法,就是将编码问题看做一个解空间的搜索问题,用树的形式来刻画码串。
文中举了一个霍夫曼编码的问题。看的时候发现已经忘了问题的准确定义了,于是又wiki+百度了一把(wiki也不靠谱啊):
霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行最优前缀编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
下面是我的理解和思路:
霍夫曼编码的问题输入包括:一个源符号的出现频率表,一个编码元表;输出:编码表。这里,评估的属性是:freq_i*len_i,目标:最小。
算法的策略其实已经给出,就是“出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码”。当然,这个策略也很显然,什么时候编码平均长度,或者sum^n_i(freq_i*len_i),最小?数学地,即文中给出的计算方法,可以假设一个较小值方案,即{<freq_i, len_i>}序列,然后再alter一下,发现要更小的话,除非len小的freq大。也就是说,每个i都满足len小的freq大的话,它就是最小值方案,没有更小了。 直观的,简化编码问题,一个字母对应一个长度的编码,那么,相当于一个len一个权重(freq),那么要想平均长度小,len大的就要分配freq小的。
好了,策略明确,剩下的任务就是算法的构造。这个有点专业,?????,要想到用树作为构造的载体,源符号充当结点,把编码元指派到树的枝上,然后通过遍历树来获取每个结点的编码串。
编码问题,基于树这个载体,抽象转换为结点的布局和枝的(编码元)指派。而枝的指派对于评估没有影响(编码长度一样),剩下的事情就是结点布局。
说到这里,前面的问题描述还有一个隐含条件:并置使用的(源符号的连接直接用编码串连接表示)变长编码,要求每个编码不能有相同的前缀——不然,同样一个前缀,组成一个长一点的编码对应个源符号,组成一个短一点的编码也对应一个源符号,无法译码了;而满足这个条件,译码时按照编码流遍历树就是了。如何满足这个条件?源符号只能挂在叶子结点上,(遍历路径的终点,只访问一次),不能挂在枝结点上。这样,我们只需要评估叶子结点的sum^n_i(freq_i*len_i),即构造叶子结点的遍历路径。
当只考虑叶子结点的遍历路径时,可以从两方面考虑。一种如原文中所述,尝试从简单情况开始构造,1,2,3,4,个叶子结点,发现一些明显的规律(同一层交换结点的构造等价),遇到一个关键点:频率第3大的结点放哪?平放(放同一层?)还是阶梯放(放上层?),尝试,然后考虑交换方案,一举例具体计算,发现跟频率和有关!另一种,比较灵异,考虑叶子结点的遍历路径时,就有两种拓扑结构:
/ / \
/ \ / \ d
/ \ / \ / \ c
a b c d a b
此时,可以发现《Algorithms》中给出的另一种cost计算方法或者说解释方法:底层叶子结点在计算freq_i*len_i时,那个len_I算起来很不漂亮,总是要从根结点数下来才知道现在在第几层(用计算机的语言来说,这个求和计算过程是迭代的,双层for循环,外层用于枚举所有叶子结点,内层用于计算其所在层数),因而,将这里的乘法转换为加法……(见片头),将两叶子结点的freq值之和标注到枝结点,递推之,将所有非叶子结点标注之(标注的过程相当于完成了原来求和计算中的乘法,因为在计算上层枝结点时下属叶子freq被求和了多次),这样处理之后,计算和的时候就再也不用管结点所在层数了,标注完了,和就定了。这样,构造问题,基于评估属性的新的解释,抽象转换为标注问题。
标注方法很简单,就是将下属(直接)结点求和。recall the unknown-如何使所有结点的和最小?回到我们已知的策略,频率最小的最深,一种思路是:举例具体计算一把吧!先来个简单的:
/ / \
/ \ / \ 4
/ \ / \ / \ 3
1 2 3 4 1 2
一个是2*(f1+f2)+2*(f3+f4),一个是3*(f1+f2)+2*f3+f4,比较一下,判决式为f1+f2 - f4。此时,阶梯状的好;于是知道构造一个反例f1+f2 > f4:
/ / \
/ \ / \ 6
/ \ / \ / \ 5
3 4 5 6 3 4
比较、归纳发现,判决式的本质是结点和的大小关系。recall 第3个放哪的悬疑,就是看已经选中的结点和与当前剩余结点的大小关系。
至此,思路探索完毕。如何证明这是对的呢?这是上面问题的第二种思路:转换为标注问题后,在不考虑算法复杂度/优化策略的前提下,仅从求解计划或者说步骤来看,是否发生了变化?求解计划是评估结点和的计划,以前是两层循环,计算求和序列两个参数;现在的是:先递归标注,再平面求和(所有结点只参算一次),原来一个内层需要全局信息的迭代计算法,变成了一个递归的,也就是两层计算法——标注一个结点时,只需要直接下属结点的信息——在第二步平面求和时,下属结点跟父节点是平等的,都只算一次。也就是说,如果自底向上标注的话,下下一层的结点可以扔掉。如何使全部结点和最小,这个问题,可见,是与递归历史(n-2)无关的(无记忆性),也就是可以采纳贪心策略,选取最小频率两个结点后,就可以扔掉,保留一个和结点,然后再重复该策略。
-------------------贪心策略 vs. 历史无关性/无记忆性/时齐的/与节拍起点无关的/time-homogeneous-----------------------