<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>刘未鹏 &#124; Mind Hacks &#187; 算法</title>
	<atom:link href="http://mindhacks.cn/topics/algorithms/feed/" rel="self" type="application/rss+xml" />
	<link>http://mindhacks.cn</link>
	<description>思维改变生活</description>
	<lastBuildDate>Sun, 20 Nov 2011 13:31:48 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>知其所以然（三）：为什么算法这么难？</title>
		<link>http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/</link>
		<comments>http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/#comments</comments>
		<pubDate>Sat, 09 Jul 2011 16:24:32 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/</guid>
		<description><![CDATA[广大码农同学们大多都有个共识，认为算法是个硬骨头，很难啃，悲剧的是啃完了还未必有用——除了面试的时候。实际工程中一般都是用现成的模块，一般只需了解算法的目的和时空复杂度即可。

不过话说回来，面试的时候面算法，包括面项目中几乎不大可能用到的算法，其实并不能说是毫无道理的。算法往往是对学习和理解能力的一块试金石，难的都能掌握，往往容易的事情不在话下。志于高者得于中。反之则不成立。另一方面，虽说教科书算法大多数都是那些即便用到也是直接拿模块用的，但不幸的是，我们这群搬砖头的有时候还非得做些发明家的事情：要么是得把算法当白盒加以改进以满足手头的特定需求；要么干脆就是要发明轮子。所以，虽说面试的算法本身未必用得到，但熟悉各种算法的人通常更可能熟悉算法的思想，从而更可能具备这里说的两种能力。

那么，为什么说算法很难呢？这个问题只有两种可能的原因：

算法本身就很难。也就是说，算法这个东西对于人类的大脑来说本身就是个困难的事儿。 
讲得太烂。 
下面会说明，算法之所以被绝大多数人认为很难，以上两个原因兼具。

我们说算法难的时候，有两种情况：一种是学算法难。第二种是设计算法难。对于前者，大多数人（至少我当年如此）学习算法几乎是在背算法，就跟背菜谱似的（“Cookbook”是深受广大码农喜爱的一类书），然而算法和菜谱的区别在于，算法包含的细节复杂度是菜谱的无数倍，算法的问题描述千变万化，逻辑过程百转千回，往往看得人愁肠百结，而相较之下任何菜谱涉及到的基本元素也就那么些（所以程序员肯定都具有成为好厨师的潜力:D）注意，即便你看了算法的证明，某种程度上还是“背”（为什么这么说，后面会详述）。我自己遇到新算法基本是会看证明的，但是发现没多久还是会忘掉，这是死记硬背的标准症状。如果你也啃过算法书，我相信很大可能性你会有同感：为什么当时明明懂了，但没多久就忘掉了呢？为什么当时明明非常理解其证明，但没过多久想要自己去证明时却发现怎么都没法补上证明中缺失的一环呢？]]></description>
			<content:encoded><![CDATA[<p><font color="#a5a5a5">不知不觉《知其所以然》系列竟然也写到第三篇了，虽然前面两篇也说了不少，但是总觉得还有东西没有说“透”，或者说没有说“好”。所以这篇试图从不同的角度用更好的例子来继续深入阐述。（感谢<a href="http://www.douban.com/people/silwile/">silwile</a>对本文的review和意见）</font></p>
<hr />
<p>广大码农同学们大多都有个共识，认为算法是个硬骨头，很难啃，悲剧的是啃完了还未必有用——除了面试的时候。实际工程中一般都是用现成的模块，一般只需了解算法的目的和时空复杂度即可。</p>
<p>不过话说回来，面试的时候面算法，包括面项目中几乎不大可能用到的算法，其实并不能说是毫无道理的。<strong>算法往往是对学习和理解能力的一块试金石</strong>，难的都能掌握，往往容易的事情不在话下。志于高者得于中。反之则不成立。另一方面，虽说教科书算法大多数都是那些即便用到也是直接拿模块用的，但不幸的是，我们这群搬砖头的有时候还非得做些发明家的事情：要么是得把算法当白盒加以改进以满足手头的特定需求；要么干脆就是要发明轮子。所以，虽说面试的算法本身未必用得到，但熟悉各种算法的人通常更可能熟悉算法的思想，从而更可能具备这里说的两种能力。</p>
<p>那么，为什么说算法很难呢？这个问题只有两种可能的原因：</p>
<ol>
<li>算法本身就很难。也就是说，算法这个东西对于人类的大脑来说本身就是个困难的事儿。 </li>
<li>讲得太烂。 </li>
</ol>
<p>下面会说明，算法之所以被绝大多数人认为很难，以上两个原因兼具。</p>
<p>我们说算法难的时候，有两种情况：<strong>一种是学算法难。第二种是设计算法难</strong>。对于前者，大多数人（至少我当年如此）学习算法几乎是在背算法，就跟背菜谱似的（“Cookbook”是深受广大码农喜爱的一类书），然而算法和菜谱的区别在于，算法包含的细节复杂度是菜谱的无数倍，算法的问题描述千变万化，逻辑过程百转千回，往往看得人愁肠百结，而相较之下任何菜谱涉及到的基本元素也就那么些（所以程序员肯定都具有成为好厨师的潜力:D）注意，即便你看了算法的证明，某种程度上还是“背”（为什么这么说，后面会详述）。我自己遇到新算法基本是会看证明的，但是发现没多久还是会忘掉，这是死记硬背的标准症状。如果你也啃过算法书，我相信很大可能性你会有同感：<em>为什么当时明明懂了，但没多久就忘掉了呢？为什么当时明明非常理解其证明，但没过多久想要自己去证明时却发现怎么都没法补上证明中缺失的一环呢？</em></p>
<p>初中学习几何证明的时候，你会不会傻到去背一个定理的证明？不会。你只会背结论。为什么？一方面，因为证明过程包含大量的细节。另一方面，证明的过程环环相扣，往往只需要注意其中关键的一两步，便能够自行推导出来。算法逻辑描述就好比定理，算法的证明的过程就好比定理的证明过程。但不幸的是，与数学里面大量简洁的基本结论不同，算法这个“结论”可不是那么好背的，许多时候，<strong>算法本身的逻辑就几乎包含了与其证明过程等同的信息量</strong>，甚至算法逻辑本身就是证明过程（随便翻开一本经典的算法书，看几个经典的教科书算法，你会发现算法逻辑和算法证明的联系有多紧密）。于是我们又回到刚才那个问题：你会去背数学证明么？<strong>既然没人会傻到去背整个证明，又为什么要生硬地去背算法呢</strong>？</p>
<p>那么，不背就不背，去理解算法的证明如何？理解了算法的证明过程，便更有可能记住算法的逻辑细节，理解记忆嘛。然而，仍然不幸的是，绝大多数算法书在这方面做的实在糟糕，证明倒是给全了，逻辑也倒是挺严谨的，可是似乎没有作者能真正还原算法发明者本身如何得到算法以及算法证明的思维过程，按理说，证明的过程应该反映了这个思维过程，但是在下文关于霍夫曼编码的例子中你会看到，其实饱受赞誉的<a href="http://www.amazon.com/Introduction-Algorithms-International-Thomas-Cormen/dp/0262533057">CLRS</a>和<a href="http://www.amazon.com/Algorithms-Sanjoy-Dasgupta/dp/0073523402">《Algorithms》</a><em>不仅没能还原这个过程，反而掩盖了这个过程</em>。</p>
<p>必须说明的是，没有哪位作者是故意这样做的，但任何人在讲解一个自己已经理解了的东西的时候，往往会无意识地对自己的讲解进行“线性化”，例如证明题，如果你回忆一下高中做平面几何证明题的经历，就会意识到，其实证明的过程是一个充满了试错，联想，反推，特例，修改问题条件，穷举等等一干“非线性”思维的，混乱不堪的过程，而并不像写在课本上那样——引理1，引理2，定理1，定理2，一口气直到最终结论。这样的证明过程也许容易理解，但绝对不容易记忆。过几天你就会忘记其中一个或几个引理，其中的一步或几步关键的手法，然后当你想要回过头来自己试着去证明的时候，就会发现卡在某个关键的地方，为什么会这样？因为证明当中并没有告诉你为什么作者当时会想到证明算法需要那么一个引理或手法，所以，<strong>虽说看完证明之后，对算法这个结论而言你是知其所以然了，但对于算法的证明过程你却还没知其所以然</strong>。在我们大脑的记忆系统当中，<strong>新的知识必须要和既有的知识建立联系，才容易被回忆起来</strong>（<a href="http://mindhacks.cn/2009/03/28/effective-learning-and-memorization/">《如何有效地学习与记忆》</a>）<strong>，联系越多，越容易回忆，而一个天外飞仙似地引理，和我们既有的知识没有半毛钱联系，没娘的孩子没人疼，自然容易被遗忘。</strong>（为什么还原思维过程如此困难呢？我曾经在<a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/">知其所以然（一）</a>里详述）</p>
<p>正因为绝大多数算法书上悲剧的算法证明过程，很多人发现证明本身也不好记，于是宁可选择直接记结论。当年我在数学系，考试会考证明过程，但似乎计算机系的考试考算法证明过程就是荒谬的？作为“工程”性质的程序设计，似乎更注重使用和结果。但是如果是你需要在项目中自己设计一个算法呢？这种时候最起码需要做的就是证明算法的正确性吧。我们面试的时候往往都会遇到一些算法设计问题，我总是会让应聘者去证明算法的正确性，因为即便是<b>一个“看上去”正确的算法，真正需要证明起来往往发现并不是那么容易</b>。</p>
<p>所以说，绝大多数算法书在作为培养算法设计者的角度来说是失败的，比数学教育更失败。大多数人学完了初中平面几何都会做证明题（数学书不会要求你记住几何所有的定理），但很多人看完了一本算法书还是一团浆糊，不会证明一些起码的算法，我们背了一坨又一坨结论，非但这些结论许多根本用不上，就连用上的那些也不会证明。为什么会出现这样的差异？因为数学教育的理想目的是为了让你成为能够发现新定理的科学家，而码农系的算法教育的目的却更现实，是为了让你成为能够使用算法做事情的工程师。然而，事情真的如此简单么？如果真是这样的话干脆连算法结论都不要背了，只要知道算法做的是什么事情，时空复杂度各是多少即可。</p>
<p>如果说以上提到的算法难度（讲解和记忆的难度）属于<a href="http://en.wikipedia.org/wiki/Accidental_complexity">Accidental Complexity</a>的话，算法的另一个难处便是Essential Complexity了：算法设计。还是拿数学证明来类比（如果你看过《Introduction to Algorithms：A Creative Approach》就知道算法和数学证明是多么类似。），与单单只需证明相比，<strong>设计算法的难处在于，定理和证明都需要你去探索，尤其是前者</strong>——你需要去自行发现关键的那（几）个定理，跟证明已知结论相比（已经确定知道结论是正确的了，你只需要用逻辑来连接结论和条件），<strong>这件事情的复杂度往往又难上一个数量级。</strong></p>
<p>一个有趣的事实是，<b>算法的探索过程往往蕴含算法的证明过程</b>，理想的算法书应该通过还原算法的探索过程，从而让读者不仅能够自行推导出证明过程，同时还能够具备探索新算法的能力。之所以这么说，皆因为我是个懒人，懒人总梦想学点东西能够实现以下两个目的：</p>
<ol>
<li><strong>一劳永逸</strong>：程序员都知道“<a href="http://en.wikipedia.org/wiki/Write_once,_run_anywhere">一次编写到处运行</a>”的好处，多省事啊。学了就忘，忘了又得学，翻来覆去浪费生命。为什么不能看了一遍就再也不会忘掉呢？到底是教的不好，还是学得不好？ </li>
<li><strong>事半功倍</strong>：事实上，程序员不仅讲究一次编写到处运行，更讲究“一次编写到处使用”（也就是俗称的“复用”）。如果学一个算法所得到的经验可以到处使用，学一当十，推而广之，时间的利用效率便会大大提高。究竟怎样学习，才能够使得经验的外推（extrapolate）效率达到最大呢？ </li>
</ol>
<p>想要做到这两点就必须<b>尽量从知识树的“根节点”入手</b>，虽然这是一个美梦，例如数学界寻找“根节点”的<a href="http://mindhacks.cn/2008/04/18/learning-from-polya/">美梦由来已久</a>（《跟波利亚学解题》的“一点历史”小节），但哥德尔一个证明就<a href="http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/">让美梦成了泡影</a>（《永恒的金色对角线》））；但是，这并不阻止我们去寻找更高层的节点——<a href="http://mindhacks.cn/2008/04/18/learning-from-polya/">更具普适性的解题原则和方法</a>。所以，理想的算法书或者算法讲解应该是从最具一般性的思维法则开始，顺理成章地推导出算法，这个过程应该尽量还原一个”普通人“思考的过程，而不是让人看了之后觉得”这怎么可能想到呢？</p>
<p>以本文上篇提到的霍夫曼编码为例，第一遍看霍夫曼编码的时候是在本科，只看了算法描述，觉得挺直观的，过了两年，忘了，因为不知道为什么要把两个节点的频率加在一起看做单个节点——一件事情不知道“为什么”就会记不牢，知道了“为什么”的话便给这件事情提供了必然性。不知道“为什么”这件事情便可此可彼，<b>我们的大脑对于可此可彼的事情经常会弄混，它更容易记住有理有据的事情</b>（<em>从信息论的角度来说，一件必然的事情概率为1，信息量为0，而一件可此可彼的事情信息量则是大于0的</em>）。第二遍看是在工作之后，终于知道要看证明了，拿出著名的《Algorithms》来看，边看边点头，觉得讲得真好，一看就理解了为什么要那样来构造最优编码树。可是没多久，又给忘了！这次忘了倒不是忘了要把两个节点的频率加起来算一个，而是忘了为什么要这么做，因为当时没有弄清霍夫曼为什么能够想到为什么应该那样来构造最优编码树。结果<em>只知其一不知其二</em>。</p>
<p>必须说明的是，如果只关心算法的结论（即算法逻辑），那么理解算法的证明就够了，光背算法逻辑难记住，理解了证明会容易记忆得多。但如果也想不忘算法的证明，那么不仅要理解证明，还要理解证明背后的思维，也就是<em>为什么背后的为什么</em>。后者一般很难在书和资料上找到，唯有自己多加揣摩。为什么要费这个神？只要不会忘记结论不就结了吗？取决于你想做什么，如果你想真正弄清算法设计背后的思想，不去揣摩算法原作者是怎么想出来的是不行的。</p>
<p>回到霍夫曼编码问题，我们首先看一看《Algorithms》上是怎么讲的：</p>
<p>首先它给出了一棵编码树的cost function：</p>
<p>cost of tree = Σ freq(i) * depth(i)</p>
<p>这个cost function很直白，就是把编码的定义复述了一遍。但是接下来就说了：</p>
<p><em>There is another way to write this cost function that is very helpful.</em> Although we are only given frequencies for the leaves, we can define the frequency of any internal node to be the sum of the frequencies of its descendant leaves; this is, after all, the number of times the internal node is visited during encoding or decoding&#8230;</p>
<p>接着就按照这个思路把cost function转换了一下：</p>
<p>The cost of a tree is the sum of the frequencies of all leaves and internal nodes, except the root.</p>
<p>然后就开始得出算法逻辑了：</p>
<p>The <em>first formulation</em> of the cost function tells us that the <em>two symbols with the smallest frequencies must be at the bottom of the optimal tree</em>, as children of the lowest internal node (this internal node has two children since the tree is full). Otherwise, swapping these two symbols with whatever is lowest in the tree would improve the encoding.</p>
<p>This suggests that we start constructing the tree greedily: find the two symbols with the smallest frequencies, say i and j, and make them children of a new node, which then has frequency fi + fj. To keep the notation simple, let&#8217;s just assume these are f1 and f2. By the <em>second formulation</em> of the cost function, any tree in which f1 and f2 are sibling-leaves has cost f1 + f2 plus the cost for a tree with n &#8211; 1 leaves of frequencies (f1 + f2), f3, f4, .., fn. <em>The latter problem is just a smaller version of the one we started with</em>.</p>
<p>读到这里我想大多数人有两种反应：</p>
<ol>
<li>觉得理所当然。 </li>
<li>觉得恍然大悟。 </li>
</ol>
<p>因为我当时也是这么觉得的。可是后来当我发现自己无法从头证明一遍的时候，我知道肯定是理解的不够深刻。如果理解的够深刻，那么基本上是不会忘掉的。</p>
<p>如果看完霍夫曼编码这样一个简短证明你觉得顺理成章，一切都挺显然，<em>那就坏了</em>，即便是看上去最基本的性质也往往实际上没那么显然。“逢山开路，遇水架桥”在我们今天看来是无比显然的事实，但是试想在没有桥的远古时代，一个原始人走到一条湍急的河流前，他会怎么想，他又能有什么法子呢？这是个他从来没有遇见过的问题。如果后来有一天，他路过另外一条小溪，看到小溪上有一截被闪电劈断的枯树，于是他踏着树干走过了小溪，并意识到“树横过河面”可以达到“过河”这个目的，这就将条件和目的建立了直接的联系（事实上，是自然界展示了这个联系，我们的原始人只是记住了这个联系）。后来他又路过那条河流，他寻思如何达到“过河”这个目的的时候，忽然意识到在他的记忆中已经遇到过需要达成同样目的的时候了，那个时候的条件是“树横过河面”，于是问题便归结为如何满足这个“树横过河面”的条件，而后一个问题就简单多了。（事实上波利亚在他的著作《How to Solve it》中举的正是这么个例子）</p>
<p>为什么那么多的算法书，就看不到有一本讲得好的？因为我们求解问题过程中的思维步骤太容易被自己当作“显然”的了，但除了我们天生就会的认知模式（联系，类比），没有什么是应该觉得显然的，试错是我们天生就会的思维法则么？是的，但是可供尝试的方案究竟又是怎么来的呢？就拿上面的例子来说，一个从没有见过枯树干架在小溪上的原始人，怎么知道用树架桥是一种可选的方案呢？俗话说巧妇难为无米之炊啊。我们大脑的神经系统会的是将目的和条件联系起来，第一次原始人遇到小溪过不去，大脑中留下了一个未实现的目的，后来见到小溪上的树干，忽然意识到树干是实现这个目的的条件，两者便联系起来了，因此问题就规约为如何架树干了。</p>
<p>回到《Algorithms》中的证明上，这个看似简洁明了的证明其实有几处非常不显然的地方，甚至不严谨的地方，这些地方也正是你过段时间之后试图自己证明的话会发现卡住的地方：</p>
<ol>
<li>作者轻飘飘地就给出了cost function的另外一种关键的描述，而对于如何发现这种描述却只是一语带过：&quot;<em>There is another way to write this cost function that is very helpful.. </em>we can define the frequency of any internal node to be the sum of the frequencies of its descendant leaves“这其实就是我常常痛恨的“<em>我们考虑</em>&#8230;”，这里作者其实就是在说”让<em>我们考虑</em>下面这样一种<em>奇妙</em>的转换“，可是怎么来的却不说。但必须承认，《Algorithms》的作者还是算厚道的，因为后面他又稍微解释了一下：“this is, after all, the number of times the internal node is visited during encoding or decoding&#8230;”这个解释就有点让人恍然大悟了，但是千万别忘了，这种恍然大悟是一种错觉，你还是没明白为什么他会想到这一点。这就像是作者对你说“<em>仔细观察</em>问题条件，我们<em>容易发现</em>这样一种奇妙的性质..”，怎么个“仔细”法？凭什么我自己“观察”半天就是发现不了呢？霍夫曼本人难道也是死死盯着问题<a href="http://en.wikipedia.org/wiki/Huffman_tree#History">“观察”了一学期</a>然后就“发现”了么？我们有理由相信霍夫曼肯定尝试了各种各样的方法，作出了各种各样的努力，否则当年Shannon都没搞定的这个问题花了他一学期，难道他在这个学期里面大脑就一片空白（或者所有的尝试全都是完全不相干的徒劳），然后到学期末尾忽然“灵光一现”吗？ </li>
<li>如果“仔细观察”:)，我们会发现两个cost function表达中frequency的概念有微妙的差异，在第一个cost function中，只有叶子节点有frequency，而这个frequency必须和叶子节点的深度相乘。而在第二个cost function中，内部节点也具有了frequency，可是所有节点的“frequency”忽然全都不跟深度相乘了。frequency的不同含义令人困惑。 </li>
<li>作者提到：第一个cost function告诉我们频率最低的两个节点必然处于最优编码树的底端，作为最低内部节点的两个子节点。这是一个不严谨的说法，从前文给出的条件和性质，只能推导出编码树的最底层必然能找到频率最低的两个节点，但<em>它们未必一定要是兄弟节点</em>，如果树的最底层不止能容纳两个节点的话它们就可以有不同的父节点。“我们不妨考虑”这样一个例子：对A,B,C,D四个字母进行编码，假设它们的频率分别是1， 1， 2， 2。这个时候我们可以构造如下图所示的两棵树，两棵树的cost都是12，都是最优的。但其中一棵树中，两个频率最低的节点并非兄弟。       <br /><a href="http://mindhacks.cn/wp-content/uploads/2011/07/tree2.jpg"><img style="background-image: none; border-right-width: 0px; margin: 5px 0px 10px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="tree2" border="0" alt="tree2" src="http://mindhacks.cn/wp-content/uploads/2011/07/tree2_thumb.jpg" width="434" height="246" /></a> </li>
</ol>
<p>为什么要提到上面这几点不显然和不严谨的地方，因为只要当你看到算法书上出现不显然和不严谨的地方，基本上就意味着作者其实跳过了关键的思维步骤。</p>
<p>不幸的是《Algorithms》这本书里面讲霍夫曼编码已经算是讲的好的了，如果你翻开著名的CLRS，看一看当中是怎么证明的，你就知道我说的什么意思了。有时候这些证明是如此的企图追求formal和严谨，一上来就定义符号一大摞，让人看了就想吐。</p>
<p>说了这么多，<em>有没有可能把霍夫曼编码讲的更好呢</em>？前面说过，霍夫曼编码我记了又忘，忘了又记，好几次了，有一次终于烦了，心想如果要自己去证明，会怎么去证，那个时候我已经忘了《Algorithms》里面怎么讲的了。所以我得从头来起，首先，对于算法问题，有一个一般性原则是，<strong>先看一看解空间的构成</strong>。尤其是对于搜索问题（最优化问题可以看做搜索问题的一个特例），这一点尤其重要。霍夫曼编码的可能的编码树是有穷的，如果穷举所有的编码树，然后找到那棵代价最小的，这种方法至少是可行的，有了可行的方法（即便是穷举）至少让我们内心感到踏实。</p>
<p>接下来便是提高搜寻效率的问题。而提高搜寻效率的关键（同样也是一个一般性原则），便是<strong>尽量去寻找问题条件能够推导出来的性质，</strong><strong>然后利用这些性质去避免不必要的搜寻</strong>，只要你学过二分搜索就应该理解这个一般性原则：二分搜索的效率之所以高于“穷搜”（O(n)），便是因为它利用了问题中的性质（有序）来避免了不必要的搜寻。有时候这个性质甚至可以直接将时间降为O(1)，例如在一个有序数组中寻找出现次数大于n/2的数（假设该数存在），利用“该数一定出现在数组正中间”这个性质，我们直接就避免了所有的计算。</p>
<p>不过，话虽如此，有时候这些性质并不是那么“显然”的，需要对问题进行深入的折腾才能有可能发现。第三个一般原则：如果你要搜寻的元素是某个满足特定条件的元素（例如寻找最优解的时候，“最优”的定义就是这个“特定条件”），那么可以“<strong>倒过来推</strong>”（数学证明常用手法，结论当条件使），即假设你已经找到了你要找的元素，那么能得出哪些结论，每一个结论都是最优解的一个必要条件，而<em>每一个必要条件都能够帮助你避免不必要的搜寻</em>，因为你<em>只要发现某个候选解不满足某个必要条件，就可以立即将其丢弃</em>，前面提到的寻找出现次数大于n/2的例子是一个极端情况，我们得出的必要条件导致我们可以直接丢弃除中点元素之外的一切其他元素，再例如如果有人叫你寻找有序数组中最小元素，你会毫不犹豫地把该数组头尾元素中较小的那个给他，因为你知道“如果那个最小元素存在，那么它<em>必然</em>位于头尾”——这个必要条件直接允许你丢弃掉n-2个候选解。</p>
<p>回到霍夫曼编码问题，按照这个原则，我们会去<em>假设已经得到了最优编码树</em>，那么我们能够发现关于它的什么性质呢？这里又要提到另一个适用于很多最优化问题的原则（前面提到的原则适用于一般性搜索问题），<strong>所谓最优解，就是说比其他所有解都要更好</strong>，虽然这句话听上去像是废话，但是它的一个直接推论——<strong>比与它邻近的所有候选解都要好</strong>——就是一个非常有用的，不是废话的性质了。学过微积分的都知道，光滑函数的最值点必然是大（小）于其邻域内的所有点的，然后再根据这个就自然推出该点的一阶导数（切线斜率）必然为0的性质，这个性质（必要条件）让我们直接省掉了去整个区间内搜索的麻烦，从而可以直接锁定有限几个候选解。那么，既然我们说最优霍夫曼树一定比它“附近”的树更好，我们就想看看，怎么来找到它附近的树。我们知道要从一个点到它附近，往往是对这个点进行一些调整，例如N+1是到达附近的另一个整数。霍夫曼树是一棵树，所以对这棵树的所有的一次“改动”（或“折腾”）都能够到达与它的“改动”距离为1的点（是不是想起“编辑距离”这个概念），怎么改动呢？最符合直觉的（虽然并不是唯一的）改动便是把叶子节点进行互换。</p>
<p>于是我们得到一个重要的推论：</p>
<ul>
<li><em>在最优霍夫曼树中，无论互换哪两个叶子节点，得到的树都变得更“差”。（严格来说是不会变得更“好”，因为最优树未必唯一）</em> </li>
</ul>
<p>这个性质看上去有点像废话，值得费这么多事么？值得。因为虽然前文说了很多，但都是大多数人大脑里面既有的，一般性的法则，前面说过，如果我们能够从我们已经掌握的一般法则出发来推导出问题的解，那么记忆负担是最小的，因为这里面用到的所有法则我们都很清楚，也知道怎么一步步往下走。</p>
<p>上面这个性质究竟意味着什么呢？如果你假设这两个叶子节点的频率为f1和f2，深度为d1和d2，互换它们的时候，其他叶子节点的cost保持不变，令为常量C，那么互换前总cost为C+f1d1+f2d2，互换后为C+f1d2+f2d1，既然互换之后的树一定更”差“那么就是说f1d1+f2d2 &lt; f1d2 + f2d1，简单变换一下就得到结论：f1(d1-d2)&lt;f2(d1-d2)，也就是说如果d1&lt;d2，那么f1必然&gt;f2，如果d1&gt;d2，那么f1必然&lt;f2。换言之就是<em>叶子节点的深度越高，频率必须越低</em>，否则就不可能是最优霍夫曼树。那么，之前我们觉得不那么显然的结论便呼之欲出了：频率最低的叶子节点必然位于树的最底层，频率最高的叶子节点必然位于树的最高层。</p>
<p>有了这个结论之后，我们便能够对最优霍夫曼树的构建走出确定性的一步，即，将频率最低的两个叶子节点放在最底层。别小看这一步，这一步已经排除了大量的可能性。这里，我们容易一开始天真地觉得最底层只有这两个叶子节点，于是它们拥有共同父节点，<em>这样一来霍夫曼树的整个拼图便已经拼好了一个小小的角落</em>。</p>
<p>然后我们会发现，要是它们不是兄弟怎么办呢？这里提到另一个一般原则——<strong>归约</strong>。不是兄弟的情况能否归约为是兄弟的情况？反正我们要求的是<em>一个</em>最优解，而不是<em>所有的</em>最优解，我们只需证明，如果当这两个最低频率的叶子不是兄弟的时候的确存在着某棵最优霍夫曼树，那么通过交换它们各自的兄弟，从而让这两个叶子团聚之后，修改后的树仍然是最优的就可以了。事实情况也的确如此，证明非常直接——既然这里涉及到的所有4个节点都在最底层同一个高度上，那么互相交换的时候不会改变他们任何一个人的深度值，所以总cost不会改变。</p>
<p>但是接下来我们犯了难，整个树的一个小小的樱桃状的局部是确定下来了，接下来怎么办呢？一个最自然的思路就是考虑第三小的叶子，因为前面说了，元素频率越低就越位于树的底部嘛。第三小的叶子有两种可能的归属，一是跟最小的两个叶子同样位于最底层（这不会违反我们前面得到的推论），这个时候第三小的叶子的兄弟叶子肯定是第四小的叶子，如下图：</p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2011/07/tree3.jpg"><img style="background-image: none; border-right-width: 0px; margin: 5px 0px 10px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="tree3" border="0" alt="tree3" src="http://mindhacks.cn/wp-content/uploads/2011/07/tree3_thumb.jpg" width="244" height="106" /></a></p>
<p>另一种归属就是往上一层去（注意，一旦第三小的叶子往上去了一层，那么剩下的所有叶子都必须至少在这个层以上），往上一层去了之后，它的兄弟是谁呢？不妨将它和刚才第一第二叶子的父节点结为兄弟（前面证明过，同层之前节点互换不会改变编码的cost），如下图：</p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2011/07/tree5.jpg"><img style="background-image: none; border-right-width: 0px; margin: 5px 0px 10px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="tree5" border="0" alt="tree5" src="http://mindhacks.cn/wp-content/uploads/2011/07/tree5_thumb.jpg" width="181" height="244" /></a></p>
<p>可是现在问题出现了：虽然第一步构建（最小的两个叶子）是确定的，但是到了第二步摆在我们面前的就有两个选择了，到底选择哪个呢？一个办法就是把两种选择都记下来，然后继续往下走。可是别小看两种选择，接下去每一步都有两种选择的话就变成指数复杂度了。所以现在我们便有了动机<em>回头看一看</em>，<em>看问题中是否有什么没有发现的性质能够帮助我们再排除掉其中一个选择</em>。理想情况下如果每一步都是必然的，确定的，那么N步我们就可以构建出整棵树，这是我们希望看到的，抱着这个良好的愿望，我们仔细观察上面两种构型，一个自然而然的问题是：这两种构型都有潜质成为最优解吗？如果我们能够证明其中一种构型不能成为最优解那该多好？就省事多了嘛。这里引入另一个一般性的解题法则：<strong>特例</strong>。我们的<strong>大脑喜欢具体的东西，在特例中折腾和观察会方便的多</strong>。</p>
<p>上面这个{1, 2, 3, 4}的例子就是个很好的特例，如图（注：图中节点旁的数字一概为<em>频率值，</em>而非编号）：</p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2011/07/tree31.jpg"><img style="background-image: none; border-right-width: 0px; margin: 5px 0px 10px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="tree3" border="0" alt="tree3" src="http://mindhacks.cn/wp-content/uploads/2011/07/tree3_thumb1.jpg" width="277" height="126" /></a></p>
<p>多加折腾一番<em>也许</em>我们不难发现，如果将1，2及其父节点跟叶子4进行交换（注意：<em>交换的时候1，2也被一同带走了</em>，因为反正1，2两个节点已确定是好兄弟永远不会分家了，折腾的时候只能作为一个整体移动，所以这里也可以说是<strong>交换子树</strong>），那么树的编码将会变得更优，因为这样一次交换会将1和2的深度+1，意味着整棵树的代价+3，而同时会将叶子4的深度-1，也就是说整棵树的代价-4，总体上整棵树的代价就是+3-4=-1（注意，在计算的时候我们只需考虑被交换的局部，因为树的其他部分的代价保持不变）。如下图：</p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2011/07/tree4.jpg"><img style="background-image: none; border-right-width: 0px; margin: 5px 0px 10px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="tree4" border="0" alt="tree4" src="http://mindhacks.cn/wp-content/uploads/2011/07/tree4_thumb.jpg" width="244" height="227" /></a></p>
<p>这个交换启发了我们，其实前面一开始说的交换两个叶子节点<em>可以推广为交换内部节点和叶子节点</em>，然后很快我们就会意识到其实可以推广到交换任意两个节点。（注意，<em>当我们说交换内部节点的时候，其实是连同该内部节点作为局部根节点的整个子树都交换过去</em>）于是前面我们的推论就可以推广为：</p>
<ul>
<li><em>在最优霍夫曼树中，无论互换哪两个节点，得到的树都变得更“差”（交换内部节点则是连同该内部节点作为局部根的子树一同带走）</em> </li>
</ul>
<p>这个推论很容易理解，只不过是多增加了一种“编辑”最优霍夫曼树的方法罢了（记住<em>最优霍夫曼树无论怎么“编辑”都不会变得更“好”，包括“交换子树”这种“编辑”</em>），我们前面没有想到这种“编辑”方法是因为它不那么显然，而且当时我们已经想到一种最直接的“编辑”方法了，即交换叶子，就容易顺着那个思路一直走下去，直到我们发现必须寻找新的性质，才回过头来看看有没有其他法子。</p>
<p>当然，并不排除一开始就想到这种推广的可能性，问题求解的过程并不是这么线性的，如果我们习惯了推而广之的思维，也许一下就能想到这个推广来。类似的，也不排除从另一种思路出发想到这种推广的可能性。所以这里只是可能的思维轨迹中的一种，重点在于其中并没有某处忽然出现一个不知从哪里冒出来的，神启一般的结论。</p>
<p>刚才提到，构造最优树的第二步是考虑第三小的叶子，但也有另一种常见的思维：考虑到第一步（即选取频率最小的两个叶子）所做的事情是从N个叶子中选择两个黏在一起作为兄弟，那么也许对于一些人来说自然而然的第二步就是试图继续选取两个节点黏在一起作为兄弟（注意这里不仅可以选择叶子，也可以选择已经生成的内部节点），然后依次类推来拼完整棵树。按照这一思路，第二步的选项仍然还是集中在第三小的叶子上，因为这个选择要么是让第三第四小的叶子结拜为兄弟，要么是让最小两个叶子的父节点和第三小的叶子结拜。</p>
<p>回到刚才我们的推论：在最优霍夫曼树中，无论互换哪两个节点，得到的树都变得更“差”（交换内部节点则是连同该内部节点作为局部根的子树一同带走） 。根据这个推论我们容易计算出，<em>在最优霍夫曼树当中，两个内部节点n1和n2，如果n1比n2更深，那么n1下面的所有叶子的频率之和必然要小于n2下面所有叶子的频率之和</em>。如果交换的是一个内部节点和一个叶子节点，则道理是类似的。这个性质的证明和上面的类似，就不赘述了。</p>
<p>这个性质暗示了一个重要的推广结论：<em>如果我们把每个内部节点的所有叶子的频率之和标在它旁边，那么整棵树的每个节点便都有了一个数值，这个数值遵循统一的规律：即越往深层越小</em>。这就意味着，我们刚才的二选一困境有办法了！当我们将最小的两个叶子f1和f2合并的时候，生成了一个新的节点M，这个节点有一个数字（为两个叶子的频率之和f1+f2），根据上面的推论，<em>这个数字f1+f2跟所有频率一同，遵循最小的在最底层的原则</em>，所以我们下一步必须在剩下的那些互相之间关系待确定的节点（叶子节点和内部节点）之中，即{(f1 + f2), f3, f4}里面选择最小的两个数字结合成兄弟（由于f1和f2这两个节点已经铁板钉钉结为整体了，所以从集合里面可以看做移除）。到这里，我们就发现递归已经出现了，接下去的过程对于绝大多数人应该就真的很显然了。</p>
<p>以上的解释，比《Algorithms》更简短吗？显然不是。反而要长得多（其实真正的思维过程比这要更长，因为中间还会涉及各种不成功的尝试）。但是它比《Algorithms》当中的版本更不容易被忘记，因为其中关键的思维拐点并不是毫无来由的，而是从你已经熟知的，或者说虽然不知道，但容易理解的一般性解题法则出发自然推导出来的，所以你基本上不需要记忆什么东西，因为你需要记的已经在你脑海中了。</p>
<p>在上面的证明过程中，还有一个不像看上去那么显然的事情：在我们寻找最优霍夫曼树的时候，我们曾经试图去比较假想的最优树和它的“临近”的树，从而去探索最优树的性质。但是，究竟什么是临近的树？在前面的讲解中，我们说如果交换A和B这两个叶子节点，便得到一颗不同的树，可以看做和原树的“编辑距离”为1的树。但是，真的这么显然么？难道除了交换叶子的位置，就没有其他办法去“折腾”这棵树了？后来我们看到，可以<em>交换子树</em>而不仅仅是叶子，而交换子树让我们得到了至关重要的推论。此外，如果不是交换，而是像AVL树那样“旋转”呢？说到底，二叉树是一个离散的东西，并不像连续值那样，天生就有“距离”这个概念，如果我们离散而孤立地去看待所有的树，那么没有什么临近不临近的，临近本是一个距离的概念，除非我们定义树和树之间的距离函数，才能说临近与否，而距离函数怎么定义才是“显然”的呢？</p>
<p>还有，其实以上只是试图给出最优霍夫曼树的<em>证明</em>的一个更自然的过程，而<em>当年霍夫曼面临这个问题的时候根本还没有人想到要用二叉树呢！更不要说在二叉树的前提之下进行证明了</em>。根据wikipedia的介绍，霍夫曼同学（当年还在读Ph.D，所以的确是“同学”，而这个问题是坑爹的导师Robert M. Fano<a href="http://www.huffmancoding.com/my-family/my-uncle">给他们作为大作业的</a>，Fano自己和Shannon合作给出了一个suboptimal的编码方案，为得不到optimal的方案而寝食难安，情急之下便死马当活马医扔给他的学生们了）当年为这个问题憔悴了一个学期，最后就快到deadline的时候“忽然”想到二叉树这个等价模型，然后在这个模型下三下五除二就搞定了一篇流芳千古的论文，超越了其导师。</p>
<p>最后说两个有趣的现象：也许很多人会觉得，越是大师来写入门教科书越是好，其实很多时候并非如此，尤其是在算法设计和数学领域，往往越是在其中浸淫久了越是难写出贴近初学者的书，因为大量对初学者来说一点都不显然的事情在他看来已经是“不假思索”了，成了他的内隐记忆，尤其是当他想要和你解释一个复杂的东西的时候你就会发现他会常常逻辑跳跃，满嘴跑术语，根本没有意识到别人对有些术语和隐含的逻辑根本没有像他那样的理解。</p>
<p><strong>最适合将一个东西讲给别人听的时候并不是等懂了很多年以后，而是刚刚弄懂的时候</strong>，这个时候从不懂到懂的差别记忆还非常鲜明，能够清清楚楚地记得到底是哪些关键的地方是最折磨人的，也最能够站在不懂者的角度来思考问题。像波利亚这样，成了大师还能够站在不懂者角度去换位思考的，可以说是凤毛麟角。所以说前Amazon CAO（首席算法官）的<a href="http://www.amazon.com/Introduction-Algorithms-Creative-Udi-Manber/dp/0201120372/">《Introduction to Algorithms: a Creative Approach》</a>绝对是本罕见的好算法书）</p>
<p><a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/">知其所以然（一）</a>里面曾经提到，要弄清来龙去脉，最好去看看原始作者是怎么想的，可是正如上文所说，即便是最初的发明者，在讲述的时候也会有意无意地“线性化”，我就去查看了霍夫曼最初的论文，那叫一个费解，不信你可以自己看看(<a href="http://compression.ru/download/articles/huff/huffman_1952_minimum-redundancy-codes.pdf">PDF</a>)。</p>
<p>可以归约为搜索算法的问题（非常多）一般来说相对还是有一些头绪的，因为搜索空间一般还比较容易界定，难点在于要从问题的条件中推导出用于节省搜索的性质。而策略设计问题则完全是另一个世界，因为策略的设计空间貌似是可列无穷的，常常让人感觉无从下手，摸不着头绪，许多让人挠头的智力问题就有这个特点（例如著名的100个囚徒和1个灯泡的房间就让很多人有这种感觉），策略设计问题也有一些较通用的法则，以后再说。</p>
<p>怎么才能在学算法的时候学到背后的东西呢？有以下几点很重要：</p>
<ol>
<li><strong>不要觉得每个步骤都很显然</strong>，每个nontrivial的算法背后都有一段艰辛的探索经历，觉得显然的话必然是一种幻觉。<strong>Stay foolish</strong>，才能发现某些环节其实并不是那么显然的。 </li>
<li>检验是否真正理解的最佳方法就是<strong>过一段时间之后，自己试着证明一次</strong>。如果真正理解了的话，你的证明便会比较顺畅。如果当时没有真正理解，那么凡是那些你当时觉得显然但其实不显然的地方，都会成为你证明里面缺失的环节。 </li>
<li>对于一个算法，<strong>多寻找各种来源的资料</strong>，也许能够找到一个讲的比较深刻的。我在<a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/">《数学之美番外篇：快排为什么那么快》</a>和<a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/">《知其所以然（一）》</a>里面都举到了这样的例子。 </li>
<li><strong>多试着去抽象背后的一般性法则</strong>，即便后来发现抽象得是错的，也比不去抽象要好。<a href="http://mindhacks.cn/2009/03/28/effective-learning-and-memorization/">抽象是推广的基础</a>。只有抽象出更深层的法则，才能让你事半功倍，触类旁通，否则一个萝卜永远是一个坑。（注意，其实我们的下意识是会进行一定程度的抽象的，例如前面提到的原始人的例子，小溪和小河（或者小沟）细节上是不同的，但本质上是一样的，我们的大脑会自动进行这种简单抽象，提出事物的共性。正因此，即便你不去有意识地总结一般规律，只要你看的足够多，练的足够多，必然就会越来越谙熟。） </li>
</ol>
<p>最后留个问题：虽然按照上文的方式来构造霍夫曼树一定能够得到<em>一个</em>最优树，但是怎么证明一定能得到呢？乍一看这个问题似乎很多余，因为证明很简单：我们拼装整棵树的每一步都没得选，而且每一步都必然拼凑出最优树的一个小小局部，如果最终还没有得到最优树的话，只能说最优树是不存在的了，然而最优树是一定存在的，因为所有树的集合是有穷的，有穷集必有最值，因此证毕。这个证明固然是没问题的，但它其实是一个间接证明，换句话说，我们在构建树的过程中的逻辑是这样的：“之所以我们选择粘结n1和n2，是因为其他粘法必然违反最优树的两个性质。所以我们别无选择。”但是，这并没有说，我们选择了粘结n1和n2，一定就<em>符合</em>了最优树的性质。（也就是说“其他做法都是错”并不能推出“这种做法必然对”，这就像是你在一大堆豆子当中寻找一个特殊的豆子，你拿起一个，看看不是，扔掉，又拿起一个，还不是，扔掉，排除到最后只剩一个豆子了，假设你又知道这个特殊的豆子必然存在，那么这个时候你根本不用看就知道这个豆子一定就是你要找的）那么，你能否直接证明，拼装最优树的过程每一步都<em>符合</em>最优树的性质呢？</p>
<hr />
<p>P.S.</p>
<p>[1] 《逃出你的肖申克》和《BetterExplained》是我喜欢的两个系列，还会继续写，我有很多问题，也在Evernote里面记了不少零碎的思考和资料，但只有当我觉得理解的足够深入，系统，以及手头有足够的有意思和有说服力的例子的时候，我才会把整条线串起来成文，所以这事慢慢来不着急，反正这个博客也不会关掉。</p>
<p>[2] 工作之后可用业余时间急剧减少，已经陆续基本把GReader砍掉了，时间再往前推，砍掉邮件列表，再往前是Twitter，再往前是BBS。现在基本就只剩邮件了。越来越发现当时间有限的时候，看书比看网要有效得多，也不会那么信息焦虑，网络上的那些消息当中真正重要的会自己来找你，不用每天去刷屏。不过有个例外，我过一阵子就会去逛一下Amazon的个性化推荐项目。如果你已经工作，苦于时间有限，我建议你这么做。最近看过的几本值得好好推荐的书有：《Number Sense》，《Reading in the Brain》，《The Vision Revolution》，《The Tell-Tale Brain》，《Kluge》。</p>
<p>[3] 顺便吐槽国内出版社引进Pop Science类书籍的效率和质量，就我观察，台湾引进Pop Science类书籍需要延迟两年左右，大陆则从三四年到无限期不等（某种程度上，一个国家的出版方的认识水平，决定了这个国家大众的认识水平。你去看下我在豆瓣的书单就知道有多少好书与国内读者失之交臂了），例如《Number Sense》这本好书，到现在还没有引进，99年出版的书啊。《Kluge》更是译为《乱乱脑》这种坑爹的书名，封面搞得跟少儿读物一样。《Reading in the Brain》引入的算较快的，但也延迟了一年半了，而且翻译质量也不是很上乘（算是不功不过吧），说到这里要赞中信出版社，最近一年引入了很多给力的Pop Science畅销书，眼光还算不错。最近在Amazon上搜一些好的发展心理学的书，通过Amazon的推荐引擎看到了<a href="http://www.amazon.com/Pink-Brain-Blue-Differences-Troublesome/dp/0618393110">《Pink Brain，Blue Brain》</a>，这本受到因研究大脑记忆的分子机制而获诺奖的Eric Kandel盛赞的科普09年就出了，到现在国内影子都见不着，还好在卓越上买到了原版。虽然基本还没开始看，但可以郑重推荐给初为父母的同学们:)</p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/" title="知其所以然（续）">知其所以然（续）</a> (42)</li><li><a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/" title="知其所以然（以算法学习为例）">知其所以然（以算法学习为例）</a> (36)</li><li><a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/" title="数学之美番外篇：快排为什么那样快">数学之美番外篇：快排为什么那样快</a> (23)</li><li><a href="http://mindhacks.cn/2008/04/18/learning-from-polya/" title="跟波利亚学解题(rev#3)">跟波利亚学解题(rev#3)</a> (21)</li><li><a href="http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/" title="数学之美番外篇：进化论中的概率论">数学之美番外篇：进化论中的概率论</a> (13)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2011. | <a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/#commenting">57 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e7%ae%97%e6%b3%95/" rel="tag">算法</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/">原始超链接</a>: http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/feed/</wfw:commentRss>
		<slash:comments>57</slash:comments>
		</item>
		<item>
		<title>知其所以然（续）</title>
		<link>http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/</link>
		<comments>http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/#comments</comments>
		<pubDate>Sun, 14 Nov 2010 09:41:00 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[学习方法]]></category>
		<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2010/11/14/%e7%9f%a5%e5%85%b6%e6%89%80%e4%bb%a5%e7%84%b6%ef%bc%88%e7%bb%ad%ef%bc%89/</guid>
		<description><![CDATA[我有一个习惯，看定理必看证明。一个你不明白其证明的定理在我看来比不知道这个定理还要糟糕，因它给你造成一种懂了的错觉。在没有明白背后的证明之前，任何一个定理对你来说都是等价的——等价于背乘法口诀（只不过有的长一点有的短一点）。一个原本美妙的定理，把其证明扔掉就是真正的买椟还珠，暴殄天物。 

从现实意义来说，去理解一个定理的证明会带来巨大的好处，首当其冲的好处就是你很难再忘掉它。这一点其实很容易解释——在理解一个定理的证明之前，定理对你而言是一堆没有内在联系的词句，而在理解了证明之后，定理就归约为证明它所需的条件加上逻辑...这是一个树状的知识结构，越往上层走，需要记忆的节点就越少。]]></description>
			<content:encoded><![CDATA[<p>查了一下，上篇<a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/">知其所以然（以学习算法为例）</a>是08年7月写的，现在已经是10年11月，过去了两年零4个月，这说明了三件事情：1，一个问题其实你可以一直放在脑子里面，利用<a href="http://mindhacks.cn/2009/12/20/dark-time/">暗时间</a>对其软泡硬磨，时间足够久你总会有一点新的感悟，问题其实就像那句老话说的那样，不怕贼偷就怕贼惦记，聚精会神的思考一天，也许比不上惦记一个星期（据说<a href="http://blog.csdn.net/pongba/archive/2008/07/24/2705151.aspx">数学家庞加莱就特别会惦记问题</a>）。2，事实上，当你感觉懂了的时候，你至少得反问自己一句，真的懂了吗？当你确信自己真的懂了的时候，你至少得讲给别人听，别人听懂了吗？考察你自己是否真懂了的一个很好的依据是，你是否有一种“哦，原来是这样啊，这下再也不可能忘记了”的感觉。3，我其实没有忘记这个博客。如我之前说的，<a href="http://mindhacks.cn/2009/02/15/why-you-should-start-blogging-now/">记录只是学习和思考的副作用</a>，只要还在学习和思考，就必然会有新的记录。 </p>
<p>我有一个习惯，看定理必看证明。一个你不明白其证明的定理在我看来比不知道这个定理还要糟糕，因它给你造成一种懂了的错觉。<strong>在没有明白背后的证明之前，任何一个定理对你来说都是等价的——等价于背乘法口诀</strong>（只不过有的长一点有的短一点）。一个原本美妙的定理，把其证明扔掉就是真正的买椟还珠，暴殄天物。 </p>
<p>从现实意义来说，<strong>去理解一个定理的证明会带来巨大的好处，首当其冲的好处就是你很难再忘掉它</strong>。这一点其实很容易解释——在理解一个定理的证明之前，定理对你而言是一堆没有内在联系的词句，而在理解了证明之后，定理就<strong>归约为</strong>证明它所需的条件加上逻辑，“逻辑”本来就存在于你的大脑里面，而证明的过程中除了公理和用到的常见定理（往往没几条）之外，宽泛地说，需要你去记的，一般来说也只有一个或两个关键的insights，也就是我们常说的证明中的神来之笔，比如几何证明里面的某条看上去莫名其妙的辅助线，一旦你知道了这条辅助线，那么整个证明就毫无难处，那么该定理的信息量便直接缩减为一条辅助线的信息量；虽然看上去这一步信息并没有缩减多少，但是如果你考虑到类似的辅助线不仅会用在这个特定的定理上，往往会在很多地方用到。很多关键的证明手法是通用的。那么其实你就是把所有以这个辅助线为关键证明手法的定理的集合的信息量归约为了这条辅助线。如果你进而甚至能够理解了作这条辅助线的思想精髓，那就更牛逼了，因为解决问题的思路更具有一般性，理解了寻找正确的辅助线的思路，你就根本不需要去记得某条特定辅助线的作法，你就把所有以作一条或几条辅助线为证明核心的定理的集合的信息量归约为了这个“寻找辅助线的思路”。 </p>
<p><strong>这是一个树状的知识结构，越往上层走，需要记忆的节点就越少</strong>。所谓触类旁通者，其实便是因为他擅长去理解解法背后的更具一般性的东西。所以我还有一个习惯，就是看到美妙的证明和解法总是会去一遍又一遍的去反复揣摩，试图理解想出这个证明的人到底是怎么想出来的，有没有什么一般性的方法可循，很多时候，在这样揣摩的过程中，你会理解到更深刻的东西，对问题性质更深刻的认识，对解决问题的思路更深刻的认识，这些认识不仅对于你理解当前这个定理或问题有极大的帮助，同时也有助于你解决以后会遇到的表面不同但本质一样的问题。 </p>
<p>与看定理必看证明类似，看一个问题的解法，必然要看解法所诞生的过程，背后是否隐藏着更具一般性的解决问题的思路和原则。否则一个解法就只是一个问题的解法，跟背口诀一样。即便记住了也无法推广，即便当时记住了也容易遗忘。 </p>
<p>举个经典的例子：每本算法书都会讲动态规划，每本讲动态规划的书都会讲背包问题，每次讲背包问题都会讲可重复背包和01背包，我们就拿《Algorithms》这本<a href="http://book.douban.com/review/1325850/">还算不错</a>的算法书对背包问题的讲解来说吧，重复背包问题的递归公式是这样的： </p>
<p>K(W) = max { K(W-Wi) + Vi : Wi &lt;= W } </p>
<p>这个公式的理解倒是很简单：为了把问题降阶，我们在最终的最优解里面去掉一个元素，对这个元素的可能性进行讨论，它必然是任何Vi之一（前提是Wi &lt;= W，否则就装不下），而在去掉这个元素之后，剩下的元素肯定构成问题 K(W-Wi) 的最优解，于是递归关系出现了。 </p>
<p>此外也可以这样来理解：要拿一组最优元素，那么总得开始一个个拿吧，对第一个拿的元素进行讨论，而问题的最优解等于讨论的各个分支的最优解中的最优者；如果拿掉Vi之后，剩下来要怎么拿才能最优呢？这就是一个 K(W-Wi) 的问题了。 </p>
<p>01背包问题就大不一样了——每个物品都只有一件，拿掉之后就不能再拿了。我们不妨看看重复背包问题的解法是不是能用到01背包上呢？还是讨论第一个拿的元素，设被拿掉的是第i个元素，问题就归结为把剩下的物品（注意，可拿的物品少了一件）最优地装入容量为 W-Wi 的包里，所以，问题的参数便变成了两个，一个是背包剩余容量 W-Wi，另一个是剩余可拿的物品集合 S\{i} （表示去掉i之后的子集），显而易见第二个参数是物品集合的各种可能的子集，那么其可能性个数就是 2^n ，这就导致子问题的个数是 2^n， 由于要依次计算每个子问题，那么算法复杂度显然也是 2^n ，是不可接受的。 </p>
<p>那么，《Algorithms》上又是怎么来讲解01背包问题的解法的呢？以下是原文： </p>
<p>Our earlier subproblems now become completely useless. We must therefore refine our concept of a subproblem to carry additional information about the items being used. We add a second parameter, 0 &lt;= j &lt;= n: K(W, j) = maximum value achievable using a knapsack of capacity w and items 1..j: The answer we seek is K(W, n). </p>
<p>首先作者说了，之前重复背包问题的解法在这里完全废掉了，所以我们必须重新定义子问题，并且子问题的条件必须要包含目前拿剩下的物品。以上这些都还不错，关键是接下来就让人吐血了。作者接着说道，<strong>我们</strong>给子问题加上一个新的参数j&#8230; </p>
<p>凭什么啊？ </p>
<p>还是让我们回顾一下<a href="http://images.google.com/images?hl=en&amp;source=imghp&amp;biw=1024&amp;bih=620&amp;q=then+a+miracle+occurs&amp;gbv=2&amp;aq=f&amp;aqi=g3g-m6&amp;aql=&amp;oq=&amp;gs_rfai=">这样一幅经典的漫画</a>吧： </p>
<p><img style="margin: 5px 0px 10px" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image003.gif" /> </p>
<p>“我们给子问题加上一个参数j”，这就像你在看数学证明时看到无比邪恶的“<strong>我们考虑</strong>…“一样，一看到这样的句子，你就知道，这个问题的证明远远不像看上去那么简单，之所以你一路看下去理解上全无困难，那完全是因为作者直接把最重要的一个insight告诉你了，举个很简单的例子，证明素数无最大，谁都会第一时间想到去反证：假设存在一个最大的素数P，那么找到比P大的素数就是证明中最关键的一步，怎么找的？一般书上是不会说的，你会看到书上这样说：假设P是最大的素数，那么我们考虑P’ = 小于等于P的所有素数的乘积+1。那么P’一来显然大于P，二来不能被小于它的所有素数整除，那么P&#8217;就成了大于P的素数。 </p>
<p>如果你经常注意反证法，你会发现一个有趣的现象，反证法里面经常会有这样一句“我们考虑”，而“我们考虑”后面几乎肯定接着一个天外飞仙一般的insight。素数无最大这个古老的证明里面的“我们考虑”尚算是比较有迹可循的（我们想要构造一个更大的素数，而素数的等价定义就是“不能被小于它的所有素数整除，为了达到这个目的，构造的方法就较明显了）。但是有非常非常多的证明，其中关键的一步就跟嗑药磕出来做梦做出来走路跌跟头跌出来的一样（不信去翻一翻《<a href="http://www.amazon.com/Proofs-BOOK-Martin-Aigner/dp/3540404600">Proofs from THE Book</a>》），让你完全不知道他怎么想到的。 </p>
<p>话说回来，虽然有很多数学证明的关键步骤是很难逆向工程的（因为很多时候想出那个关键步骤的本人其实也是尝试了各种方法，撞了无数堵墙，在寻求证法的尝试空间中作了N次回溯才“妙手偶得”，与其说是妙手偶得，不如说是绞尽脑汁），但并非全无章法可循，否则陶哲轩也不会写出《<a href="http://www.amazon.com/Solving-Mathematical-Problems-Personal-Perspective/dp/0199205604">Solving Mathematical Problems</a>》这样的著作来，而求解问题也就成了真正的Black Art了。</p>
<p>算法的解法则比精妙的数学证明稍加更容易逆向工程一点。只要你有耐心仔细地去琢磨算法的关键步骤和本质，总能从中窥探到一些更general的思想和思路来。 </p>
<p>此外，很多经典问题，算法书上的讲法虽然时时令我们失望，但如果去网上一搜，则通常会发现更优秀的解释来。比如<a href="http://www.google.com/webhp?hl=en#sclient=psy&amp;hl=en&amp;site=webhp&amp;source=hp&amp;q=%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98+%E8%A7%A3%E7%A9%BA%E9%97%B4&amp;aq=f&amp;aqi=&amp;aql=&amp;oq=&amp;gs_rfai=&amp;pbx=1&amp;fp=ab43752bbefd1914">背包问题就是如此</a>。</p>
<p>简单地说，如果你对于每个问题都能真正弄清以下这几个问题的答案，那么可以肯定的是，你的理解，记忆，以及学习的效率都会得到质的提高： </p>
<ul>
<li>为什么这种解法是对的？ </li>
<li>为什么那种解法是错的？ </li>
<li>为什么这种解法不是最优的？ </li>
<li>证明为什么没有更优的解法。 </li>
</ul>
<p>回到人民群众喜闻乐见的经典例子：背包问题。为什么01背包问题的正确（高效）算法是正确（高效）的。表面的解释是，因为01背包问题的子问题定义是 K(W, j)，其两个维度相乘的可能性一共有nW种，也就是说一共要计算nW个子问题，而计算每个子问题的复杂度是O(1)的。</p>
<p>但是如果仅仅满足于这样的解释，可以说是隔靴搔痒，并没有触及到本质。算法本质上可以看做是在一个解空间当中的搜索问题，所以要分析一个算法的好坏，首先弄清它的解空间的结构，然后分析它是怎么来探索这个解空间的。</p>
<p>弄清解空间的是第一步，例如排序算法，其解空间可以看做是所有可能的下标排列组合，其中有且仅有一个排列是正确的排序排列（简单起见假设元素各不相同）。那么一个算法在探索这个解空间方面的行为就决定了它的效率高低，最简单的，如果一个算法每次只能检查解空间中的一个点，那么这个算法的复杂度就是解空间的大小。对排序算法而言也就是n!。从这个角度来看，我们就会很容易的发现，所有基于比较的排序算法，其复杂度为什么是以O(nlogn)为下界的，因为一次比较操作最多有两个结果，a&gt;b或a&lt;b，既然只有两种结果，那么最多只能将解空间进行2分，如果每次都能完美的2分，那么找到那个唯一点最终需要的步骤就是log(n!) = O(nlogn)。如此就不难理解<a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/">什么基于比较的排序算法的复杂度最好不过如此了</a>。</p>
<p>回到01背包问题，01背包问题的解空间其实也是类似的。一次选取就是一个01数组，其中每个元素代表其所对应的物品要不要选取。很显然，这个解空间的大小是2^n。在01背包的算法里面，每当我们解出K(W, j)（需要O(W)次计算）之后，解空间就会被折半（排除掉1/2的可能性），一共如此做n次，就能得到最终解。由于每次折半的代价是O(W)，便不难理解为什么算法复杂度是O(nW)了。</p>
<p>那么，为什么每次计算出K(W,j)就能使解空间折半呢？那就需要来看看这个算法是如何探索解空间的，算法探索解空间的方式在其递归公式里面：</p>
<p>K(W, j) = max { K(W, j-1), K(W-Wj, j &#8211; 1)&#160; + Vj }</p>
<p>也就是说，首先看你要不要选取第一个物品，有两种可能性（两个分支），每个分支都是一个更低阶的子问题，即在其中的任意一个分支下都要决定要不要选取第二个物品（又是两个分支），如此下递归去，可以构建出一棵有2^n方个叶子节点的树，每条从根结点到叶子节点的路径“01..101”就对应一个解，其中每个分叉代表“选”或“不选”当前的物品。</p>
<p>建立在对这个解空间的理解上，我们再来看为什么01背包问题的正确解法能做到O(nW)。（首先你最好将这棵树画在纸上，其中每个节点都是一个子问题K(W,j)，每条分叉都是0或1。）当我们计算出所有的K(W, 1)（需要O(W)次操作）之后，我们容易注意到，所有离叶子节点的距离为1的内部节点K(W, 2)到叶子节点的两个分支都必然只能取其一了，也就是说，有一半的叶子节点被排除掉了（对解空间折半）。当我们进而计算出K(W,2)之后，同样的道理，我们容易看到，到叶子节点距离为2的内部节点的两个分支也只能取其一了，这就进而再次将解空间折半。由于每次折半需要O(W)的复杂度，所以就不难理解算法的总复杂度为O(nW)了。另一种理解的方法是，当我们计算出K(W,j)的时候，从内部节点K(W,j)到根节点的唯一路径便确定了。经过O(nW)次计算，从根节点到那个唯一解（叶子节点）的路径便完全确定了。</p>
<p><strong>知道怎么做是从正确（高效）解法得到的，而知道为什么必须得那样做则往往是从错误（低效）的解法当中得到的。</strong></p>
<p>然而遗憾的是，绝大多数算法书或教程都只顾一上来就告诉你正确的做法是什么，对于一些常见的错误解法，或者常见的低效解法，却根本不加分析。经验告诉我们，理解错误的做法为什么错误同样甚至更为重要，往往是在理解了错误的解法为什么错误之后，我们才能深刻的体会到为什么正确的解法是如此正确。</p>
<p>还是拿经典的背包问题来作例子，你几乎看不到哪本书会告诉你一个典型的低效解法为什么低效的深刻原因。我们都知道动态规划的核心在于子问题的划分，同样的问题，不同的划分办法得到的复杂度完全不一样。前面已经提到了，重复背包问题的思路在01背包问题上会带来指数级的复杂度，但是为什么呢？如果你满足于说：因为如果拿重复背包问题的思路来解01背包问题，那么子问题定义的第二个维度（物品的子集）（见前文）是指数级的，那么要计算所有子问题，当然是指数级的。那么你只是看到这个问题的表象。</p>
<p>如果从对解空间的探索方式来说，可以容易看出这个现象的本质，我们回顾一下01背包问题的正确（高效）算法：</p>
<p>K(W, j) = max { K(W, j-1), K(W-Wj, j &#8211; 1)&#160; + Vj }</p>
<p>这个算法讨论的是两种情况，“要”或者“不要”选取第j个物品，这两种情况所对应的解空间是完全不交的，这就有效地将解空间划分为了不重复的两个部分。</p>
<p>而再来看利用重复背包问题思路的解法：</p>
<p>K(W, S) = max { K(W-Wi, S\{i}) + Vi : Wi &lt;= W } </p>
<p>这里讨论的是首先拿掉哪一个物品，还是那句话，讨论的每一个分支都对应了算法对解空间的一个切分，我们容易看出，在“先拿物品i”和”先拿物品j“这两个分支里面，存在大量的重复，因为先拿物品i再拿j，和先拿物品j再拿i对应的是完全一样的一组选取。事实上，如果你将这个递归公式画成树状结构，会发现有n!个叶子节点。n!是什么概念？01背包问题的解空间大小本质上就只有2^n次方，穷举也不过O(2^n)的复杂度，结果这样一切分却变成了n!，可见这种对解空间的切分方法的冗余度是多么高了。你不妨看看，每一次计算K(W, S)子问题能对解空间排查多少呢？是否能像前面正确的算法那样，每次都能有效排查一半情况？理解了这一点之后，我们便注意到在划分解空间，也就是定义子问题的时候的一个原则，就是在建立递归公式的时候，尽量将解空间进行不交的切分。同时我们便有了趁手的工具去分析一个动态规划的解法的效率。</p>
<p>最后再举一个例子：算法书上几乎必讲的霍夫曼树。你所看的算法书在讲霍夫曼树的时候给了证明吗？讲过霍夫曼树的<a href="http://en.wikipedia.org/wiki/Huffman_tree#History">历史八卦</a>吗？也许你看了霍夫曼树的构造方法之后觉得：“哦，这样啊，显然”。但是你可曾想到，在最优编码这个问题上，连<a href="http://en.wikipedia.org/wiki/Shannon-Fano_coding">香农本人之前给出的解法</a>都只是suboptimal的，而且霍夫曼本人在得到这个算法之前也是绞尽脑汁几近放弃。<strong>如果你10分钟就“理解”了，那么百分之百只是背了课文而已。</strong></p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/" title="知其所以然（以算法学习为例）">知其所以然（以算法学习为例）</a> (36)</li><li><a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/" title="知其所以然（三）：为什么算法这么难？">知其所以然（三）：为什么算法这么难？</a> (57)</li><li><a href="http://mindhacks.cn/2009/12/20/dark-time/" title="暗时间">暗时间</a> (143)</li><li><a href="http://mindhacks.cn/2009/10/05/im-a-tiny-bird-book-review/" title="不是书评 ：《我是一只IT小小鸟》">不是书评 ：《我是一只IT小小鸟》</a> (57)</li><li><a href="http://mindhacks.cn/2009/07/06/why-you-should-do-it-yourself/" title="[BetterExplained]遇到问题为什么应该自己动手">[BetterExplained]遇到问题为什么应该自己动手</a> (62)</li><li><a href="http://mindhacks.cn/2009/05/17/seven-years-in-nju/" title="我在南大的七年">我在南大的七年</a> (147)</li><li><a href="http://mindhacks.cn/2009/03/28/effective-learning-and-memorization/" title="[BetterExplained]如何有效地记忆与学习">[BetterExplained]如何有效地记忆与学习</a> (102)</li><li><a href="http://mindhacks.cn/2009/02/15/why-you-should-start-blogging-now/" title="[BetterExplained]为什么你应该（从现在开始就）写博客">[BetterExplained]为什么你应该（从现在开始就）写博客</a> (200)</li><li><a href="http://mindhacks.cn/2009/02/09/writing-is-better-thinking/" title="[BetterExplained]书写是为了更好的思考">[BetterExplained]书写是为了更好的思考</a> (90)</li><li><a href="http://mindhacks.cn/2009/01/14/make-yourself-irreplacable/" title="什么才是你的不可替代性和核心竞争力">什么才是你的不可替代性和核心竞争力</a> (25)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2010. | <a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/#commenting">42 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e5%ad%a6%e4%b9%a0%e6%96%b9%e6%b3%95/" rel="tag">学习方法</a>, <a href="http://mindhacks.cn/tags/%e7%ae%97%e6%b3%95/" rel="tag">算法</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/">原始超链接</a>: http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/feed/</wfw:commentRss>
		<slash:comments>42</slash:comments>
		</item>
		<item>
		<title>知其所以然（以算法学习为例）</title>
		<link>http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/</link>
		<comments>http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/#comments</comments>
		<pubDate>Mon, 07 Jul 2008 13:05:00 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[学习方法]]></category>
		<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/</guid>
		<description><![CDATA[其实下文的绝大部分内容对所有学习都是同理的。只不过最近在正儿巴经地学算法，而后者又不是好啃的骨头，所以平时思考总结得就自然要比学其它东西要多一些。

问题：目前几乎所有的算法书的讲解方式都是欧几里德式的、瀑布式的、自上而下的、每一个推导步骤都是精准制导直接面向目标的。由因到果，定义、引理、定理、证明一样不少，井井有条一丝不乱毫无赘肉。而实际上，这完全把人类大脑创造发明的步骤给反过来了。看起来是阳关大道，实际上车马不通。]]></description>
			<content:encoded><![CDATA[<p><strong>Updated(2008-7-24)</strong><strong>：更新见正文部分，有标注。</strong></p>
<p>其实下文的绝大部分内容对所有学习都是同理的。只不过最近在<a href="http://blog.csdn.net/pongba/archive/2008/06/05/2513263.aspx">正儿巴经地</a>学算法，而后者又不是好啃的骨头，所以平时思考总结得就自然要比学其它东西要多一些。</p>
<p>问题：目前几乎所有的算法书的讲解方式都是欧几里德式的、瀑布式的、自上而下的、每一个推导步骤都是精准制导直接面向目标的。由因到果，定义、引理、定理、证明一样不少，井井有条一丝不乱毫无赘肉。而实际上，这完全把人类大脑创造发明的步骤给反过来了。看起来是阳关大道，实际上车马不通。</p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image001.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="clip_image001" border="0" alt="clip_image001" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image001-thumb.jpg" width="244" height="166" /></a></p>
<p>而对读者来说，这就<strong>等于直接告诉你答案&amp;做法了</strong>，然后<strong>让你去验证</strong>这个答案&amp;做法是可行&amp;成立的。而关于答案&amp;做法到底是怎么来的，从问题到答案之间经历了怎样的思维过程。却鲜有书能够很好的阐释。就我有限的阅（算法）书经验，除了波利亚的《怎样解题》还算合格之外（也并非最理想），其它的（包括有名的《算法导论》、《如何解题：现代启发式方法》、《Algorithms》、《编程珠玑》，甚至TAOCP——公平地说由于高老大对算法领域历史了解得非常通透，所以许多地方能够从原始脉络来讲述一个问题，譬如令人印象深刻的从竞赛树到堆的讲解就寥寥一页纸道出了堆这个数据结构的本质来，而像刚才列的几本有名的书却都没有做到），在思维的讲述上都算不上合格<strong>（当然不是说这些书没有价值，作为知识性的参考书籍，它们将知识整理出系统结构，极大的便利了知识的掌握，就像《什么是数学》所做的工作一样）</strong>，为什么我这么说呢，因为我发现每每需要寻找对一个算法的解释的时候，翻开这些书，总是直接就看到关于算法逻辑的描述，却看不到整个算法的诞生过程背后的思想。</p>
<p><strong>我们要的不是相对论，而是诞生相对论的那个大脑。我们要的不是金蛋，而是下金蛋的那只鸡。</strong></p>
<p><strong><i>Update(2008-7-24):</i></strong><em> </em><em>收到不少同学的批评，想来这个开头对一些著作的语气过重了，实际上，注意，</em><strong><i>我完全不否认这些著作的价值，我自己也在通过阅读它们来学习算法，并且有很多收获</i></strong><em>。这篇文章更多的只是建议</em><strong><i>除了阅读这些著作之外还需要做的功课</i></strong><em>。此外，对于这类知识讲述（欧几里德）方式的批判西方（尤其是在数学领域）早就有了，早在欧拉和庞加莱的时候，他们俩就极其强调思维的传授，欧拉认为如果不能传授思维，那数学教学是没意义的。而庞加莱本人则更是对数学思维有极大的兴趣和研究（</em><strong><i>我前阵子在讨论组上还转载了一篇庞加莱的著名演讲，就是说这个的，参见</i></strong><a href="http://groups.google.com/group/pongba/browse_frm/thread/3dfc84e0506486cc"><strong><i>这里</i></strong></a><em>）。我只是在说目前的算法书没有做到思维讲述的层面，因此建议阅读这些书之余应该寻找算法的原始出处，应该寻根究底，多做一些功课，知道算法到底是怎么诞生的，并且我说明了为什么应该知其所以然，有哪些好处（见下文），我还给了几个例子譬如红黑树作者讲红黑树的，g9讲后缀树的，以及Knuth讲heap的。唉，其实挺正统的观点，授人以渔，不管是东方西方都有类似的古老谚语。而我只是从认知科学的角度加了点解释，windstorm称之为“解释文”。而已。可惜被开头的语气搞砸了，算了，既发了也就不改了。</em></p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image002.gif"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="clip_image002" border="0" alt="clip_image002" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image002-thumb.gif" width="179" height="191" /></a></p>
<p>为什么会这样，其实是有原因的。</p>
<p>我们在思考一个问题的过程中有两种思维形式：</p>
<ul>
<li><strong>联想</strong>：这种思维某种程度上可以说是“混乱”的（虽然从一个更根本的层面上说是有规则的），所谓混乱是指很多时候并不确定联想到的做法最终是否可行，这些联想也许只是基于题目中的某个词语、语法结构、问题的某个切片、一些零星局部的信息。这个过程是试探性的。最后也许有很大一部分被证明是不可行的。很多时候我们解决问题用的都是这种思维，简言之就是首先枚举你关于这个问题能够想到的所有你学过的知识，然后一一往上套看看能否解决手头的问题。这种思维方式受限于人脑联想能力本身的局限性。我在<a href="http://blog.csdn.net/pongba/archive/2008/04/18/2302905.aspx">《跟波利亚学解题》</a>中就提到了几个例子。联想本身需要记忆提取的线索，所以受到记忆提取线索的制约，如果线索不足，那怎么也联想不起来。而提取线索的建立又取决于当初保存记忆的时候的加工方法（<a href="http://www.douban.com/subject/1315575/">《找寻逝去的自我》</a>里面有阐述），同时，面对一个问题，你能够从中抽取出来的联想线索又取决于你对问题的认识层度/抽象深度，表浅的线索很可能是无关的，导致无效的联想&amp;试错（<a href="http://www.douban.com/subject/2845839/">《Psychology of Problem Solving》</a>里面有阐述）。总之，联想这个过程充满了错误的可能。</li>
<li><strong>演绎</strong><strong>&amp;</strong><strong>归纳</strong>：演绎&amp;归纳是另一种思维形式。它们远比联想有根据。其中演绎是严格的，必然的。归纳也是有一定根据的。在面对一个问题的时候，我们有意无意的对问题中的各个条件进行着演绎；譬如福尔摩斯著名的“狗叫”推理——狗+生人=&gt;吠叫 &amp; 昨晚狗没有叫 =&gt; 那个人是熟人。就是一个典型的对问题的各个条件进行演绎的推理过程。还有就是通过对一些特殊形式的观察来进行归纳，试图总结问题中的规律。然而，不幸的是，面对复杂的问题，演绎&amp;归纳也并不总是“直奔”问题的解决方案的。人的思维毕竟只能一下子看到有限的几步逻辑结论，一条逻辑演绎路径是否直奔答案，不走到最后往往是不知道的，只要答案还未出现，我们大脑中的逻辑演绎之树的末端就始终隐藏在黑暗之中。而当最终答案出现了之后，我们会发现，这棵演绎之树的很多分支实际上都并不通往答案。所以，虽然演绎&amp;归纳是一种“必然”的推理，然而却并不“必然”引向问题的结论，它也是试错的，只不过比联想要更为靠谱一些。</li>
</ul>
<p>既然认识到，<strong>人类解决问题的两大思维方式实际上都是有很大的试错成分的</strong>（好听一点叫“探索”），那么就不难意识到，对一个问题的思考过程实际上是相当错综复杂的，而且<strong>充满了无效分支</strong>——在思考的过程中我们也会不断的对分支进行评估，做适当的剪枝——因此当我们找到问题的解之后，<strong>一来思维的漫长繁杂的过程已经在大脑里面淡化得差不多了</strong>，只有那些引向最终结论的过程会被加“高亮”——我们在思考的过程中本就会不断的抛弃无效的思路，只留下最有希望的思路。简而言之就是最后证明没用或者早先我们就不抱希望的一些想法就被从工作记忆中扔掉了。<strong>二来，思考过程是我们的空气和水，而“<a href="http://blog.csdn.net/pongba/archive/2008/01/04/2025830.aspx">鱼是最后一个感觉到水的</a>”</strong>，我们感觉不到思维法则本身的存在，我们只是不知不觉运用它。<strong>三来，由于我们的目标是问题的解</strong>，解才是我们为之兴奋和狂喜的东西，而不是求解的过程，<strong>过程只是过程，目的才是目的</strong>。这就像一个寻宝者，在漫长曲折的寻宝历程之后，在找到宝藏的时候，他会对宝藏感到狂喜（记得阿基米德的“找到了！”吗？）而迫不及待地要展示出来，而漫长的思考本身却成了注脚。我们是有目的的动物，目的达到了，其它的就相对不那么重要了。最后，对于传授知识的人，也许还有<strong>其四：感到介绍思维过程是不相干的</strong>，毕竟思维过程并不是算法问题的解，算法问题的解才是算法问题的解。然而不幸的是，忽视到达解的那个过程实际上却变成了舍本逐末。我们看到的是寥寥数行精妙绝伦的算法，然后仰天长叹自己想不出来啊想不出来。为什么想不出来，因为你不知道那短短数行算法背后经历的事怎样漫长的思考过程，如果问题求解是一部侦探小说，<strong>那么算法只是结局而已，而<a href="http://blog.csdn.net/pongba/archive/2008/05/07/2412144.aspx">思考过程</a>才是情节</strong>。</p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image003.gif"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="clip_image003" border="0" alt="clip_image003" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image003-thumb.gif" width="214" height="244" /></a></p>
<p>既然如此，也就难怪古往今来算法牛人们算法牛，但却没有几个能真正在讲述的时候还原自己的思维过程的（那个“ 渔”字），手把手的教学生走一遍推理的思路，就可以让学生获得思维过程的训练。金出武雄在<a href="http://www.douban.com/subject/1867455/">《像外行一样思考，像专家一样实践》</a>中说<strong>写论文应该写得像侦探小说一样</strong>，我很赞同。欧几里德式的介绍，除了提供枯燥的知识之外，并没有提供帮助人获得知识的东西——思维（关于对数学书籍的欧几里德式写法的批评其实也是由来已久了，并且有人呼吁了好几种其它的<a href="http://en.wikipedia.org/wiki/Mathematics_education#Methods">教学方法</a>）。<strong>从这方面，我们所尊敬的一些“圣经”级书籍在传道授业上还不如侦探小说</strong>，前者是罗列一大堆知识，后者则是阐述获得知识的过程——推理&amp;联想。</p>
<p>然而，我们都是人，人类该有的思维形式，我们难道不是都有吗。既然如此，思维本身又有什么需要一遍遍教的呢？</p>
<p>并非如此。</p>
<p>讲述思维过程而非结果有几个极其重要的价值：</p>
<ul>
<li><strong>内隐化</strong>：思维法则其实也是知识（只不过它是元知识——是帮助我们获得新知识的知识）；是<a href="http://en.wikipedia.org/wiki/Implicit_memory">内隐的记忆</a>。我们在思考的过程中觉察不到思维法则的作用，它们却在幕后实实在在的左右着我们的思维轨迹。要将思维方法内隐化，<a href="http://blog.csdn.net/pongba/archive/2008/06/05/2513263.aspx">需要不断练习</a>，就像需要不断练习才能无意识状态下就能骑自行车一样。</li>
<li><strong>跨情境运用</strong>：思维法则也是知识记忆，是问题解决策略。既然是记忆，就受到提取线索的制约，这就是为什么当波利亚<a href="http://www.douban.com/subject/1456890/">告诉你</a>要“注意未知数”之后你还是不能真正在所有需要你“注意未知数”的地方都能提醒自己“注意未知数”。很多时候未知数是很隐蔽的，未知数并不会总是头顶一个大帽子上面写着“我是未知数”。所以很多时候缺乏对这个策略的“提醒”线索，这也是为什么<strong>你学会了在解决数学问题的时候“</strong><strong>注意未知数”</strong><strong>却不一定能在解决现实生活中的问题中时刻都能“</strong><strong>注意你的未知数”</strong>（<a href="http://www.douban.com/subject/1135754/">《你的灯亮着吗？》</a>整本书的价值便在于此），因为解数学题和解决生活中问题的场景不一样，不同的环境线索，在你大脑中激发的记忆也不一样。就连问题求解中，不同的问题之间的细小差别也可能导致思维轨迹很大的不同，有时你的注意力会被一个无关线索激发的联想吸引开去，忘记如“注意你的未知数”这样的重要法则。而一本从思维角度来讲问题求解的书则可以一遍遍将你置于不同的问题场景下然后在该提醒你的时候提醒你，让你醒悟到“<strong>哦，原来</strong><em><b>这个</b></em><strong>时候也应该想到</strong><em><b>这个</b></em><strong>啊。</strong>”，做多了这样的思维演习你就会逐渐从中领悟到某种共性，并将一些思维习惯得到强化，于是终于能够在需要运用某策略的时候能适时的想起来了。</li>
<li><strong>对问题解的更多</strong><strong><a href="http://blog.csdn.net/pongba/archive/2008/06/05/2513263.aspx">记忆提取线索</a></strong>：我们平时学习算法时几乎仅止于“理解”，别人把一个方案放在你面前，你去验证一下，心说“哦，不错，这个的确可以工作”。然后就没了。稍微简单一点的算法还好，复杂一点的对于记忆的负担是很大的，这就是为什么有时候我们看到一个绝妙的解法，这个解法看上去不知道从哪里来的，但经过我们的理解，却发现是对的，我们感叹，真巧妙，结果一些天之后，别人问起这个问题，我们说：“<strong>唉，那是个多么巧妙的算法啊，但是我只记得它巧妙，却不记得它到底是怎样的了。</strong>” 为什么？因为在不知其所以然的情况下，算法只是一堆离散的机械步骤，缺少背后的思想的支撑，这些步骤之间就没有一个<strong>本质层面上的关联</strong>（先知亚里士多德早就指出：学习即联接）。所以就跟背历史书也没多大区别。然而，<strong>知道了算法是怎样一步步被推导出来的，我们就一下拥有了大量的记忆提取线索：对算法发现过程中的任何一个关键步骤（尤其是本质）的回忆都可能使我们能够自己动手推导出剩余的内容</strong>。<strong>譬如</strong>你知道堆(heap)是怎样由朴素的决策树演化而来的，它又是为了解决什么问题的，你即便忘记了具体的细节，也可以自己推导出来。<strong>譬如</strong>你知道KMP算法的本质在于消除回溯，至于如何消除回溯却并不是那么难以推导的，所以即便忘了也可以借助于大脑的逻辑演绎能力再现出来。<strong>譬如</strong>你知道Tarjan算法其实只是从后序遍历经过两个优化调整而来的（其中并査集的使用其实只是优化手段——为了能够迅速判断祖先节点是谁——而非算法本质——当然，算法设计的主要任务本来就是通过问题条件中蕴含的知识来“消除冗余计算”和“避免不必要计算”，所以你也可以说并査集的使用是关乎本质的，只不过，知道了为什么需要引入并査集，就会强烈地感觉到一切是顺理成章的了），那这个出了名的绕人的算法也就不那么难以理解和记忆了。<strong>譬如</strong>你知道<a href="http://blog.csdn.net/pongba/archive/2008/06/13/2544933.aspx">排序的本质</a>，就能够对什么是最优排序，为什么它是最优排序有深刻的认识。四两拨千斤。</li>
</ul>
<p><a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image004.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="clip_image004" border="0" alt="clip_image004" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image004-thumb.jpg" width="244" height="124" /></a></p>
<ul>
<li><strong>包含了多得多的知识</strong>：<strong>记一个算法，就只有一个算法</strong>。一个萝卜一个坑。<strong>就好比背99</strong><strong>乘法表只能解决乘法问题一样</strong>。而记<strong>背后的思想，却有助于解决一类问题</strong>。思想所处的抽象层面往往比到处都是实现细节的算法本身要低，越是低的抽象层次，越是本质，涵盖范围越是广泛。数学的发展本身就体现了这个过程，抽象代数就是非常好的例子。<strong>算法诞生过程中的思路往往包含了比实际算法更本质得多的知识</strong>，实际算法乃至算法的某个特定语言的实现包含了太多表面的不相干知识，它们会阻碍对本质的理解。</li>
<li><strong>重在分析推理，而不是联想</strong>：学了一大通算法和数据结构之后的一个<strong>副作用</strong>就是，看到一个问题之后，脑袋里立即不管三七二十一冒出一堆可能相干的数据结构和算法来。联想是强大的<a href="http://www.douban.com/doulist/127649/">思维捷径</a>，在任何时候都会<a href="http://en.wikipedia.org/wiki/Attention#Neural_correlates_of_attention">抢占</a>大脑的<a href="http://en.wikipedia.org/wiki/Working_memory">工作记忆</a>，由不得你控制——比如我问你“如何寻找区间的最大值”，首先进入你的意识的肯定就是学过的那个算法，甚至算法的实现细节都一一跳了出来，也许最先跳出来的还是算法实现中某个最容易弄错的边界细节，或是某个比较tricky的实现技巧！然而这些其实根本不反映一个算法的本质，结果想来想去总是停留在问题的表层。而另一方面，重在思维的传授则可以让人养成从问题本质入手，逐步分析推理的习惯，而不是直接生搬硬套。<strong>当然，完全不可否认，联想本身也是极其重要的思维方法，甚至可以说是人类思维<a href="http://en.wikipedia.org/wiki/Hebbian_learning">最重要的</a></strong><strong>特征</strong>。很多时候我们并不知道问题的本质是什么，就需要靠联想、类比来领路探索。只不过，养成优先从问题的本质入手进行考察的好习惯绝对是有更大的好处的。</li>
</ul>
<p>那到底什么样的才算是授人以渔的呢？波利亚的《如何解题》绝对算是一本，他的《数学的发现》也值得一看。具体到算法书，那就不是光看text book就足够的了，为了深入理解一个算法的来龙去脉前因后果，从一个算法中领悟尽量深刻的东西，则需要做到三件事情：</p>
<ul>
<li><strong>寻找该算法的原始出处</strong>：TAOCP作为一个资料库是绝对优秀的，基础的算法只要你能想到的，几乎都可以在上面找到原始出处。查到原始出处之后（譬如一篇paper），就可以去网上搜来看了。因为最初的作者往往对一个方案的诞生过程最为了解。比如经典数据结构中的红黑树是出了名的令人费解的结构之一，但它的作者Sedgewick<a href="http://groups.google.com/group/pongba/browse_thread/thread/3513a21065faba68">一张PPT，给你讲得通通透透</a>，比算法导论上的讲法强上数倍。</li>
<li><strong>原始的出处其实也未必就都推心置腹地和你讲得那么到位</strong>：前面说过，算法设计出来了之后人们几乎是不会去回顾整个的思维过程细节的，只把直指目标的那些东西写出来。结果就又是一篇欧几里德式的文章了。于是你就迷失在一大堆“定义”、“引理”、“定理”之中了。这种文章看上去整个写得井井有条，其实是把发明的过程整个给颠倒过来了，我一直就想，如果作者们能够将整个的思路过程写出来，哪怕文字多上十倍，我也绝对会比看那一堆定义定理要容易理解得多。话说回来，怎么办？可以再去网上找找，牛人讲得<a href="http://blog.csdn.net/g9yuayon/archive/2008/06/21/2574781.aspx">未必比经典教材上的差</a>。那倘若实在找不出好的介绍呢，就只能自己<strong>揣摩</strong>了。<strong>揣摩的重要性，是怎么说都不为过的</strong>。揣摩的<strong>一些指导性的问题有</strong>：为什么要这样（为什么这是好的）？为什么不是那样（有其它做法吗？有更好的做法吗？）？这样做是最好的吗？（为什么？能证明吗？）这个做法跟其它的什么做法有本质联系吗？<em>这个</em>跟<em>这个</em>的区别是什么？<strong>问题的本质是什么</strong>？<strong>这个做法的本质又是什么</strong>？到底本质上是什么东西导致了这个做法如此..？与这个问题类似的还有其它问题吗？（同样或类似的做法也适用吗？）等等。</li>
<li><strong>不仅学习别人的思路，整理自己的思路也是极其重要的</strong>：详见<a href="http://blog.csdn.net/pongba/archive/2008/04/18/2302905.aspx">《跟波利亚学解题》</a>的“4. 一个好习惯”和“7. 总结的意义”。</li>
</ul>
<p>前一段时间我们讨论组上有不少例子，见<a href="http://groups.google.com/group/pongba/web/toplang-problemsolvingseries">这里</a>，或<a href="http://del.icio.us/pongbablog/%E8%A7%A3%E9%A2%98">这里</a>。</p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/" title="知其所以然（续）">知其所以然（续）</a> (42)</li><li><a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/" title="知其所以然（三）：为什么算法这么难？">知其所以然（三）：为什么算法这么难？</a> (57)</li><li><a href="http://mindhacks.cn/2009/12/20/dark-time/" title="暗时间">暗时间</a> (143)</li><li><a href="http://mindhacks.cn/2009/10/05/im-a-tiny-bird-book-review/" title="不是书评 ：《我是一只IT小小鸟》">不是书评 ：《我是一只IT小小鸟》</a> (57)</li><li><a href="http://mindhacks.cn/2009/07/06/why-you-should-do-it-yourself/" title="[BetterExplained]遇到问题为什么应该自己动手">[BetterExplained]遇到问题为什么应该自己动手</a> (62)</li><li><a href="http://mindhacks.cn/2009/05/17/seven-years-in-nju/" title="我在南大的七年">我在南大的七年</a> (147)</li><li><a href="http://mindhacks.cn/2009/03/28/effective-learning-and-memorization/" title="[BetterExplained]如何有效地记忆与学习">[BetterExplained]如何有效地记忆与学习</a> (102)</li><li><a href="http://mindhacks.cn/2009/02/15/why-you-should-start-blogging-now/" title="[BetterExplained]为什么你应该（从现在开始就）写博客">[BetterExplained]为什么你应该（从现在开始就）写博客</a> (200)</li><li><a href="http://mindhacks.cn/2009/02/09/writing-is-better-thinking/" title="[BetterExplained]书写是为了更好的思考">[BetterExplained]书写是为了更好的思考</a> (90)</li><li><a href="http://mindhacks.cn/2009/01/14/make-yourself-irreplacable/" title="什么才是你的不可替代性和核心竞争力">什么才是你的不可替代性和核心竞争力</a> (25)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2008. | <a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/#commenting">36 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e5%ad%a6%e4%b9%a0%e6%96%b9%e6%b3%95/" rel="tag">学习方法</a>, <a href="http://mindhacks.cn/tags/%e7%ae%97%e6%b3%95/" rel="tag">算法</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/">原始超链接</a>: http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/feed/</wfw:commentRss>
		<slash:comments>36</slash:comments>
		</item>
		<item>
		<title>数学之美番外篇：快排为什么那样快</title>
		<link>http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/</link>
		<comments>http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/#comments</comments>
		<pubDate>Fri, 13 Jun 2008 11:53:00 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[数学]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[计算机科学]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/</guid>
		<description><![CDATA[知道这个理论是在TopLanguage上的一次讨论，先是g9转了David MacKay的一篇文章，然后引发了牛人们的一场关于信息论的讨论。Anyway，正如g9很久以前在Blog里面所说的： 

有时无知是福。俺看到一点新鲜的科普也能觉得造化神奇。刚才读Gerald Jay Sussman（SICP作者）的文章，Building Robust Systems – an essay，竟然心如小鹿乱撞，手心湿润，仿佛第一次握住初恋情人温柔的手。

而看到MacKay的这篇文章我也有这种感觉——以前模糊的东西忽然有了深刻的解释，一切顿时变得明白无比。原来看问题的角度或层面能够带来这么大的变化。再一次印证了越是深刻的原理往往越是简单和强大。所以说，土鳖也有土鳖的幸福:P 

这篇文章相当于MacKay原文的白话文版。MacKay在原文中用到了信息论的知识，后者在我看来并不是必须的，尽管计算的时候方便，但与本质无关。所以我用大白话解释了一通。]]></description>
			<content:encoded><![CDATA[<p><strong>目录</strong></p>
<p>0. 前言</p>
<p>1. 猜数字</p>
<p>2. 称球</p>
<p>3. 排序</p>
<p>&#160;&#160;&#160; 3.1 为什么堆排比快排慢</p>
<p>&#160;&#160;&#160; 3.2 为什么快排其实也不是那么快</p>
<p>&#160;&#160;&#160; 3.3 基排又为什么那么快呢</p>
<p>4. 信息论！信息论？</p>
<p>5. 小结</p>
<p><strong>0. </strong><strong>前言</strong></p>
<p>知道这个理论是在<a href="http://groups.google.com/group/pongba">TopLanguage</a>上的一次讨论，先是g9<a href="http://groups.google.com/group/pongba/msg/f95aa12feb4dfd67">转了David MacKay的一篇文章</a>，然后引发了牛人们的<a href="http://groups.google.com/group/pongba/browse_frm/thread/28ac39e0222becf2">一场关于信息论的讨论</a>。Anyway，正如g9很久以前在<a href="http://blog.csdn.net/g9yuayon">Blog</a>里面所<a href="http://blog.csdn.net/g9yuayon/archive/2007/04/22/1574518.aspx">说</a>的： </p>
<blockquote><p>有时无知是福。俺看到一点新鲜的科普也能觉得造化神奇。刚才读Gerald Jay Sussman（<a href="http://mitpress.mit.edu/sicp/">SICP</a>作者）的文章，<a href="http://swiss.csail.mit.edu/classes/symbolic/spring07/readings/robust-systems.pdf">Building Robust Systems – an essay</a>，竟然心如小鹿乱撞，手心湿润，仿佛第一次握住初恋情人温柔的手。</p>
</blockquote>
<p>而看到<a href="http://users.aims.ac.za/~mackay/">MacKay</a>的这篇文章我也有这种感觉——以前模糊的东西忽然有了深刻的解释，一切顿时变得明白无比。原来看问题的角度或层面能够带来这么大的变化。再一次印证了越是深刻的原理往往越是简单和强大。所以说，土鳖也有土鳖的幸福:P </p>
<p>这篇文章相当于MacKay<a href="http://users.aims.ac.za/~mackay/sorting/sorting.html">原文</a>的白话文版。MacKay在原文中用到了信息论的知识，后者在我看来并不是必须的，尽管计算的时候方便，但与本质无关。所以我用大白话解释了一通。 </p>
<p><strong>1. </strong><strong>猜数字 </strong></p>
<p>我们先来玩一个猜数字游戏：我心里默念一个1~64之间的数，你来猜（你只能问答案是“是”或“否”的问题）。为了保证不论在什么情况下都能以尽量少的次数猜中，你应该采取什么策略呢？很显然，二分。先是猜是不是位于1~32之间，排除掉一半可能性，然后对区间继续二分。这种策略能够保证无论数字怎么跟你捉迷藏，都能在log_2{n}次以内猜中。用算法的术语来说就是它的下界是最好的。 </p>
<p>我们再来回顾一下这个游戏所蕴含的本质：为什么这种策略具有最优下界？答案也很简单，这个策略是平衡的。反之如果策略不是平衡的，比如问是不是在1~10之间，那么一旦发现不是在1~10之间的话就会剩下比N/2更多的可能性需要去考察了。 </p>
<p><a href="http://blog.youxu.info/">徐宥</a>在讨论中提到，这种策略的本质可以概括成“让未知世界无机可乘”。它是没有“弱点的”，<strong>答案的任何一个分支都是等概率的</strong>。反之，一旦某个分支蕴含的可能性更多，当情况落到那个分支上的时候你就郁闷了。比如猜数字游戏最糟糕的策略就是一个一个的猜：是1吗？是2吗？&#8230; 因为这种猜法最差的情况下需要64次才能猜对，下界非常糟糕。二分搜索为什么好，就是因为它每次都将可能性排除一半并且<strong>无论如何</strong>都能排除一半（它是最糟情况下表现最好的）。 </p>
<p><strong>2. </strong><strong>称球 </strong></p>
<p>12个小球，其中有一个是坏球。有一架天平。需要你用最少的称次数来确定哪个小球是坏的并且它到底是轻还是重。 </p>
<p>这个问题是一道流传已久的智力题。网络上也有很多讲解，还有泛化到N个球的情况下的严格证明。也有零星的一些地方提到从信息论的角度来看待最优解法。本来我一直认为这道题目除了试错之外没有其它高妙的思路了，只能一个个方法试，并尽量从结果中寻找信息，然后看看哪种方案最少。 </p>
<p>然而，实际上它的确有其它的思路，一个更本质的思路，而且根本用不着信息论这么拗口的知识。 </p>
<p>我们先回顾一下猜数字游戏。为了保证任何情况下以最少次数猜中，我们的策略是每次都排除恰好一半的可能性。类比到称球问题上：坏球可能是12个球中的任意一个，这就是12种可能性；而其中每种可能性下坏球可能轻也可能重。于是“坏球是哪个球，是轻是重”这个问题的答案就有12×2=24种可能性。现在我们用天平来称球，就等同于对这24种可能性发问，由于天平的输出结果有三种“平衡、左倾、右倾”，这就相当于我们的问题有三个答案，即可以将所有的可能性切成三份，根据猜数字游戏的启发，我们应当尽量让这三个分支概率均等，即平均切分所有的可能性为三等份。如此一来的话一次称量就可以将答案的可能性缩减为原来的1/3，三次就能缩减为1/27。而总共才有24种可能性，所以理论上是完全可以3次称出来的。 </p>
<p>如何称的指导原则有了，构造一个称的策略就不是什么太困难的事情了。首先不妨解释一下为什么最直观的称法不是最优的——6、6称：在6、6称的时候，天平平衡的可能性是0。刚才说了，最优策略应该使得天平三种状态的概率均等，这样才能三等分答案的所有可能性。 </p>
<p>为了更清楚的看待这个问题，我们不妨假设有6个球，来考虑一下3、3称和2、2称的区别： </p>
<p>在未称之前，一共有12种可能性：1轻、1重、2轻、2重、&#8230;、6轻、6重。现在将1、2、3号放在左边，4、5、6放在右边3、3称了之后，不失一般性假设天平左倾，那么小球的可能性就变成了原来的一半（6种）：1重、2重、3重、4轻、5轻、6轻。即这种称法能排除一半可能性。 </p>
<p>现在再来看2、2称法，即1、2放左边，3、4放右边，剩下的5、6不称，放一边。假设结果是天平平衡，那么可能性剩下——4种：5重、5轻、6重、6轻。假设天平左倾，可能性也剩下4种：1重、2重、3轻、4轻。右倾和左倾的情况类似。总之，这种称法，不管天平结果如何，情况都被我们缩小到了原来的三分之一！我们充分利用了“天平的结果状态可能有三种”这个条件来三等分所有可能性，而不是二等分。 </p>
<p>说到这里，剩下的事情就实在很简单了：第二步称法，只要记着这样一个指导思想——你选择的称法必须使得当天平平衡的时候答案剩下的可能性和天平左倾（右倾）的时候答案剩下的可能性一样多。实际上，这等同于你得选择一种称法，使得天平输出三种结果的概率是均等的，因为天平输出某个结果的概率就等同于所有支持这个结果（左倾、右倾、平衡）的答案可能性的和，并且答案的每个可能性都是等概率的。 </p>
<p>MacKay在他的书《Information Theory: Inference and Learning Algorithms》（<a href="http://users.aims.ac.za/~mackay/itila/book.html">作者开放免费电子书</a>）里面4.1节专门讲了这个称球问题，还画了一张不错的图，我就照抄了： </p>
<p><a href="http://mindhacks.cn/wp-content/uploads/2009/02/23131201.jpg"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="2313120" border="0" alt="2313120" src="http://mindhacks.cn/wp-content/uploads/2009/02/2313120-thumb1.jpg" width="476" height="480" /></a> </p>
<p>图中“1+”是指“1号小球为重”这一可能性。一开始一共有24种可能性。4、4称了之后不管哪种情况（分支），剩下来的可能性总是4种。这是一个完美的三分。然后对每个分支构造第二次称法，这里你只要稍加演算就可以发现，分支1上的第二次称法，即“1、2、6对3、4、5”这种称法，天平输出三种结果的可能性是均等的（严格来说是几乎均等）。这就是为什么这个称法能够在最坏的情况下也能表现最好的原因，没有哪个分支是它的弱点，它必然能将情况缩小到原来的1/3。 </p>
<p><strong>3. </strong><strong>排序 </strong></p>
<p>用前面的看问题视角，排序的本质可以这样来表述：一组未排序的N个数字，它们一共有N!种重排，其中只有一种排列是满足题意的（譬如从大到小排列）。换句话说，排序问题的可能性一共有N!种。任何基于比较的排序的基本操作单元都是“比较a和b”，这就相当于猜数字游戏里面的一个问句，显然这个问句的答案只能是“是”或“否”，一个只有两种输出的问题最多只能将可能性空间切成两半，根据上面的思路，最佳切法就是切成1/2和1/2。也就是说，我们希望在比较了a和b的大小关系之后，如果发现a&lt;b的话剩下的排列可能性就变成N!/2，如果发现a&gt;b也是剩下N!/2种可能性。由于假设每种排列的概率是均等的，所以这也就意味着支持a&lt;b的排列一共有N!/2个，支持a&gt;b的也是N!/2个，换言之，a&lt;b的概率等于a&gt;b的概率。 </p>
<p>我们希望每次在比较a和b的时候，a&lt;b和a&gt;b的概率是均等的，这样我们就能保证无论如何都能将可能性缩小为原来的一半了！最优下界。 </p>
<p>一个直接的推论是，如果每次都像上面这样的完美比较，那么N个元素的N!种可能排列只需要log_2{N!}就排查玩了，而log_2{N!}近似于NlogN。这正是快排的复杂度。 </p>
<p><strong>3.1 </strong><strong>为什么堆排比快排慢 </strong></p>
<p>回顾一下堆排的过程： </p>
<p>1. 建立最大堆（堆顶的元素大于其两个儿子，两个儿子又分别大于它们各自下属的两个儿子&#8230; 以此类推） </p>
<p>2. 将堆顶的元素和最后一个元素对调（相当于将堆顶元素（最大值）拿走，然后将堆底的那个元素补上它的空缺），然后让那最后一个元素从顶上往下滑到恰当的位置（重新使堆最大化）。 </p>
<p>3. 重复第2步。 </p>
<p>这里的关键问题就在于第2步，堆底的元素肯定很小，将它拿到堆顶和原本属于最大元素的两个子节点比较，它比它们大的可能性是微乎其微的。实际上它肯定小于其中的一个儿子。而大于另一个儿子的可能性非常小。于是，这一次比较的结果就是概率不均等的，根据前面的分析，概率不均等的比较是不明智的，因为它并不能保证在糟糕情况下也能将问题的可能性削减到原本的1/2。可以想像一种极端情况，如果a肯定小于b，那么比较a和b就会什么信息也得不到——原本剩下多少可能性还是剩下多少可能性。 </p>
<p>在堆排里面有大量这种近乎无效的比较，因为被拿到堆顶的那个元素几乎肯定是很小的，而靠近堆顶的元素又几乎肯定是很大的，将一个很小的数和一个很大的数比较，结果几乎肯定是“小于”的，这就意味着问题的可能性只被排除掉了很小一部分。 </p>
<p>这就是为什么堆排比较慢（堆排虽然和快排一样复杂度都是O(NlogN)但堆排复杂度的常系数更大）。 </p>
<p>MacKay也提供了一个修改版的堆排：每次不是将堆底的元素拿到上面去，而是直接比较堆顶（最大）元素的两个儿子，即选出次大的元素。由于这两个儿子之间的大小关系是很不确定的，两者都很大，说不好哪个更大哪个更小，所以这次比较的两个结果就是概率均等的了。具体参考<a href="http://users.aims.ac.za/~mackay/sorting/sorting.html">这里</a>。 </p>
<p><strong>3.2 </strong><strong>为什么快排其实也不是那么快 </strong></p>
<p>我们考虑快排的过程：随机选择一个元素做“轴元素”，将所有大于轴元素的移到左边，其余移到右边。根据这个过程，快排的第一次比较就是将一个元素和轴元素比较，这个时候显而易见的是，“大于”和“小于”的可能性各占一半。这是一次漂亮的比较。 </p>
<p>然而，快排的第二次比较就不那么高明了：我们不妨令轴元素为pivot，第一次比较结果是a1&lt;pivot，那么可以证明第二次比较a2也小于pivot的可能性是2/3！这容易证明：如果a2&gt;pivot的话，那么a1，a2，pivot这三个元素之间的关系就完全确定了——a1&lt;pivot&lt;a2，剩下来的元素排列的可能性我们不妨记为P（不需要具体算出来）。而如果a2&lt;pivot呢？那么a1和a2的关系就仍然是不确定的，也就是说，这个分支里面含有两种情况：a1&lt;a2&lt;pivot，以及a2&lt;a1&lt;pivot。对于其中任一种情况，剩下的元素排列的可能性都是P，于是这个分支里面剩下的排列可能性就是2P。所以当a2&lt;pivot的时候，还剩下2/3的可能性需要排查。 </p>
<p>再进一步，如果第二步比较果真发现a2&lt;pivot的话，第三步比较就更不妙了，模仿上面的推理，a3&lt;pivot的概率将会是3/4！ </p>
<p>这就是快排也不那么快的原因，因为它也没有做到每次比较都能将剩下的可能性砍掉一半。 </p>
<p><strong>3.3 </strong><strong>鸡排为什么又那么快呢？ </strong></p>
<p>传统的解释是：<a href="http://en.wikipedia.org/wiki/Radix_sort">基排</a>不是基于比较的，所以不具有后者的局限性。话是没错，但其实还可以将它和基于比较的排序做一个类比。 </p>
<p>基排的过程也许是源于我们理顺一副牌的过程：如果你有N（N&lt;=13）张牌，乱序，如何理顺呢？我们假象桌上有十三个位置，然后我们将手里的牌一张一张放出去，如果是3，就放在位置3上，如果是J，就放在位置11上，放完了之后从位置1到位置13收集所有的牌（没有牌的位置上不收集任何牌）。 </p>
<p>我们可以这样来理解基排高效的本质原因：假设前i张牌都已经放到了它们对应的位置上，第i+1张牌放出去的时候，实际上就相当于“一下子”就确立了它和前i张牌的大小关系，用O(1)的操作就将这张牌正确地插入到了前i张牌中的正确位置上，这个效果就相当于插入排序的第i轮原本需要比较O(i)次的，现在只需要O(1)了。 </p>
<p>但是，为什么基排能够达到这个效果呢？上面只是解释了过程，解释了过程不代表解释了本质。 </p>
<p>当i张牌放到位之后，放置第i+1张牌的时候有多少种可能性？大约i+1种，因为前i张牌将13个位置分割成了i+1个区间——第i+1张牌可以落在任意一个区间。所以放置第i+1张牌就好比是询问这样一个问题：“这张牌落在哪个区间呢？”而这个问题的答案有i+1种可能性？所以它就将剩下来的可能性均分成了i+1份（换句话说，砍掉了i/i+1的可能性！）。再看看基于比较的排序吧：由于每次比较只有两种结果，所以最多只能将剩下的可能性砍掉一半。 </p>
<p>这就是为什么基排要快得多。而所有基于比较的排序都逃脱不了NlogN的宿命。 </p>
<p><strong>4. </strong><strong>信息论！信息论？ </strong></p>
<p>本来呢，MacKay写那篇文章是想用信息论来解释为什么堆排慢，以及为什么快排也慢的。MacKay在他的文章中的解释是，只有提出每种答案的概率都均等的问题，才能获得最大信息量。然而，仔细一想，其实这里信息论并不是因，而是果。这里不需要用信息论就完全能够解释，而且更明白。信息论只是对这个解释的一个形式化。当然，信息论在其它地方还是有应用的。但这里其实用不着信息论这么重量级的东西（也许具体计算一些数据的时候是需要的），而是只需要一种看问题的本质视角：将排序问题看成和猜数字一样，是通过问问题来缩小/排除（narrow down）结果的可能性区间，这样一来，就会发现，“最好的问题”就是那些能够均分所有可能性的问题，因为那样的话不管问题的答案如何，都能排除掉k-1/k（k为问题的答案有多少种输出——猜数字里面是2，称球里面是3）种可能性，而不均衡的问题总会有一个或一些答案分支排除掉的可能性要小于k-1/k。于是策略的下界就被拖累了。 </p>
<p><strong>5. </strong><strong>小结 </strong></p>
<p>这的确是“小结”，因为两点： </p>
<p>1. 这个问题可以有信息论的理论解释，而信息论则是一个相当大的领域了。</p>
<p>2. 文中提到的这种看问题的视角除了用于排序、称球，还能够运用到哪些问题上（比如搜索）。</p>
<p><strong>Update(06/13/2008)</strong> : <a href="http://blog.youxu.info/">徐宥</a>在讨论中<a href="http://groups.google.com/group/pongba/msg/07493e329ed920ff">继续提到</a>：</p>
<blockquote><p>另外，这几天我重新把TAOCP 第三卷(第二版)翻出来看了看 Knuth 怎么说这个问题的, 发现真是牛大了： </p>
<p>先说性能： </p>
<p>pp148, section 5.2.3 说： </p>
<p>When N = 1000, the approximate average runiing time on MIX are      <br />160000u for heapsort       <br />130000u for shellsort       <br />80000u&#160; for quicksort </p>
<p>这里,&#160; Knuth 同学发现一般情况下 heapsort 表现很不好. 于是，在下文他就说，习题18 (pp156, 难度21) </p>
<p>(R.W.Floyd) During the selection phase of heapsort, the key K tends to      <br />be quite small, so that nearly all the comparisons in step H6 find       <br />K&lt;K_j. Show how to modify the algorithm so that K is not compared with       <br />K_j in the main loop of the computation, thereby nearly cutting the       <br />average number of comparisons in half. </p>
<p>答案里面的方法和DMK的方法是一样的。(我觉得DMK是看了这个论文或者TAoCP的) 这里说 by half，就正好和快排差不多了。 </p>
<p>再说信息论分析： </p>
<p>在5.3.1 (pp181) 高爷爷就说, “排序问题可以看成是一个树上的鸟儿排排站的问题. (还特地画了一棵树), 下一段就说, 其实这个也      <br />有等价说法, 就是信息论, 我们从称球问题说起&#8230;” </p>
<p>然后后面一直讲信息论和最小比较排序&#8230;</p>
</blockquote>
<p>高爷爷真不愧是姓高的，囧rz..</p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2008/09/21/the-magical-bayesian-method/" title="数学之美番外篇：平凡而又神奇的贝叶斯方法">数学之美番外篇：平凡而又神奇的贝叶斯方法</a> (48)</li><li><a href="http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/" title="数学之美番外篇：进化论中的概率论">数学之美番外篇：进化论中的概率论</a> (13)</li><li><a href="http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/" title="康托尔、哥德尔、图灵&mdash;&mdash;永恒的金色对角线(rev#2)">康托尔、哥德尔、图灵&mdash;&mdash;永恒的金色对角线(rev#2)</a> (34)</li><li><a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/" title="知其所以然（三）：为什么算法这么难？">知其所以然（三）：为什么算法这么难？</a> (57)</li><li><a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/" title="知其所以然（续）">知其所以然（续）</a> (42)</li><li><a href="http://mindhacks.cn/2008/09/11/machine-learning-and-ai-resources/" title="机器学习与人工智能学习资源导引">机器学习与人工智能学习资源导引</a> (9)</li><li><a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/" title="知其所以然（以算法学习为例）">知其所以然（以算法学习为例）</a> (36)</li><li><a href="http://mindhacks.cn/2008/04/18/learning-from-polya/" title="跟波利亚学解题(rev#3)">跟波利亚学解题(rev#3)</a> (21)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2008. | <a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/#commenting">23 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e6%95%b0%e5%ad%a6/" rel="tag">数学</a>, <a href="http://mindhacks.cn/tags/%e7%ae%97%e6%b3%95/" rel="tag">算法</a>, <a href="http://mindhacks.cn/tags/%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%a7%91%e5%ad%a6/" rel="tag">计算机科学</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/">原始超链接</a>: http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/feed/</wfw:commentRss>
		<slash:comments>23</slash:comments>
		</item>
		<item>
		<title>跟波利亚学解题(rev#3)</title>
		<link>http://mindhacks.cn/2008/04/18/learning-from-polya/</link>
		<comments>http://mindhacks.cn/2008/04/18/learning-from-polya/#comments</comments>
		<pubDate>Fri, 18 Apr 2008 13:37:00 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[思维改变生活]]></category>
		<category><![CDATA[算法]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2008/04/18/learning-from-polya/</guid>
		<description><![CDATA[波利亚在他著名的《How To Solve It》中讲了这么一个有趣的心理学实验：

用一个缺了一条边的正方形围栏围住一只动物（狗、黑猩猩、母鸡、人类婴儿），在围栏的另一侧放上一个被试很想要的物体（对动物来说是食物，对人类婴儿来说是有趣的玩具），然后观察他们各自的行为。发现，狗在扒着围栏吠了几声发现无法通过的时候，不久便学会了从围栏的缺口的那一边绕出去，母鸡则朝着围栏一个劲的扑腾，不会想到绕弯子。此外，人类婴儿很快就学会了绕过障碍；而黑猩猩也学得很快（黑猩猩是和人类最近的灵长类亲属）。这个实验有力的证明了，动物解决问题的能力是进化而来的、天生的、硬编码在大脑的神经元网络里面的。]]></description>
			<content:encoded><![CDATA[<p><b>一些故事</b></p>
<p><a href="http://en.wikipedia.org/wiki/Polya">波利亚</a>在他著名的<a href="http://www.douban.com/subject/1456890/">《How To Solve It》</a>中讲了这么一个有趣的心理学实验：</p>
<blockquote><p>用一个缺了一条边的正方形围栏围住一只动物（狗、黑猩猩、母鸡、人类婴儿），在围栏的另一侧放上一个被试很想要的物体（对动物来说是食物，对人类婴儿来说是有趣的玩具），然后观察他们各自的行为。发现，狗在扒着围栏吠了几声发现无法通过的时候，不久便学会了从围栏的缺口的那一边绕出去，母鸡则朝着围栏一个劲的扑腾，不会想到绕弯子。此外，人类婴儿很快就学会了绕过障碍；而黑猩猩也学得很快（黑猩猩是和人类最近的灵长类亲属）。这个实验有力的证明了，动物解决问题的能力是进化而来的、天生的、硬编码在大脑的神经元网络里面的。<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image0011.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image001" border="0" alt="clip_image001" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image001-thumb1.jpg" width="102" height="154" /></a></p>
</blockquote>
<p>事实上，不仅解决问题方面是如此，人类整个认知系统中绝大部分功能从本质上都是硬编码的，能在后天习得的只是“程度”的不同，而不是“本质”的不同。<a href="http://www.douban.com/subject/1712350/">《动机心理学》</a>中有一个令人印象深刻的一个例子： </p>
<blockquote><p>先给小鼠喝某种甜味水（称为“可口水”），然后用X射线促使其产生反胃感，能使小鼠形成对这种味道的水的厌恶和回避（经典条件反射）。但如果不是在水里面加味道，而是在它喝水的时候伴随强光刺激（即让它喝“光噪水”），然后同样刺激其反胃，却无法使它养成对“光噪水”的厌恶。另一方面，如果不是促使其反胃（身体不适），而是用电击惩罚，则它无法形成对“可口水”的厌恶，而是形成对“光噪水“的厌恶。显然，小鼠对事件之间的关联的归因也具有着某种硬编码好了的倾向。在这个例子中，老鼠的大脑里面硬编码了“将身体不适（内部事件）归因于食物而不是闪光”、”将电击（外部事件）归因于闪光而非食物” 这种逻辑。</p>
</blockquote>
<p>而人类也有类似的归因倾向。金出武雄在<a href="http://www.douban.com/subject/1867455/">《像外行一样思考，像专家一样实践》</a>中也提到，他认为人类的直觉实际上也是计算，捷径式的计<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image002.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image002" border="0" alt="clip_image002" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image002-thumb.jpg" width="101" height="155" /></a>算，只不过由于我们目前还不了解人类大脑内神经元的全部结构（或者说“感性”的物质基础）这才把“感性”当成人类所特有的；金出武雄的这种观点跟心理学中的认知捷径不谋而合。实际上，越是高等的动物，大脑中用于处理特定问题的硬编码神经元回路就越是多和复杂。例如，达尔文早在<a href="http://www.douban.com/subject/1049670/">《人类和动物的情绪表达》</a>中就先知先觉的提出了动物情绪的适应价值；<a href="http://www.douban.com/subject/1128662/">《Mean Genes》</a>列出了用于解决生存繁衍问题的特定认知倾向；<a href="http://www.douban.com/subject/1193621/">《决策与判断》</a>里面则列出了人类在解决更具一般性的决策问题中的一些系统性的、可预测的认知偏差；而<a href="http://www.douban.com/subject/2990015/">《Predictably Irrational》</a>更是把这个认识提高到方法论的层面，主张人类的非理性实际上是完全可预知的。事实上，所有这些观点都建立在一个基本事实的基础上，即人类大脑中的千亿神经元是由在漫长的进化过程中被塑造出来的分工明确的、<a href="http://en.wikipedia.org/wiki/Ad_hoc">ad hoc</a>的一组子系统构成的。</p>
<p>越是高等的动物，解题能力越高，猩猩能够进行某种顿悟，在脑子里就构想出通过堆放墙角的箱子来帮助获取高高吊着的香蕉；而出于进化之树 顶端的人类则具有非比寻常的大脑，在人类整个进化的过程中，解决问题的能力一直在进化，所以说人脑中的神经元最重要的部分是为了解题而存在的也不为过。不同的人只是在解题能力程度上不同，并没有本质上能与不能的差异。</p>
<p>波利亚在《How To Solve It》中另外还举了下面这个例子：</p>
<p>一个原始人站在一条小溪前，他想要越过这条小溪，但溪水经过昨天一夜，已经涨了上来；因此他面临一个问题：如何越过这条小溪。他联想起以前曾经从一棵倒下并横在河上的树木上走过去，于是他的问题变成了如何找到这样一颗倒下并横在溪流上的树木。他环顾四周，发现溪流上没有这样的横着的树木，但他发现周围倒是有不少生长着的树木；于是问题再次变成了：如何使这些树木躺到溪流上。</p>
<p>在这个想像的故事中我们看到了一个问题是如何被一步步<a href="http://en.wikipedia.org/wiki/Reduction_%28complexity%29">归约</a>的：首先，原始人通过对一个已知的类似问题的联想认识到一个重要的性质：如<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image003.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image003" border="0" alt="clip_image003" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image003-thumb.jpg" width="102" height="154" /></a>果有一棵树横在河上，我就可以借助这棵树过河。这就将一个无法直接解决的问题转化为了一个新的、已知的、并容易解决的问题。值得注意的是这里“联想”是极其重要的一个环节，联想可以将手上的问题与已知的类似问题联系起来，并从后者中吸取能够利用的方法。联想也能够将与问题有关的定理或性质从大脑的知识系统中提取出来。基本上，如果一个联想能够得到某个性质，而这个性质能够或者将问题往上归约一层，或者将条件往下推导一层，这个联想就是有用的。事实上，如果你仔细注意以下解题的过程，你也许会发现，所有的启发式思维方法（<a href="http://en.wikipedia.org/wiki/Heuristics">heuristics</a>）实质上都是为了联想服务的，而联想则是为了从我们大脑的知识系统中提取出有价值的性质或定理，从而补上从条件到结论、从已知到未知之间缺失的链环。</p>
<p><b>一段历史</b></p>
<p>实际上，人类自从进入理性文明以来，不仅在不断的解题，还在不断的对自身的解题方法进行反省和总结。在这条路上，有一个真正光荣与辉煌的梦想，那就是发现人类解题的所有<a href="http://en.wikipedia.org/wiki/Heuristic">一般性法则</a>，并借此建造出一台能够解决人类能够解决的所有问题的<a href="http://en.wikipedia.org/wiki/General_Problem_Solver">一般解题机</a>。与物理中的建造永动机不一样，这个梦想并非遥不可及的，自从古希腊哲学家对人类心智的反省思考以来，许多著名的数学和哲学家为此建造了阶梯，<a href="http://en.wikipedia.org/wiki/Pappus_of_Alexandria">Pappus</a>，亚历山大学派最后一位伟大的几何学家，就曾在他恢弘的八卷本《数学汇编》中描述了其中的一种法则，他将它称为“分析与综合”，大意如下：</p>
<p>首先我们把需要求解的问题本身当成条件，从它推导出结论，再从这个结论推导出更多的结论，直到某一个点上我们发现已经出现了真正已知的条件。这个过程称为分析。有了这条路径，我们便可以从已知条件出发，一路推导到问题的解。</p>
<p>波利亚在他的三卷本中把这种做法叫做Working Backwards（倒过来解）。</p>
<p><a href="http://en.wikipedia.org/wiki/Ren%C3%A9_Descartes">笛卡尔</a>也曾经试图将人类思维的规则总结为36条（最终完成了<a href="http://en.wikisource.org/wiki/Rules_for_the_Direction_of_the_Mind">21条</a>）。<a href="http://en.wikipedia.org/wiki/Gottfried_Leibniz">莱布尼兹</a>，现代计算机实质上的发明者，也说到：</p>
<p>在我看来，没有什么能比探索发明的源头还要重要，它远比发明本身更重要。</p>
<p>再后来，捷克数学家<a href="http://en.wikipedia.org/wiki/Bernard_Bolzano">波尔查诺</a>也试图总结人类思维的本质规律，他在他的著作《科学的理论》中写道：</p>
<p>我根本不奢望自己能够提供任何超于其他天才所使用过的科学探索方法之外的新方法，从这个意义上，你别指望能在书中看到什么新的东西。但是，我会尽我的全力去总结所有伟大的思想者们共有的、思维的原则和方法，我认为即便是他们自己在思考的时候也未必全都意识到自己在使用什么方法。</p>
<p>再后来，就到了近代，随着科学技术的进步，心理学最活跃的子学科——<a href="http://en.wikipedia.org/wiki/Cognitive_science">认知科学</a>——开始辉煌起来，人类开始向思维乃至自我意识的物质基础发起进攻。两位多才多艺的计算机科学家兼认知科学家，<a href="http://en.wikipedia.org/wiki/Herbert_Simon">Herbert Simon</a>（另外还是经济学家）和<a href="http://en.wikipedia.org/wiki/Allen_Newell">Allen Newell</a>写出了世界上第一个<a href="http://en.wikipedia.org/wiki/General_Problem_Solver">一般性解题机</a>的程序（GPS），虽然GPS只能解决很狭窄的一类问题，但这是第一个将“问题解决策略”和“知识”分离开来的程序。显然，在知识之外，人类的思维是有着一些一般性的指导规则的。事实上，波利亚在《数学与猜想》中写道，欧拉是最重数学思维的教学的，欧拉认为如果不能把解决数学问题背后的思维过程教给学生的话，数学教学就是没有意义的。</p>
<p><b>一些方法</b><a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image0041.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image004" border="0" alt="clip_image004" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image004-thumb1.jpg" width="104" height="152" /></a></p>
<p>这些一般性的思维方法，就是波利亚用了整整三本书，五卷本（<a href="http://www.douban.com/subject/1456890/">《How To Solve It》</a>、<a href="http://www.douban.com/subject/1850407/">《数学的发现》</a>、<a href="http://www.douban.com/subject/1134230/">《数学与猜想》</a>）来试图阐明的。波利亚的书是独特的，从小到大，我们看过的数学书几乎无一不是欧几里德式的：从定义到定理，再到推论。是属于“顺流而下”式的。这样的书完全而彻底的扭曲了数学发现的真实过程。举个例子，<a href="http://www.douban.com/subject/2124368/">《证明与反驳：数学发现的逻辑》</a>在附录一中讲了一个非常有趣的例子：<a href="http://en.wikipedia.org/wiki/Cauchy">柯西</a>当年试图将函数的连续性从单个函数推广到无穷级数上面去，即证明由无穷多个连续函数构成的收敛级数本身也是一个连续的函数，柯西给出了一个巧妙的证明，似乎漂亮地解决了这个问题。然而傅立叶却给出了一个噩梦般的三角函数的收敛级数，它的和却并不是连续的。这令柯西大为头疼，以至于延迟了他的数学分析教程的出版好些年。后来，赛德尔解决了这个问题：原来柯西在他看似无懈可击的证明中非常隐蔽（他自己也不知觉的情况下）引入了一个潜在的假设，这个假设就是后来被称为的“一致收敛”条件。当时我看到这里就去翻我们的数学分析书，发现“一致收敛”这个概念第一次出现的时候是这样写的：定义：一致收敛&#8230;</p>
<p>所以说，从这个意义上，<a href="http://www.douban.com/subject/1049136/">《数学，确定性的丧失》</a>从历史的角度再现了真实的数学发展过程，是一本极其难得的好书。而事实上，<a href="http://www.math.nmsu.edu/%7Ehistory/">从真实的数学 历史发展的角度去讲授数学</a>，也是<a href="http://en.wikipedia.org/wiki/Mathematics_education#Methods">数学教学法</a>的最佳方法。不过，《数学，确定性的丧失》的弱点是并没有从思维的角度去再现数学发现的思维过程，而这正是波利亚所做的。</p>
<p>总结波利亚在书中提到的思维方法，尤其是《How To Solve It》中的启发式思考方法，有这样一些：<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image005.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image005" border="0" alt="clip_image005" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image005-thumb.jpg" width="103" height="152" /></a></p>
<ul>
<li><b>时刻不忘未知量</b>（即时刻别忘记你到底想要求什么，<a href="http://www.douban.com/subject/1135754/">问题是什么</a>。）莱布尼兹曾经将人的解题思考过程比喻成晃筛子，把脑袋里面的东西都给抖落出来，然后正在搜索的注意力会抓住一切细微的、与问题有关的东西。事实上，要做到能够令注意力抓住这些有关的东西，就必须时刻将问题放在注意力层面，否则即使关键的东西抖落出来了也可能没注意到。</li>
<li><b>用特例启发思考</b>。一个泛化的问题往往给人一种无法把握、无从下手、或无法抓住里面任何东西的感觉，因为条件太泛，所以看起来哪个条件都没法入手。一个泛化的问题往往有一种 “不确定性”（譬如元素的个数不确定，某个变量不确定等等），这种不确定性会成为思维的障碍，通过考虑一个合适的特例，我们不仅使得问题的条件确定下来从而便于通过试错这样的手法去助探问题的内部结构，同时很有可能我们的特例中实质上隐藏了一般性问题的本质结构，于是我们便能够通过对特例的考察寻找一般问题的解。</li>
<li><b>反过来推导</b>。反过来推导是一种极其重要的启发法，正如前面提到的，Pappus在他的宏篇巨著中将这种手法总结为解题的最重要手法。实际上，反向解题隐含了解题中至为深刻的思想：归约。归约是一种极为重要的手法，一个著名的关于归约的笑话这样说：有一位数学家失业了，去当消防员。经过了一些培训之后，正式上任之前，训练的人考他：如果房子失火了怎么办？数学家答出了所有的正确步骤。训练人又问他：如果房子没失火呢？数学家答：那我就把房子点燃，这样我就把它归约为了一个已知问题。人类思维本质上善于“顺着”推导，从一组条件出发，运用必然的逻辑关系，得出推论。然而，如果要求的未知量与已知量看上去相隔甚远，这个时候顺着推实际上就是运用另一个启发式方法——试错——了。虽然试错是最常用，又是也是最有效的启发法，然而试错却并不是最高效的。对于许多题目而言，其要求的结论本身就隐藏了推论，不管这个推论是充分的还是必要的，都很可能对解题有帮助。如果从结论能够推导出一个充要推论，那么实际上我们就将问题进行了一次“双向”归约，如果原问题不容易解决，那么归约后的问题也许就容易解决了，通过一层层的归约，让逻辑的枝蔓从结论上一节节的生长，我们往往会发现，离已知量越来越近。此外，即便是从结论推导出的必要非充分推论（“单向”归约），对问题也是有帮助的——任何不满足这个推论的方案都不是问题的解：譬如通过驻点来求函数的最值，我们通过考察函数的最值（除了函数边界点外），发现它必然有一个性质，即在这个点上函数的一阶导数为0，虽然一阶导数为0的点未必是最值点，但我们可以肯定的是，任何一阶导数不为0的点都可以排除，这就将解空间缩小到了有穷多个点，剩下的只要做做简单的排除法，答案就出现了。再譬如线性规划中经典的<a href="http://en.wikipedia.org/wiki/Simplex_algorithm">单纯形算法</a>（又见<a href="http://www.douban.com/subject/1996256/">《Algorithms》</a>），也是通过对结论的考<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image006.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image006" border="0" alt="clip_image006" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image006-thumb.jpg" width="112" height="140" /></a> 察揭示出只需遍历有限个顶点便必然可以到达最值的。此外很多我们熟知的经典题目也都是这种思路的典范，譬如《How To Solve It》上面举的例子：通过一个9升水的桶和一个4升水的桶在河里取6升水。这个题目通过正向试错，很快也能发现答案，然而通过反向归约，则能够不偏不倚的命中答案。另一些我们耳熟能详的题目也是如此，譬如：100根火柴，两个人轮流取，每个人每次只能取1~7根，谁拿到最后一根火柴谁赢；问有必胜策略吗，有的话是先手还是后手必胜？这个问题通过试错就不是那么容易发现答案了。同样，这个问题的推广被收录在<a href="http://www.douban.com/subject/3004255/">《编程之美》</a>里面：两堆橘子，各为m和n个，两人轮流拿，拿的时候你只能选择某一堆在里面拿（即不能跨堆拿），你可以拿1~这堆里面所有剩下的个橘子，谁拿到最后一个橘子谁赢；问题同上。算法上面很多聪明的算法也都是通过考察所求结论隐藏的性质来减小复杂度的，譬如刚才提到的单纯形问题，譬如经典面试题“名人问题”、“和最小（大）的连续子序列”等等。倒推法之所以是一种极为深刻的思维方法，本质上是因为它充分利用了题目中一个最不易被觉察到的信息——结论。<strong>结论往往蕴含着丰富的条件，譬如对什么样的解才是满足题意的解的约束。一般来说，借助结论中蕴含的知识，我们便可以更为“</strong><strong>智能地”</strong><strong>搜索解空间</strong>。举一个直白的例子，有人要你在地球上寻找一栋满足如下条件的建筑：__层高（填空自己填），__风格，__年代始建，&#8230; （省略若干约束条件）。对于这样一个问题，最平凡的解法是穷举地球上每一栋建筑，直到遇到一个满足条件的为止。而更“智能”的（或者说更“启发”的）方法则是充分利用题目里面的约束信息，譬如假若条件里面说要60层楼房，你就不会去非洲找，如果要拜占庭风格的，你估计也不会到中国来找，如果要始建于很早的年代的，你也不会去非常新建的城市里面去找，等等。倒推法是如此的重要，以至于笛卡尔当时认为可以把一切问题归结为求解代数方程组，笛卡尔的万能解题法就是首先将问题转化为代数问题，然后设出未知数，列出方程，最后解这组（个）方程。其中设未知数本质上就是一种倒推：通过设出一个假想的结论x，来将题目对x的需求表达出来，然后顺势而下推导出x。仔细想想设未知数这种手法所蕴含的深刻思想，也就难怪笛卡尔会认为它是那个解决所有问题的一般性钥匙了。</li>
<li><b>试错</b>。试错估计是世界上被运用最广泛的启发法，你拿到一个题目，里面有一些条件，你需要求解一个未知量。于是你对题目这里捅捅那里捣捣，你用上所有的已知量，或使用所有你想到的操作手法，尝试着看看能不能得到有用的结论，能不能离答案近一步。事实上，如果一个问题的状态空间是有限的话，往往可以通过穷举所有可能性来找到那个关键的性质。譬如这样一个问题：有一个囚犯，国王打算处决他，但仁慈的国王给了他一个生还的机会。现在摆在他面前有两个瓶子，一个里面装了50个白球，一个装了50个黑球，这个囚犯有一个机会可以随便怎样重新分配这些球到两个瓶子中（当然，要保证不空），分配完了之后囚犯被蒙上眼睛，国王随机取一个瓶子给他，他在里面摸出一个球（因为蒙着眼睛，所以也是随机抽取），如果白球，则活，否则挂掉。问，这个囚犯如何分配，才能最大化生还几率。结合特例和试错法，这个题目的答案是很容易发现的。这样的题目还有很多。实际上，历史上很多有名的发现也都是<a href="http://www.xiaolai.net/?p=904">无意间发现的</a>（可以看作是试错的一种）。</li>
<li><b>调整题目的条件</b>（如，删除、增加、改变条件）。有时候，通过调整题目的条件，我们往往迅速能够发现条件和结论之间是如何联系的。通过扭曲问题的内部结构，我们能发现原本结构里面重要的东西。譬如这样一个题目（感谢alai同学提供）：A国由1000000个岛组成，岛与岛之间只能用船作为交通工具，有些岛之间有船来往，从任意一个岛都可以去到另外任一个岛，当然其中可能要换船。现在有一个警察要追捕一个逃犯，开始时他们在不同的岛上，警察和逃犯都是每天最多乘一次船，但这个逃犯还有点迷信，每个月的13日不乘船，警察则不迷信。警察每天乘船前都知道逃犯昨天在哪个岛上，但不知道他今天会去哪个岛。请证明，警察一定可以抓到逃犯（即到达同一个岛）。通过拿掉题目中一个关键的条件，观察区别，然后再放上那个条件，我们就能“感觉”到题目的内在结构上的某种约束，进而得到答案。</li>
<li><b>求解一个类似的题目</b>。类似的题目也许有类似的结构，类似的性质，类似的解方案。通过考察或回忆一个类似的题目是如何解决的，也许就能够借用一些重要的点子。然而如何在大脑中提取出真正类似的题目是一个问题。所谓真正类似的题目，是指那些抽象结构一样的题<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image007.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image007" border="0" alt="clip_image007" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image007-thumb.jpg" width="103" height="152" /></a>目。很多问题表面看是类似的，然而抽象结构却不是类似的；另一些题目表面看根本不像，然而抽象层面却是一致的。表面一致抽象不一致会导致错误的、无效的类比；而表面不一致（抽象一致）则会阻碍真正有用的类比。<a href="http://www.douban.com/subject/2845839/">《Psychology of Problem Solving》</a>里面对此有详细 的介绍。后面也会提到，为了便于脑中的知识结构真正能够“迁移”，在记忆掌握和分析问题的时候都应该尽量抽象的去看待，这样才能够建立知识的本质联系，才能够最大化联想空间。</li>
<li><b>列出所有可能跟问题有关的定理或性质</b>。这个不用说，我们在最初学习解题的时候就是这么做的了。</li>
<li><b>考察反面，考察其他所有情况</b>。很多时候，我们在解题时容易陷入一种特定的手法，比如为什么一定要是构造式的来解这个题目呢？为什么不能是逼近式的？为什么一定要一步到位算出答案？为什么不能从一个错误的答案调整到正确答案？为什么这个东西一定成立？不成立又如何？等等。经典例子：100个人比赛，要决出冠军至少需要赛多少场。</li>
<li><b>将问题泛化，并求解这个泛化后的问题</b>。刚才不是说过，应该通过特例启发思考吗？为什么现在又反倒要泛化呢？实际上，有少数题目，泛化之后更容易解决。即，解决一类问题，比解决这类问题里面某个特定的问题还要容易。波利亚称之为“发明者悖论”，关于“发明者悖论”，《数学与猜想》第一卷的开头有一个绝妙的例子，可惜这里空间太小，我就不摘抄了- _-||| <a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image008.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image008" border="0" alt="clip_image008" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image008-thumb.jpg" width="104" height="151" /></a></li>
</ul>
<p>以上是我认为最重要的，也是最具一般性的、放之四海都可用的思维法则。一些更为“问题特定”的，或更为现代的启发法，可以参见<a href="http://www.douban.com/subject/1232071/">《如何解题：现代启发式方法》</a>以及所有的<a href="http://www.douban.com/people/pongba/booktags/%E7%AE%97%E6%B3%95">算法书</a>。不过，在结束这一节之前，还有两个有趣的启发法值得一提： </p>
<ul>
<li><b>下意识孵化法</b>。这个方法有点像老母鸡孵小鸡的过程：我们先把问题的吃透，放在脑子里，然后等着我们的下意识把它解出来。不过，不宜将这个方法的条件拉伸过远，实际上，除非能够一直保持一种<a href="http://blog.csdn.net/pongba/archive/2007/05/24/1624382.aspx">思索的状态</a>（金出武雄所谓“思维体力”），或者问题很简单，否则一转头去做别的事情之后，你的下意识很容易就把问题丢开了。据说庞加莱有一次在街上，踏上一辆马车的那一瞬间，想出了一个重要问题的解。其他人也像仿效，结果没一个人成功。实际上，非但马车与问题无关，更重要的是，庞加莱实际上在做任何事的时候除了投入有限的注意力之外，其他思维空间都让给了那个问题了。同样，阿基米德从浴缸里面跳出来也是如此；如若不是经过了极其痛苦和长时间的思索，也不会如此兴奋。如果你也曾经花过几天的时间思考一个问题，肯定也是会有类似的经历的。</li>
<li><b>烫手山芋法</b>。说白了，就是把问题扔给别人解决。事实上，在这个网络时代，这个方法有着无可比拟的优越性。几乎任何知识性的问题，都可以迅速搜索或请教到答案。不过，如何在已知知识之外发掘出未知知识，如何解决未知问题，那就还是要看个人的能力了。数学界流传一个与此有关的笑话：如果你有一个未解决问题，你有两个办法，一，自己解决它。二，让<a href="http://en.wikipedia.org/wiki/Terence_Tao">陶哲轩</a>对它感兴趣。<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image009.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image009" border="0" alt="clip_image009" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image009-thumb.jpg" width="103" height="153" /></a></li>
</ul>
<p>除了波利亚的书之外，陶哲轩的<a href="http://www.douban.com/subject/1859573/">《Solving Mathematical Problems》</a>也对解题的启发式思路作了极有意义的介绍，他在书的第一章遵循波利亚的思路从一个具体的题目出发，介绍了如何运用波利亚在书中提到的各种启发式方法来对解题进行尝试。</p>
<p>总而言之，充分挖掘题目中蕴含的知识，是解题的最关键步骤。本质上，所有启发式方法某种意义上都是为此服务的。这些知识，有些时候以联想的方式被挖掘出来，此时启发式方法充当的便是辅助联想的手段。有时候则以演绎和归纳的手法被挖掘出来，此时启发式方法则充当助探（辅助探索）工具。 </p>
<p><b>一点思考</b></p>
<p><b>1. 联想的法则</b></p>
<p>人类的大脑是一个复杂而精妙的器官，然而某种程度上，人类的大脑也是一个愚蠢的器官。如果你总结过你解过的一些有意义的好题目，你会发现它们有一个共同点：没有用到你不知道的知识，然而那个最关键的、攸关成败的知识点你就是想不到。所以你不禁要问，为什么明明这个知识在我脑子里（也就是说，明明我是“能够” 解决这个问题的），但我就是没法想到它呢？“你是怎么想到的？”这是问题解决者最常问的一个问题。甚至对于熟练的解题者来说，这个问题的答案也并不总是很明确的，很可能他们自己也不清楚那个关键的想法是怎么“蹦”出来的。我们在思考一个问题的时候，自己能意识到的思维部分似乎是很少的，绝大多数时候我们能感知到的就是一个一个的转折点在意识层面显现，我们的意识就像一条不连续的线，在其上的每一段之间那个空档内发生了什么我们一无所知，往往我们发现被卡在一个地方，我们苦思冥想，然后一个知识（也许是一个性质，也许是一个定理）从脑子里冒了出来，或者说，被我们意识到，然后我们沿着这条路走一段，然后又卡住，然后又等待一个新的关键知识的出现。而至于这些知识是怎么冒出来的？我们可以对它们的“冒出来”提供怎样的帮助？我们可以在意识层面做一些工作，帮助我们的下意识联想到更多重要的知识吗？那些灵光一现的瞬间，难道只能等待它们的出现？难道我们不能通过一些系统化的步骤去“捕获”或“生成”它们？又或者我们能不能至少做些什么工作以使得它们更容易发生呢？</p>
<p>正如金出武雄在《像外行一样思考，像专家一样实践》中所说的，人类的灵感一定是有规律的，认知科学目前至少已经确认了人类思维的整个物质基础——神经元。而既然它们是物质，自然要遵循物质的运行规律。只不过我们目前还没有窥破它们，但至少我们可以确信的是，它们在那里。事实上，不需要借助于认知科学，单单是通过对我们自己思维过程的自我观察，也许就已经能够总结出一些重要的规律了，也许，对自身思维过程的反观真的是人有别于其它动物的本质区别。</p>
<p><a href="http://www.douban.com/subject/2296845/">《专注力》</a>当中有这样一个例子：一天夜里，你被外面的吵闹声叫醒了，你出去一看，发现有一群人，其中有一个人开着很名贵的轿车，他跟你说他们正在玩一个叫“拾荒者”的游戏，由于一些原因，他必须要赢这个游戏，现在他需要一块1.5m*1m的木板，如果你能帮忙的话，愿以一万美元酬报。你怎么办？被测试的大多数人都没有想到，只要把门拆给他就可以了（如果你想到了，祝贺你:-)），也许你会说现在的门都是钢的，没关系，那你有没有想到床板、立柜的门、大桌子的桌面之类的？这个问题测试的就是心理学上所谓的“范畴陷阱”，“木板”这个名词在你脑子里的概念中如果是指“那些没有加工的，也许放在木材厂门口的，作为原材料的木板”的话，那么“木板”就会迅速在你的下意识里面建立起一个搜索范畴，你也会迅速的反应到“这深更半夜叫我上哪去找木板呢？”如果你一下就想到了，那么很大的可能性是“木板”这个概念在你脑子里的范畴更大，更抽象，也许包含了所有“木质的、板状的东西”。</p>
<p>这就是联想的法则。</p>
<p>我们的大脑无时无刻不在对事物进行归类，实际上，不仅是事物，一切知识，都在被自动的归类。在有关对世界的认知方面，被称为<a href="http://en.wikipedia.org/wiki/Schema_%28psychology%29">认知图式</a>，我们根据既有的知识结构来理解这个世界，会带来很大的优势。实际上，<a href="http://www.sciam.cn/article.php?articleid=334">模块化</a>是一个重要的降低复杂性的手段。然而，<b>知识是一把双刃剑，一方面，它们提供给了我们解决问题的无以伦比的捷径优势</b>，“砖头是砌墙的”，于是我们遇到砌墙这个问题的时候就可以迅速利用砖头。然而<b>另一方面，知识却也是思维的桎梏</b>。思维定势就是指下意识遵循既有知识框架思考的过程。上面的那个木板的例子也是思维定势的例子。每一个知识都是一个优势，同时又是一个束缚。著名的科幻作家<a href="http://en.wikipedia.org/wiki/Arthur_C._Clarke">阿瑟·克拉克</a>有一句名言：如果一位德高望重的老科学家说某个事情是不可能的，那么他很可能是错的。所以，如何在获取知识优势的同时，防止被知识束缚住，是一门技术。</p>
<p>掌握这门技术的钥匙，就是抽象。在吸收知识的时候进行抽象，同时在面对需要用到知识的新问题时也要对问题进行抽象。就以大家都知道的“砖头”有多少种用途为例，据说这道题目是用于测试人的发散思维的，能联想到的用途越多，思维定势就越小。实际上，借助于抽象这个利器，这类题目（乃至更广的一类问题）是可以系统性的进行求解的，我们只需对砖头从各个属性维度进行抽象。譬如，砖头是——长方形的（长方形的东西有什么用途？还有哪些东西也是长方形的，它们都有什么用途？）、有棱角的（问题同上）、坚硬的、固体、有一定大小的体积的、红色的、边界线条平直的、有一定重量的&#8230; 对于每一个抽象，我们不妨联想还有其他什么物体也是具有同样抽象性质的，它们具有同样的用途吗？当然，除了抽象之外，还有“修改”，我们可以在各个维度上对砖头的属性进行调整，以期得到新的属性：譬如大小可以调整、固体可以调整为碎末、棱角可以打磨、重量也可以调整、形状也可以调整&#8230; 然后看看新的属性可以如何联想开去。</p>
<p>除了这个简单的例子之外，我们也不妨看一看一些算法上的例子，同样一个算法，不同的人来理解，也许你脑子里记得的是某个特定的巧妙技巧（也许这个技巧在题目的某步关键的地方出现，从而带来了最令人意外的转折点），然而另一人个记得得也许是“递归”这种手法，还有另外一个人记得的也许是“分治”这种更一般化的解题思路。从不同的抽象层面去掌握这道题目的知识信息，以后遇到类似的问题，你能够想起这道题所提供的知识的可能性是有极大的差异的。《Psychology of Problem Solving》的第11章举了这样一个例子：先让被试（皆为大学生）阅读一段军事材料，这个材料是说一小撮军队如何通过同时从几个不同方向小规模攻击来击溃一个防守严实的军事堡垒的。事实上这个例子的本质是对一个点的同时的弱攻击能够集聚成强大的力量。然后被试被要求解决一个问题：一个医生想要用X射线杀死一个恶性肿瘤，这个肿瘤只可以通过高强度的X射线杀死，然而那样的话就会伤及周围的良好组织。医生应该怎么办呢？在没有给出先前的军队的例子的被试中只有10%想到答案，这是控制基线。然后，在先前学习了军队例子的被试中，这个比例也仅仅只增加到30%，也就是说只有额外20%的人“自动”地将知识进行了转移。最后一组是在提醒之下做的，达到了75%，即比“自动”转移组增加了45%之多。这个例子说明，知识的表象细节会迷惑我们的眼睛，阻碍我们对知识的运用，在这个例子中是阻碍问题之间的类比。</p>
<p>而抽象，则正是对非本质细节去枝减叶的过程，抽象是我们在掌握知识和解决问题时候的一把有力的<a href="http://en.wikipedia.org/wiki/Occam%27s_Razor">奥卡姆剃刀</a>。所以，无论是在解题还是在学习的过程中，问自己一个问题“<b>我是不是已经掌握了这个知识最深刻最本质的东西</b>”是非常有益的。</p>
<p><b>2. 知识，知识</b></p>
<p>如果你是一个熟练的解题者，你也许会发现，除了一些非常一般性的、本质的思维法则之外，将不同“能力”的解题者区分开来的，实际上还是知识。知识是解<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image010.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image010" border="0" alt="clip_image010" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image010-thumb.jpg" width="105" height="150" /></a>题过程中的<a href="http://en.wikipedia.org/wiki/Rosetta_Stone">罗塞塔碑石</a>。一道几何题为什么欧几里德能够做出来我们不能，是因为欧几里德比我们所有人都更了解几何图形有哪些性质，借助于一个性质，他很容易就能抵达问题的彼岸；反之，对于不知道某个性质的我们，倒过来试图“发现”需要这样的性质有时几乎是不可能的。有人说数学是在黑暗中摸索的学科，是有道理的。并不是所有的问题都能够通过演绎、归纳、类比等手法解出来的。这方面，费马大定理就是一个绝好的例子，<a href="http://www.douban.com/subject/1322358/">《费马大定理：一个困惑了世间智者358年的谜》</a>一书描述了费马大定理从诞生到被解决的整个过程，事实上，通过对费马大定理本身的考察，几乎是毫无希望解决这个问题的，我们根本不能推导出“好，这里我只需要这样一个性质，就可以解决它了”，也许大多数时候我们可以，但那或者是因为我们有已知的知识，或者这样的归约很显然。而对于一些致命的问题，譬如费马大定理，最重要的归约却是由别人在根本不是为了解决费马大定理的过程中得出来的。运气好的话，我们在既有的知识系统中会有这样的定理可以用于归约，运气不好的话，就得去摸索了。</p>
<p>所幸的是，绝大多数问题并不像费马大定理这样难以解决。而且绝大多数问题需要用到的知识，在现有的知识系统里面都是存在的。我们只要掌握得足够好，就有希望联想起来，并用于解题。</p>
<p>当然，也有许多题目，求解它们的那个关键的知识可以通过考察题目本身蕴涵的条件来获得，这类题目就是测试思维本身的能力的好题目了。而如果这个性质根本无法通过对题目本身的考察得出来，那么这个题目测试的就是知识储备以及联想能力。</p>
<p><b>3. 好题目、坏题目</b></p>
<p>在我看来，好题目即测试一个人思维的习惯的题目（因为知识性的东西是更容易弥补的，尤其是在这样一个年代；而好习惯不是一朝一夕养成的），它应有这样一些性质：</p>
<ul>
<li>不需要用到未知的知识，或者</li>
<li>需要用到未知的知识，但一个敏锐的解题者可以通过对题目的分析自行发现这些所需的知识。</li>
<li>考察解题的一般性思路，而不是特定（<a href="http://en.wikipedia.org/wiki/Ad_hoc">ad hoc</a>）的解题技巧，尤其是当这个技巧几乎不可能在短时间内通过演绎和试错发现的时候。譬如题目需要用到某种性质，而这个性质对于不知道它的人来说几乎是无法从对题目的考察中得出来的。</li>
<li>考察思维能力：联想能力、类比能力、抽象能力、演绎能力、归纳能力、观察能力、发散能力（思维不落巢臼的能力）。</li>
<li>考察一般性的思维方法：通过特例启发思考、通过试错寻找规律、通过泛化试探更一般性命题、通过倒过来推导将问题进行归约、通过调整（分解、删除、增加等等）题目的条件来感知它们之间的联系以及和结论的联系、通过系统化的分类讨论来覆盖每种可能性。</li>
<li>好题目举例：烙饼排序问题（考察特例启发法以及观察能力）、Nim问题（还有简单版本的取火柴问题）（烙饼排序问题和Nim问题可参见<a href="http://www.douban.com/subject/3004255/">《编程之美》</a>）、9公升4公升水桶倒6公升水的问题（考察倒过来思考问题的能力）、<a href="http://en.wikipedia.org/wiki/Thinking_outside_the_box">9点连线问题</a>、6根火柴搭出4个面的问题、“木板”问题（考察思维定势，此外《心理学与生活》的第九章也有好几个经典的问题）、许多数论问题（观察能力、演绎能力、归纳能力）。此外，<a href="http://groups.google.com/group/pongba">我们</a>最近也在<a href="http://blog.csdn.net/pongba/archive/2008/04/09/2270171.aspx">讨论好题目</a>。</li>
</ul>
<p>而坏题目呢：</p>
<ul>
<li>好题目各有各的好，坏题目都是相似的。</li>
<li>坏题目基本上就是指那些所谓的 unfair questions，什么是unfair，举个例子：一个人住在一栋非常高的楼上，每天早晨他乘电梯下到一楼，出门上班。但晚上回来之后却最多只能坐到一半高度的楼层，剩下一半只能走楼梯上去，除非是下雨天。问为什么。这个例子据说不少人小时候在脑筋急转弯里面做过，但我很怀疑基本上任何正常人是不是可能想出来。这个问题的问题在于他需要用到千百个有可能与问题有关的性质中的一个，而且这个性质还根本无法通过对题目本身的考察得出来，只可能某天我们碰巧遇到类似的场景也许才能想到。知道答案的人也许会说答案很显然，但别忘了心理学上的<b>事后偏见</b><b>——</b><b>一旦知道结果之后，所有指向结果的证据看上去都那么显然和充分，而同时所有反结果的证据看起来都那么不显然和不充分</b>。譬如这题关键是要想到这人是矮子和雨天要带伞，也许你会说“只要考虑一下电梯的按钮面板就会发现了”，或者“看到下雨，那还不想到带伞么？”，然而这只是事后的合情推断。在不知道答案的情况下，这个故事中有数不清的因素可能会成为问题的解释，除非某天我们碰到类似的问题，否则大致也只能一个个穷举了去使劲往上凑，譬如除了身高之外还有：是不是瞎子、是不是聋子、是不是哑子、男人女人、什么牌子的电梯、大厦是哪种大厦？这些因素重要吗？不重要吗？最令人头疼的是，在不知道答案的时候，我们也根本不知道他们重不重要，一个出谜语的人可能从任何一个微小的地方引申出某个谜语来；更头疼的是，我们不知道我们不知道的那些因素是不是也可能与题目的解有关，譬如这样一个问题：一个人走进酒吧，问酒保要一杯水，酒保掏出一只枪，拉上扳机；这人说声“谢谢”，走了出去。这些题目固然有趣，但几乎没有价值。</li>
<li>值得注意的是，这样的问题跟著名的<a href="http://en.wikipedia.org/wiki/Thinking_outside_the_box">9点连线问题</a>和6根火柴搭出4个面的问题（还有《如何解题：现代启发式方法》里面那个经典的“小球在盒内碰撞何时回到原轨迹”的问题）不同，后者的条件都在眼前，并且解的搜索空间无论如何很小，就看思维能不能突破某一个框框。而上面这些问题则是要人进行根本不可能的联想。9点问题实际上是可以系统化思考解决的，但unfair question则像许多谜语一样，随便哪个人都可以出一个另一个人根本无法想出来的谜语，因为从谜语隐含的信息加上人可能从谜语中联想出来的信息，加起来也不足以构成解题的充分条件；这种情况下除非你遇到出题人在出题时的心理或所处情况，否则是无法解的。</li>
<li>最后，发散性思维其实是可以系统化的，参见前文“联想的规则”。</li>
</ul>
<p>出题的误区：</p>
<ul>
<li>最大的误区就是把知识性的题目误当成能力型的题目。如果题目中需要用到某个重要的定理或性质，而对于一个原本不知道这个定理或性质的人来说是无法通过题目本身到达这个性质的，那这就属于知识性的题目。</li>
<li>虽然几乎所有题目归根到底都是知识性的，但有些题目更为知识性，尤其是当解题中需要用到的定理或性质并不那么trivial的时候。</li>
<li>一个最好的题目就是问题明明白白，而且最终的解也没有用到什么神秘的定理，但要想获知到解，取决于你会不会思考一个问题（参见“好问题”）。譬如烙饼问题和Nim问题，还有许许多多问题简洁明确但很锻炼思考的算法问题。</li>
</ul>
<p><b>4. 一个好习惯</b></p>
<p>在解题的过程中，除了必要条件——知识储备——之外，对于一些并不涉及什么你不知道的定理的题，很大程度上就要看思维能力或者习惯了。而在思考一个问题的时候，最容易犯的一类错误就是忘了考虑某种可能性，不管这种可能性是另一种做法（譬如只顾着构造一个能一步得出结果的算法，没记得还可以从错误情况逼近。譬如只顾着正着推导，却忘了可以反过来推。只顾着反过来推，居然忘了可以考察简单特例。试了各种手法，却发现忘了考虑题目的某个条件。觉得试遍了所有可能性，已经走不下去了，然后其实在思维的早些时候就已经落入了思维陷阱。等等）事实上，即便是一个熟练的解题者也容易犯顾此失彼的问题，因为我们一旦意识到一个看似能够得到结论的解法，整个注意力就容易被吸引过去，而由于推导的路径是很长的，所以很容易在一条路上走到黑，试图再往下走一步就得出解。却忘了回过头来看看再更高的层面上还有没有其它手法，思路上有没有其他可能性。</p>
<p>而对于像我这样目前尚不谙熟所有思维方法的人来说，则更容易犯这样的错误。为了避免这样的错误，一个有效的办法就是将自己的思考过程（中的重要环节）清晰的写在纸上（称为“看得见的思考”），这有如下几个好处：</p>
<ul>
<li>人在思考一个问题的时候，就像是在黑暗中打着电筒往前走（事实上，我们的<a href="http://en.wikipedia.org/wiki/Working_memory">工作记忆</a>资源是有限的，有研究证明我们只能在工作记忆里面持有<a href="http://en.wikipedia.org/wiki/The_Magical_Number_Seven%2C_Plus_or_Minus_Two">7加减2个项目</a>；此外<a href="http://en.wikipedia.org/wiki/Cognitive_load">认知负荷</a>也是有极限的），每一步推导，每一步逻辑或猜测都将我们往前挪一步，然而电筒的光亮能找到的范围是有限的，我们走了几步发现后面又黑了。有时候，我们是如此努力地试图一下就走出很远，同时又老是怕忘记目前已经取得的进展和重要结论，结果意识的微光就在一个很小的范围内打转，始终无法往前走出很远。而将思维过程记录下来，则给了我们完全的回顾机会。如果你是经常做笔记的人，你肯定会发现，有时候一个在脑子里觉得两句话就能说完不需要记下来的东西，一旦开始往纸上写下来，你就自然而然能得出更多的结论和东西，越写越多，最终关于你的问题的所有方面都被推导出来展现在你面前。</li>
<li>思考问题时的注意力是<a href="http://en.wikipedia.org/wiki/Attention#Neural_correlates_of_attention">自上而下控制大脑的神经处理过程</a>的，当我们集中注意力在某一个过程上时，其它的过程就会受到抑制。我们平常都遇到过一些时候，由于集中注意力从而忽略了周围发生的事情的时候（处理环境输入的神经回路受到抑制）。所以，当我们竭尽全力将一些非常重要的因素控制在工作记忆里面的时候，实际上很大程度上抑制了其余的思考——可以想见如果科比在跳投的瞬间集中注意力思考跳投的各种技术要素的话会发生怎样的灾难。此外，这么做还占用了宝贵的工作记忆空间，从这个意义上，借助于纸笔，将思考的东西写下来实际上就是扩充了我们的工作记忆，增大了思维的缓存。注意，这倒不是说思考问题不需要集中注意力，而是说由于将项目维持在工作记忆中需要很大的认知精力，使得我们的注意力无法暂时移开去思考其它相关的子问题，而写到纸上的话我们就减轻了工作记忆的负担，可以转移注意力去集中思考某个子问题；同时我们又可以随时回过头来，重新将以前想过的结论装入记忆（内存），完全不用费心去阻止它们被我们的工作记忆遗忘。</li>
<li>一句话从嘴里说出来，或者写到纸上，被视觉或听觉模块接收，再认知；跟在心里默念所产生的神经兴奋程度是不一样的。我们都有过这样的经历：一句令人不愉快的话，我们心里清楚，但就是不愿意自己也不愿意别人从嘴里说出来。同样，将思考的过程写到纸上，能够激起潜在的更多的联想。为什么会这样的另一个可能的原因是我们大脑中思维过程的呈现形式和纸上的表现形式是不一样的，既没有那么严格、详细也没有那么多的符号（如数学符号）——再一次，工作记忆资源是很有限的——而后者，作为视觉线索，可能激起更多对既有知识的回忆。</li>
<li>我们在思考问题的过程中容易落入思维定势，不知不觉就走上来某条“绝大部分时候是如此”的思维捷径，对于一些问题而言这固然能够让我们快速得到解，但对于另一些问题而言却是致命的。我们容易在逻辑的路径上引入想当然的假设，从而排除某种不该排除的可能性或做法。通过将思路过程写到纸上，我们便能够回头细细考察自己的思考过程，觉察到什么地方犯了想当然的毛病。</li>
<li>我们在思维过程中的每一个关键的一步也许都有另一种可能性，一个问题越复杂，需要推导的步骤就越多，我们就越容易忽视过程中的其它可能性，容易一条路走到黑。而将思维过程写下来，在走不下去的时候可以回过头看看，也许会发现另一种可能性，另一条“少有人走的路”。</li>
<li>最后，通过将思维过程写下来，我们就能够在解题完毕之后完整的回顾自己的整个思维过程，并从中再次体悟那些关键的想法背后所发生的心理活动过程，总结思考中的重要的一般原则，分析思维薄弱的环节，等等。就算是最终发现并没有到达结果的无效思路，也未必就没有意义，因为不是因为错误的思路，也不会知道正确的思路，况且对一道题目用不上的思路，对其它题目未必用不上。通过对自己思维过程的彻底反思，就能从每次解题中获得最多的收获。</li>
</ul>
<p><strong>5. </strong><strong>练习，练习</strong></p>
<p>本质上，练习并不产生新能力。然而练习最重要的一个作用就是将<a href="http://en.wikipedia.org/wiki/Explicit_memory">外显记忆</a>转化为<a href="http://en.wikipedia.org/wiki/Implicit_memory">内隐记忆</a>。用大白话来说就是将平时需要用脑子去想（参与）的东西转化为内在的习惯。譬如我们一开始学骑自行车的时候需要不断提醒自己注意平衡，但随着不断的联系，这种技能就内化成了所谓的<a href="http://en.wikipedia.org/wiki/Procedural_memory">程序式记忆</a>（内隐记忆的一种），从而就算你一边骑车一边进行解题这样需要消耗大量脑力的活动，也无需担心失去平衡（不过撞树是完全可能的，但那是另一回事）。</p>
<p>同样，对于解题中的思维方法来说，不断练习这些思维方法就能做到无意识间就能运用自如，大大降低了意识的负担和加快了解题速度。</p>
<p>不过，并非所有的练习方法都是等效的，有些练习方法肯定要比另一些更有效率。譬如就解题来说，解题是一项涉及到人类最高级思维机制的活动，其中尤其是推理（归纳和演绎）和联想。而后者中又尤数联想是最麻烦的，前面提到，绝大多数时候启发式方法实质上都是在为联想服务——为了能像晃筛子那样把你脑袋里那个关键的相关知识抖落出来。并且，为了方便以后能够联想，在当初吸收知识的时候就需要做最恰当的加工才行，譬如前面提到的“抽象”加工，除此之外还有将知识与既有的知识框架整合，建立最多的思维连接点（或者说“钩子”）。对于知识的深浅加工所带来的影响，<a href="http://www.douban.com/subject/1315575/">《找寻逝去的自我》</a>里面有精彩的介绍（里面也提到了提取线索对回忆的影响——从该意义上来说运用启发式思维方法来辅助联想，其实就是进行策略性记忆提取的过程）。最后，人类的无意识思维天生有着各种各样的坏习惯，譬如前面提到的范畴陷阱就是创新思维的杀手，譬如根据表面相似性进行类比也是知识转移的一大障碍。更遑论各种各样的<a href="http://www.douban.com/doulist/127649/">思维捷径</a>了（我们平常进行的绝大多数思考和决策，都是通过认知捷径来进行的）。所以说，如果任由我们天生的思维方式发展，也许永远都避不开无意识中的那些陷阱，好在我们除了无意识之外还多出了一层监督机制——意识。通过不断反省思维本身，时时纠正不正确的思考方式，我们就能够对其进行淬炼，最终养成良好的思维习惯。反之被动的练习虽然也能熟能生巧，但势必花的时间更多，而且对于涉及复杂的思维机制的解题活动来说，远远不是通过钱眼往油壶里面倒油这样简单的活动所能类比的，倒油不像思维活动那样有形形色色的陷阱，倒油不需要联想和推理，倒油甚至几乎完全不需要意识的辅助性参与，除了集中注意力（而解题活动就算对于极其熟练的人来说也不断需要大量的意识参与）。所以对于前者，良好的思维习惯至关重要，而反省加上运用正确的思维方法则是最终养成良好思维习惯的途径。</p>
<p>练习还有另外一个很重要的作用，就是增加领域知识（关于知识在问题解决中的作用，前面已经提到过）。我们看到很多人，拿到一道题目立即脑子里就反应出解法，这个反应快到他自己都不能意识到背后有什么逻辑。这是因为既有的知识（我们常说的“无他，实在是题做得太多了”）起到了极大的作用，通过对题目中几个关键元素或结构的感知，大脑中的相关知识迅速被自动提取出来。而对于知道但不熟悉相应知识（譬如很早我们就知道归纳法，但是很久以后我们才真正能够做到面对任何一道可能用归纳法的题目就立即能够想到运用归纳法），或者干脆就不知道该知识的人来说，就需要通过启发法来辅助联想或探索了。后者可以一定程度上代偿对知识的不够熟悉，但在一些时候知识的缺失则是致命的（参见上面第2点）。不过要注意的是，那种看到题目直接反应出答案的或许也不是纯粹的好事，因为这样的解题过程严重依赖于既有知识，尤其是做过的类似的题目，其思维过程绝大部分运用的是联想或类比，而非演绎或归纳。更重要的是，联想也分两种，被动联想和策略性联想（参考《找寻逝去的自我》），这里用的却是被动联想。所以，能直接反应出答案并不代表遇到真正新颖的题目的时候的解决能力，后者由于不依赖于既有领域知识，就真正需要看一个人的思维能力和习惯究竟如何了。</p>
<p><strong>6. </strong><strong>启发法的局限性</strong></p>
<p>首先肯定的是，启发法一定（也许很大）程度上是可以代偿知识的不足的（这里的知识主要是指大脑中的“联系”，下面还会提到另一种知识，即hard knowledge）。譬如，一道题目，别人直接就能通过类比联想到某道解过的题目，并直接使用了其中的一个关键的性质把题目给解出来了。你并没有做过那道题目，这导致两种可能的结果：一，你就是不知道那个性质。二，你虽然“知道”那个性质，但并没有在以前的解题经历中将那个性质跟你手头的这个问题中的“线索”联系起来，所以你还是“想不到”。后一种可以称为soft knowledge，即你“知道”，但就是联想（联系）不起来。所谓不能活学活用，某些时候就是这种情况，即书本上提供什么样的知识联系，脑子里也记住什么，而没有事后更广泛地去探索知识之间的本质联系（总结的作用）。前一种则可以称为hard knowledge，即你就是不知道，它不在你的脑子里。</p>
<p>而启发式方法在两个层面上起作用：</p>
<ul>
<li><strong>辅助联想起soft knowledge</strong>：譬如，特例法是一种启发式思考方法，它通过引入一个简单的特例，特例中往往蕴含有更多的“线索”，通过这些线索，有可能就会激发起对既有的知识的联想。另外一种强大的辅助联想办法就是对题目进行变形，变形之后就产生了新的视觉和语意线索，比如式子的对称性、从直角坐标到极坐标从而引发对后者的知识的联想等等。大量的启发式方法实际上的作用就是辅助联想，通过对题目中的线索的发掘，激起大脑中已知相关知识的浮现。在这个意义上，相对于那些能够直接联想到某个性质的人，那些不知道但可以通过启发式思维联想到的，启发式思维就提供了一种“曲径通幽”的策略性联想。还是以经典的例子来说：砖头的用途。有人立即能够直接联想到“敲人”。有人也许不能。然而启发式联想策略“抽象”就能够帮助后者也能够联想到“敲人”，因为“抽象”策略启发人去考虑砖头的各个性质维度，如“质地”，“形状”，当你考察到“质地坚硬”，“棱角”，离“敲人”的功能还会远么？本质上，能够直接联想到“敲人”功能的人是因为大脑中从砖头到敲人这两个概念之间的神经通路被走过了很多遍（譬如由于经常拿砖头敲人），神经元之间的联系相当“粗”（形象的说法，严格的事实请参考《追寻记忆的痕迹》），而不经常拿砖头敲人的人呢，这个联系就非常的弱，乃至于根本激不起一次神经冲动。那么为什么通过启发式方法又能联想到呢？因为启发式方法相当于带入了一种新的神经调控回路，首先它增加你联系到砖头的属性维度上的可能性，使得“质地坚硬”、“棱角”这两个语意概念被激活起来（注意，如果没有启发式方法的参与，这是不会发生的），一旦后者被激活起来，从后者到“敲人”的联系就被激活起来了。从本质上，解题中的启发联想方法做的也就是这个工作。而越是一般性的启发式方法就越是能对广泛的问题有帮助（譬如《How to Solve It》中介绍的那些，譬如分类讨论、分治、乃至我认为很重要的一个——写下自己的思维过程，详细分解各个环节，考察思维路径中有无其它可能性（我们很容易拿到一道题目便被一种冲动带入到某一条特定的思路当中，并且遵循着“最可能的”推导路径往下走，往往不自觉的忽略其它可能性，于是那些可能性上的联想就被我们的注意力“抑制”了。））。</li>
<li><strong>辅助探索出hard knowledge</strong>：倒推法是一种启发式思考方法，它将你的注意力集中到问题的结论中蕴含的知识上，一旦你开始关注可能从结论中演绎出来的知识，你就可能得到hard knowledge，即并不是早先就存在你脑子里，但是可以通过演绎获得的。上文中的最小和子序列中的倒推方法就是一个例子。</li>
</ul>
<p>而启发式方法的局限性也存在于这两个方面：</p>
<ul>
<li><strong>有些联系是不管怎样“</strong><strong>启发”</strong><strong>也想不起来的。</strong>譬如“当布被刺破了，干草堆就重要了”，你怎么解释这句话？如果有人提示一下“降落伞”，每个人都会恍然大悟。这是因为从“布”到“降落伞”之间的单向联系是近乎不存在的。而且就算运用启发法，譬如，考虑所有布做的东西，也基本绝无可能想到降落伞，因为同样，从“布做的东西”到“降落伞”之间的关联也是极其微弱的。我们脑子里只能保留那些最最重要的联系。（如果一提到布，“降落伞”和“衣服”、“被单”、“窗帘”等日常物品以同等重要级别闪现，就乱套了。）那为什么从降落伞我们能想到布呢？我们实际上不能，我们为什么有些时候能，是因为譬如有人叫你“考虑降落伞的材料”，后者就激发了“降落伞之材料”这个语意，后者又指导了我们去考察降落伞的材料构成，于是我们想到是布。否则“布”是不会直接被激发起来的。那为什么在我们的这个问题中，一旦有人提到降落伞，我们就能建立从布到降落伞的关联呢？这是因为“降落伞”和“布”这两个语意单元的同时兴奋增大了它们之间关联的可能性，就好比是加大另一端的电压从而发生了“击穿”一样。从本质上，解数学题也是如此，费马大定理的求解过程是一个很好的例子，谷山志村猜想，就相当于那个“降落伞”的提示。我们还听到很多这样的故事（或者自己经历）：苦思冥想一个问题不得要领，某一天在路上走，看到某个东西或听到某句话，然后忽然，一道闪电划破长空，那个问题解开了（阿基米德是因为躺在浴缸里从而想到浮力原理的吗？）。我敢保证，如果一个人早就把那个问题从脑海里扔到九霄云外去了（不再处于兴奋状态了），那么就算线索出现，也是不可能发生顿悟的。我们都知道，带着一个问题（使其在大脑中处于兴奋状态）去寻找答案更可能找到，即便不是有意去寻找，只要问题还在脑子里，任何周围的有可能与它相关的线索都不会被大脑漏掉，因为“问题”和“周围的其他线索”同时的兴奋增大了关联的可能性。如果问题早就被从大脑（意识或者潜意识）中撤下了，即便周围出现提示也不会被捕捉到。</li>
<li><strong>许多hard knowledge</strong><strong>是不能被启发探索出来的。</strong>至少是不能被“直接命中目标”地探索出来的。一个问题有可能跟三角函数有关，也许你只能带着问题去探索三角函数的所有性质，从而最终发现那个关键的性质。费马大定理与椭圆方程有关，也许只能去探索椭圆方程的所有性质，这个过程一定程度上是盲目的，试错的，遍历的。而不是直接面向目标的。再聪明的人也无法从费马大定理直接反推到谷山志村猜想。在这些时候，启发式方法最多只能提供一个探索的大致方向：譬如，探索三角函数的性质，并随时注意其中哪个可能对我这个问题有帮助。譬如，探索模运算的性质，看看哪些性质可能会有用。譬如，探索椭圆曲线的性质&#8230;等等。启发式方法并不能使我们的探索精准地命中目标。而只能划定一个大致的范围。也难怪有人说数学是盲目的。</li>
</ul>
<p>但话说回来，启发式方法的局限性并不能否认在大量场合启发式方法的巨大帮助，许多时候，单靠启发式方法就能带来突破。而且，一旦知识性的东西掌握的是<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image011.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image011" border="0" alt="clip_image011" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image011-thumb.jpg" width="105" height="150" /></a>一样多的，能否运用更优秀的思维方法就决定了能力的高下。有很多<a href="http://www.douban.com/people/pongba/booktags/%E6%80%9D%E7%BB%B4">介绍思维方法的书</a>。</p>
<p><strong>7. </strong><strong>总结的意义</strong></p>
<p>解题练习的最重要目的不是将特定的题目解出来，而是在于反思解题过程中的一般性的，跨问题的思维法则。简单的将题目解出来（或者解不出来看答案，然后 “恍然大悟”），只能得到最少的东西，解出来固然能够强化导致解出来的那个思维过程和方法，但缺少反思的话便不能抽取出一般性的东西供更多的题目所用。而解不出来，看答案然后“哦”的一声更是等同于没有收获，因为“理解”和“运用”相差何止十万八千里。每个人都有过这样的经历：一道题目苦思冥想不得要领，经某个人一指点其中的关键一步，顿时恍然大悟——这是理解。但这个理解是因为别人已经将新的知识（那个关键的一步）放到你脑子里了，故而你才能理解。而要运用的话，则需要自己去想出那关键的一步。因此，去揣测和总结别人的思维是如何触及那关键的一步，而你自己的思维又为什么触及不到它，有一些一般性的原则可以指导你下次也能想到那个“关键的一步”吗，是很有意义的。我们很多时候会发现，一道题目，解不出来，最终在提示下面解出来之后，发现其中并没有用到任何自己不知道的知识，那么不仅就要问，既然那个知识是在脑子里的，为什么我们当时愣是提取不出来呢？而为什么别人又能够提取出来呢？我怎么才能像别人那样也提取出相应的知识呢？实际上这涉及到关于记忆的最深刻的<a href="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image012.jpg"><img style="border-bottom: 0px; border-left: 0px; display: inline; margin-left: 0px; border-top: 0px; margin-right: 0px; border-right: 0px" title="clip_image012" border="0" alt="clip_image012" align="right" src="http://mindhacks.cn/wp-content/uploads/2009/02/clip-image012-thumb.jpg" width="103" height="153" /></a>原理，实际上文中已经提到了一些。（有兴趣的建议参考以下几本书：<a href="http://www.douban.com/subject/1944205/">《追寻记忆的痕迹》</a>，<a href="http://www.douban.com/subject/1315575/">《找寻逝去的自我》</a>，<a href="http://www.douban.com/subject/2345245/">《Synaptic Self》</a>，<a href="http://www.douban.com/subject/2845839/">《Psychology of Problem Solving》</a>）一般性的思维法则除了对于辅助联想（起关键的知识）之外，另一个作用就是辅助演绎/归纳（助探），一开始学解题的时候，我们基本上是先读懂题目条件，做可能的一些显然的演绎。如果还没推到答案的话，基本就只能愣在那里等着那个关键的步骤从脑子里冒出来了。而所谓的启发式思维方法，就是在这个时候可以运用一些一般性的，所有题目都适用的探索手法，进一步去探索问题中蕴含的知识，从而增大成功解题的可能性。启发式的思维方法有很多，从一般到特殊，最具一般性的，在波利亚的《How to Solve It》中已经基本全部都介绍了。一些更为特殊性的（譬如“如果全局搜索空间没有递归结构，那么考虑分割搜索空间”，譬如那些“看到XX，要想到YY”的联系），则需要自己在练习中不断抽象总结。</p>
<p><b>一句结尾</b></p>
<p>“我想我就在这里结束”——如果你知道我在说什么的话:-)</p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/" title="知其所以然（三）：为什么算法这么难？">知其所以然（三）：为什么算法这么难？</a> (57)</li><li><a href="http://mindhacks.cn/2011/01/23/escape-from-your-shawshank-4/" title="逃出你的肖申克（四）：理智与情感">逃出你的肖申克（四）：理智与情感</a> (125)</li><li><a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/" title="知其所以然（续）">知其所以然（续）</a> (42)</li><li><a href="http://mindhacks.cn/2010/03/18/escape-from-your-shawshank-part3/" title="逃出你的肖申克（三）：遇见20万年前的自己">逃出你的肖申克（三）：遇见20万年前的自己</a> (112)</li><li><a href="http://mindhacks.cn/2009/03/28/effective-learning-and-memorization/" title="[BetterExplained]如何有效地记忆与学习">[BetterExplained]如何有效地记忆与学习</a> (102)</li><li><a href="http://mindhacks.cn/2009/03/15/preconception-explained/" title="逃出你的肖申克（二）：仁者见仁智者见智？从视觉错觉到偏见">逃出你的肖申克（二）：仁者见仁智者见智？从视觉错觉到偏见</a> (147)</li><li><a href="http://mindhacks.cn/2009/02/15/why-you-should-start-blogging-now/" title="[BetterExplained]为什么你应该（从现在开始就）写博客">[BetterExplained]为什么你应该（从现在开始就）写博客</a> (200)</li><li><a href="http://mindhacks.cn/2009/02/09/writing-is-better-thinking/" title="[BetterExplained]书写是为了更好的思考">[BetterExplained]书写是为了更好的思考</a> (90)</li><li><a href="http://mindhacks.cn/2009/02/07/better-explained-conflicts-in-intimate-relationship/" title="[BetterExplained]亲密关系中的冲突解决">[BetterExplained]亲密关系中的冲突解决</a> (50)</li><li><a href="http://mindhacks.cn/2009/01/18/escape-from-your-shawshank-part1/" title="逃出你的肖申克（一）：为什么一定要亲身经历了之后才能明白？">逃出你的肖申克（一）：为什么一定要亲身经历了之后才能明白？</a> (63)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2008. | <a href="http://mindhacks.cn/2008/04/18/learning-from-polya/#commenting">21 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e6%80%9d%e7%bb%b4%e6%94%b9%e5%8f%98%e7%94%9f%e6%b4%bb/" rel="tag">思维改变生活</a>, <a href="http://mindhacks.cn/tags/%e7%ae%97%e6%b3%95/" rel="tag">算法</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2008/04/18/learning-from-polya/">原始超链接</a>: http://mindhacks.cn/2008/04/18/learning-from-polya/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2008/04/18/learning-from-polya/feed/</wfw:commentRss>
		<slash:comments>21</slash:comments>
		</item>
		<item>
		<title>数学之美番外篇：进化论中的概率论</title>
		<link>http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/</link>
		<comments>http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/#comments</comments>
		<pubDate>Sun, 02 Dec 2007 10:55:00 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[数学]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[概率论]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/</guid>
		<description><![CDATA[偶然性在进化中确实存在（例如，偶然性的突变可以产生新的特征），但是进化并不依赖偶然性来产生新的器官、蛋白质或其他实体。截然相反的是，自然选择，作为进化中已知的最主要机制，却会明确保留“需要的”（能适应的）特性，消除“不需要的”（无法适应的）特性。只要选择的影响力存在，自然选择就能把进化向一个方向推进，在出乎意料的短时间内产生复杂的结构。举个例子，现有由13个字母构成的序列“TOBEORNOTTOBE”，假设有几百万只猴子，每只猴子每秒钟挑一条短语，需要78,800年才能从26^13种可能中选出这样的排列。不过，Glendale College的Richard Hardison在20世纪80年代写过一个程序，它能够在随机产生序列的同时，保证那些已经出现在正确位置上的字母不会变化（这样做倒有点《汉姆雷特》 的味道。译注：这个句子看了大半天才明白，嘿嘿）。这个程序平均只需要336次迭代就能生成上文提到的短语，时间少于90秒。更神奇的是，把莎士比亚的整个剧本重新生成一遍也只需要四天半时间。]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.xiaolai.net/">李笑来</a>老师<a href="http://www.xiaolai.net/?p=565">在blog上转了</a>一篇宏文，“<a href="http://www.sciam.com/article.cfm?articleID=000D4FEC-7D5B-1D07-8E49809EC588EEDF">15 Answers to Creationist Nonsense</a>”；然后<a href="http://www.luanxiang.org/">余晟</a>同学（顺便推荐<a href="http://www.luanxiang.org/">余晟</a>同学译的<a href="http://www.douban.com/subject/2154713/">《精通正则表达式》（第3版）</a>）把它给<a href="http://www.luanxiang.org/blog/?p=348">译了出来</a>。漂亮的文章加上漂亮的翻译，当然是要拜读的:-)</p>
<p>进化论从其诞生以来受到的非难不计其数。这里提到的这篇便收集了广为神创论者提出以及广为大众误解的一些观点。其中有一点尤其引起了我的兴趣，如下：</p>
<blockquote><p><b>8. </b><b>严格说起来，我们很难相信复杂如蛋白质的物质能偶然出现，更不用说人或是活细胞了。</b><b></b></p>
<p>偶然性在进化中确实存在（例如，偶然性的突变可以产生新的特征），但是进化并不依赖偶然性来产生新的器官、蛋白质或其他实体。截然相反的是，自然选择，作为进化中已知的最主要机制，却会明确保留“需要的”（能适应的）特性，消除“不需要的”（无法适应的）特性。只要选择的影响力存在，自然选择就能把进化向一个方向推进，在出乎意料的短时间内产生复杂的结构。举个例子，现有由13个字母构成的序列“TOBEORNOTTOBE”，假设有几百万只猴子，每只猴子每秒钟挑一条短语，需要78,800年才能从26^13种可能中选出这样的排列。不过，Glendale College的Richard Hardison在20世纪80年代写过一个程序，它能够在随机产生序列的同时，保证那些已经出现在正确位置上的字母不会变化（这样做倒有点《汉姆雷特》 的味道。译注：这个句子看了大半天才明白，嘿嘿）。这个程序平均只需要336次迭代就能生成上文提到的短语，时间少于90秒。更神奇的是，把莎士比亚的整个剧本重新生成一遍也只需要四天半时间。</p>
</blockquote>
<p>关于这个随机枚举特定的13字母的单词的问题有点意思。如果是纯粹随机枚举的话，由于长度为13的单词一共有26^13个不同的（假设所有组合都是有效单词的话），其中只有一个跟目标单词一样，也就是说平均（数学期望）枚举26^13次才能枚举出目标串来。</p>
<p>我们不妨把人类的DNA链当成一个长长的单词。突变是产生随机枚举的动力。那么根据上面的分析，要枚举出我们现在用的DNA，需要的迭代次数将是跟DNA链上的“字母数”（碱基对）成指数关系的。枚举一个13个字母的单词就需要26^13次方了，上亿碱基对，需要多少次迭代？更不要说人类的一代更迭就平均要耗上十来二十年了。</p>
<p>从这个角度来看，作为生命只有短短几十年的我们，似乎的确很难理解像眼睛这么复杂精妙的结构是如何从随机的突变中产生出来的。而这也恰恰是神创论者最经常用来非难进化论的论点之一。那么，这个貌似有力的论点到底正确与否呢？其实，在达尔文同学的<a href="http://www.douban.com/subject/1969983/">《物种起源》</a>中就已经进行了一定程度的驳斥。达尔文同学列举了一系列的比我们人类眼睛简单的眼睛结构，从复杂逐渐到简单，其中最简单的“眼睛”只由一些聚集在一起的感光细胞构成；并且，达尔文同学还雄辩了为什么眼睛的复杂性并非是不可归约（irreducible）的。</p>
<p>然而，达尔文同学毕竟不是专业的理工科出生（在大学里面是学神学的），虽然其实践精神是每个科学家的楷模，虽然在《物种起源》中他运用了一系列的证据和推理，某种程度上论证了随机突变加上自然的选择之手，的确能够进化出如眼睛这么复杂的结构。然而他并没有从数学上加以证明，为什么定向选择能够导致在短得多的时间内产生复杂结构，以及这个时间与纯随机枚举相比到底短到什么程度？</p>
<p>我们仍以那个13个字母的单词为例TOBEORNOTTOBE。如果是纯随机枚举的话，平均需要26^13次方才能枚举出一个来。那么，自然中的进化过程也是这样的吗？并非如此。虽然每一个基因位都可能发生变异，然而自然选择之手会将那些“好”的部分留下来，差的部分剔掉（因为突变出来的好基因比差基因更有生存繁殖优势，于是渐渐就会在种群中通过遗传扩散开来）。反映在我们枚举单词的过程中就是，一旦我们枚举出了某个或某些特定位上的字母，那么这个字母就确定下来，不再变动，只需继续枚举剩下来的字母。这样，直觉上需要枚举的次数就会大大减少。而实际上也正是如此，引文中提到的Richard同学写的一个程序便说明了这一点：本来需要78,800年，现在只需90秒。差异何等巨大！</p>
<p>现在，我们关心的问题是，按照后一种枚举方法，能从数学上精确计算出来，要枚举出这个目标单词需要的迭代次数的数学期望吗？（即，平均枚举多少次，才能枚举出它呢？）</p>
<p>Richard同学写的那个程序显示需要平均336次；然而，问题是他写的是一个程序，而不是用数学来计算到底需要多少次。336次是数学期望吗？不是。它是一个实践值。</p>
<p>实际上，我也写了一个这样的程序，然而结果显示却是大约82次左右。那么到底谁的正确呢？</p>
<p>要检验这个结论，更重要的问题是，应该有一个数学方法能够计算出按照这种方法，可（数学）期望通过迭代多少次就迭代出目标串。</p>
<p>为了从数学上解决这个问题。我们需要用到一点基本的概率论知识：</p>
<p>如果一个随机变量X的值为x<sub>i</sub>的几率是P<sub>i</sub>，那么X的数学期望Ex就等于∑x<sub>i</sub>P<sub>i</sub>。举个例子，假设小明的考试成绩为90分的概率为30%，为80分的概率为70%，那么小明的成绩的数学期望便是90×30%+80×70%=83（虽然83其实是个不可能的成绩）。</p>
<p>回到我们要求解的问题：我们想知道需要枚举多少次才能枚举出目标单词。要求解这个问题，我们可以反过来思考：<b>平均每枚举一次能正确枚举出目标单词上的几个字母</b>（这里“正确”的意思当然是要满足“在相同位置上的字母也相同”，比如目标单词是TUBE的话，一旦枚举出POTE，我们就说正确枚举出了最后一个字母E，而T由于位置不对应，因此就不能算是正确的了）。</p>
<p>很显然，平均一次能正确枚举出的字母数目是一个随机变量，不妨令为X。该随机变量依赖于在这次枚举中，目标单词上的每一个位置上的字母是否被正确枚举出来了，于是我们设目标单词第i位上的字母被枚举的结果（即是否枚举中——只有“中”或“不中”两种结果，因而是一个二元随机变量）为随机变量X<sub>i</sub>；X<sub>i</sub>只有“中”或“不中”两种可能，我们将“中”的值量化为1，“不中”为0。由于每一位上枚举中的概率都是独立同分布的，因此对于任意一个X<sub>i</sub>来说，为0（“不中”）的概率皆为25/26；为1（“中”）的概率皆为1/26。这很容易理解，因为字母表中一共有26个字母，随机选择一个，跟目标字母相同的概率自然是1/26，不同的概率则是25/26。</p>
<p>有了X<sub>i</sub>，我们进一步发现，X其实是X<sub>i</sub>的函数：X = ∑X<sub>i</sub>。这个式子这样理解：如果在位置i处枚举中了，那么X<sub>i</sub>便是1，这样就给总共枚举中的位数X贡献了1；否则X<sub>i</sub>则为0，即没有贡献。</p>
<p>现在，我们回过头审视我们想要求得的东西：我们想求得是<b>枚举一次能正确枚举出目标单词上的字母数目的数学期望</b>。也就是X的数学期望EX。由于X = ∑X<sub>i</sub>。于是EX = E(∑X<sub>i</sub>) = ∑EX<sub>i</sub>。而EX<sub>i­</sub>对每个X<sub>i</sub>是相同的（独立同分布嘛），都是0×25/26 + 1×1/26 = 1/26。因此EX = n×EX<sub>i</sub>（其中n是目标单词的长度——本例中是13）= 13×1/26 = 1/2。</p>
<p>综上，我们得出结论：随机枚举一次可（数学）期望枚举中目标单词上的1/2个字母。</p>
<p>1/2个字母？是不是开玩笑？哪有“半个字母”的说法？实际上，因为是数学期望，而数学期望的值很可能并非所有可能值中的任一个，而是它们的概率加权平均，所以半个字母的说法在数学期望上是说得通的；更关键的是，这个期望值给我们提供了一个极其重要的信息，那就是<b>要想枚举中其中的一位，我们（数学期望上）需要枚举</b><b>2</b><b>次才行（因为每次枚举中</b><b>1/2</b><b>位）</b>。</p>
<p>一旦枚举中了其中的一位，那么后面的随机枚举过程便不需要考虑这一位，只需要考虑剩下的了。换句话说，目标单词中的字母便被剔掉了一个，只剩12个字母。而在12个字母的单词中，要想再枚举中一位，需要多少次迭代呢？重用上面的推导过程，EX仍然还是等于n×EX<sub>i</sub>。EX<sub>i</sub>没变，而n变成了12。即迭代一次平均命中12/26个字母，那么要完全命中其中一位字母，便需要26/12（即2.17次）。</p>
<p>如此类推，每次减掉其中的一个字母需要特定次数的枚举，一直到减至只剩最后一个字母，需要26次。把所有这些枚举次数的期望值加起来，便是总共需要枚举的次数了。即26/13 + 26/12 + 26/11 + … + 26/1 ~= 82.68次。</p>
<p><b>另一种思路</b><b></b></p>
<p>我们回顾一下上一个解法过程中的核心问题：<b>要命中其中的一个字母，（数学期望上）需要枚举多少次</b>，我们令这个次数为随机变量X。我们回顾一下数学期望的本质定义：每个可能的值的概率加权平均。于是，要求得X的数学期望EX，我们只需知道X所有可能的取值以及对应于各个取值的概率。</p>
<p>那么，要命中其中一个字母，究竟需要枚举多少次呢？可能是一次就中了，也可能需要两次，也可能需要三次…你会发现，有可能需要任意次。只不过随着所需次数的增加，概率也越来越小。实际上，这是一个无穷级数求和问题；所幸，你马上就会看到，这个求和其实很简单。</p>
<p>所谓一次就命中的意思是，只随机枚举一次，就会命中目标单词中的一个且仅一个字母。这个随机事件由三个部分组成，首先是其余n-1个字母不中，然后是剩下的1个字母中了，再然后是那个命中的字母有n种可能的位置。因此，其概率是[(25/26)^(n-1)]×1/26×n。同理，需要两次才能命中其中一个字母的概率是(25/26)^n×1/26×n×[(25/26)^(n-1)]…以此类推。</p>
<p>而命中其中一个字母所需枚举次数X的数学期望是：</p>
<p>1×一次就中的概率 + 2×二次才中的概率 + 3×三次才中的概率 + …</p>
<p>也就是：</p>
<p>1/26×n×(25/26)^(n-1) [1 + 2×(25/26)^n + 3×(25/26)^2n + 4×(25/26)^3n … ]</p>
<p>左边方括号内的无穷级数求和的形式为1+2q+3q^2+4q^3+…，结果为1/(1-q)^2（利用类似等比级数求和的技巧——错位相减），所以上式求和简化后的结果为：</p>
<p>[n×25^(n-1)×26^n]/[(26^n-25^n)^2]</p>
<p>即，当目标单词长为n时，平均需要[n×25^(n-1)×26^n]/[(26^n-25^n)^2]次枚举才能命中其中一个字母；而一旦命中一个字母之后，该字母就会被从单词当中剔掉，并继续枚举第二个字母，此时n减少了1，因而上式的值也发生了变化。</p>
<p>简言之，长度为N的单词，需要∑[n×25^(n-1)×26^n]/[(26^n-25^n)^2]次（其中n从1变化到N）迭代便能够完全枚举出来。</p>
<p>现在我们回到原来的问题：一个长为13的单词，TOBEORNOTTOBE，究竟需要枚举多少次才能够完全枚举出来呢？按照以上的式子，得出的结果是82.39。而采用前面的不精确近似，计算的结果82.68。跟我自己写的一个枚举程序运行一万次平均之后的结果刚好相符，后者也是在82左右徘徊。</p>
<p>不过，以上两种做法其实都建立在一个假设之上，即我们是一个一个地枚举出目标单词中的字母的。不是两个，也不是三个。然而实际当中可能一次就枚举出多个乃至全部的字母。因此，其实以上两种做法计算出的都是一个不精确的值，这也是为什么它们的结果相近但不一样的原因（真正的结果只有一个）。然而，如果想给出精确表达式或计算方法就非常复杂了，或者说至少我没有想到更简单的表达方法，如果你有不妨告诉我:-)</p>
<p><b>小结</b><b></b></p>
<p>本文介绍了隐藏在自然选择中的概率论，并说明了为什么自然选择能够在相对（与纯随机枚举相比）极短的时间内塑造出复杂的有机体；简而言之，选择之手总是不断地将生物的基因向某个方向推进，一旦基因中变异出有益的片段，该片段就会被选择保留下来并逐渐在种群中蔓延开来，反映到文中讨论的枚举单词的例子中就是，一旦某个位上的字母被枚举出来，便会被保留住，不再受到后续变异的影响（除非是更好的变异）（通过一个数学示例，我们看到，原本需要26^13次迭代才能产生的目标序列，只需82次居然就进化出来了，其间的差距是无法估计的；事实上，计算机算法上就有使用进化思想来实现算法的，也就是所谓的<a href="http://www.douban.com/subject/1232071/">进化算法</a>）；再则，加以种群中数量巨大的个体（每个个体都是一个单独的枚举器），我们就不难理解为什么自然选择能够进化出复杂如眼睛的结构了。</p>
<p><b><i>Update 2007-12-3</i></b>：<a href="http://blog.youxu.info/">徐宥</a>同学<a href="http://blog.csdn.net/pongba/archive/2007/12/02/1912466.aspx#747121">指出</a>，<a href="http://surge.ods.org/listarc/20020621.HTM">在Richard原来写的那个程序中</a>，是采用的“挨个枚举”办法。即先枚举出第一个字母（期望需要26次），然后枚举第二个，如此直到把所有字母枚举完。如此需要的时间为13×26=338；符合文中给出的值。如此说来，之所以我得出的结果跟Richard的不一样，是因为<b>采用的枚举策略不一样</b>，我将“一次枚举”定义为“枚举整个单词”，而非“枚举其中某个字母”。不过我感觉Richard的那个枚举策略显然不符合自然选择的工作方式，自然状况下，每一个基因位（“字母”）都可能发生变异（独立分布，不过变异概率未必一样），而对变异基因的择优筛选则发生在“遗传”这一个环节（严格来说是发生在遗传过程中的“差异繁殖率”上）。</p>
<p>不过，总而言之，尽管计算机模拟的选择算法不同，总的思想是一样的，即一旦加入了“选择之手”，就能够极大地加快进化的速度。</p>
<p><b>下期预告？</b><b></b></p>
<p>本来打算写写Google Pagerank算法的，但那玩意往细了写太麻烦，总找不出动力，所以索性先写篇短的:-) Google Pagerank算法是数学与工程的完美结合，其中蕴含了数学的纯粹和工程的务实，实在值得欣赏，所以，强烈推荐下面这篇：</p>
<p><a href="http://www.ams.org/featurecolumn/archive/pagerank.html">How Google Finds Your Needle in the Web&#8217;s Haystack</a>，那么这篇有多火呢？del.icio.us上save的人竟有1,774人！可见一斑。</p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/" title="数学之美番外篇：快排为什么那样快">数学之美番外篇：快排为什么那样快</a> (23)</li><li><a href="http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/" title="知其所以然（三）：为什么算法这么难？">知其所以然（三）：为什么算法这么难？</a> (57)</li><li><a href="http://mindhacks.cn/2010/11/14/the-importance-of-knowing-why-part2/" title="知其所以然（续）">知其所以然（续）</a> (42)</li><li><a href="http://mindhacks.cn/2008/09/21/the-magical-bayesian-method/" title="数学之美番外篇：平凡而又神奇的贝叶斯方法">数学之美番外篇：平凡而又神奇的贝叶斯方法</a> (48)</li><li><a href="http://mindhacks.cn/2008/07/07/the-importance-of-knowing-why/" title="知其所以然（以算法学习为例）">知其所以然（以算法学习为例）</a> (36)</li><li><a href="http://mindhacks.cn/2008/04/18/learning-from-polya/" title="跟波利亚学解题(rev#3)">跟波利亚学解题(rev#3)</a> (21)</li><li><a href="http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/" title="康托尔、哥德尔、图灵&mdash;&mdash;永恒的金色对角线(rev#2)">康托尔、哥德尔、图灵&mdash;&mdash;永恒的金色对角线(rev#2)</a> (34)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2007. | <a href="http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/#commenting">13 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e6%95%b0%e5%ad%a6/" rel="tag">数学</a>, <a href="http://mindhacks.cn/tags/%e6%a6%82%e7%8e%87%e8%ae%ba/" rel="tag">概率论</a>, <a href="http://mindhacks.cn/tags/%e7%ae%97%e6%b3%95/" rel="tag">算法</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/">原始超链接</a>: http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/feed/</wfw:commentRss>
		<slash:comments>13</slash:comments>
		</item>
		<item>
		<title>康托尔、哥德尔、图灵&#8212;&#8212;永恒的金色对角线(rev#2)</title>
		<link>http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/</link>
		<comments>http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/#comments</comments>
		<pubDate>Sun, 15 Oct 2006 11:16:00 +0000</pubDate>
		<dc:creator>刘未鹏</dc:creator>
				<category><![CDATA[数学]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[计算机科学]]></category>

		<guid isPermaLink="false">http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/</guid>
		<description><![CDATA[哥德尔的不完备性定理震撼了20世纪数学界的天空，其数学意义颠覆了希尔伯特的形式化数学的宏伟计划，其哲学意义直到21世纪的今天仍然不断被延伸到各个自然学科，深刻影响着人们的思维。图灵为了解决希尔伯特著名的第十问题而提出有效计算模型，进而作出了可计算理论和现代计算机的奠基性工作，著名的停机问题给出了机械计算模型的能力极限，其深刻的意义和漂亮的证明使它成为可计算理论中的标志性定理之一。丘齐，跟图灵同时代的天才，则从另一个抽象角度提出了lambda算子的思想，与图灵机抽象的倾向于硬件性不同，丘齐的lambda算子理论是从数学的角度进行抽象，不关心运算的机械过程而只关心运算的抽象性质，只用最简洁的几条公理便建立起了与图灵机完全等价的计算模型，其体现出来的数学抽象美开出了函数式编程语言这朵奇葩，Lisp、Scheme、Haskell… 这些以抽象性和简洁美为特点的语言至今仍然活跃在计算机科学界，虽然由于其本质上源于lambda算子理论的抽象方式不符合人的思维习惯从而注定无法成为主流的编程语言[2]，然而这仍然无法妨碍它们成为编程理论乃至计算机学科的最佳教本。而诞生于函数式编程语言的神奇的Y combinator至今仍然让人们陷入深沉的震撼和反思当中…]]></description>
			<content:encoded><![CDATA[<p><i>我看到了它，却不敢相信它</i><sup>[1]</sup><i>。</i><i></i></p>
<p><i>——</i><i>康托尔</i><i></i></p>
<p><i>计算机是数学家一次失败思考的产物。</i><i></i></p>
<p><i>——</i><i>无名氏</i><i></i></p>
<p><a href="http://en.wikipedia.org/wiki/Kurt_Godel">哥德尔</a>的<a href="http://www.answers.com/topic/g-del-s-incompleteness-theorems">不完备性定理</a>震撼了20世纪数学界的天空，其数学意义颠覆了<a href="http://en.wikipedia.org/wiki/David_Hilbert">希尔伯特</a>的形式化数学的宏伟计划，其哲学意义直到21世纪的今天仍然不断被延伸到各个自然学科，深刻影响着人们的思维。<a href="http://www.alanturing.net/">图灵</a>为了解决希尔伯特著名的<a href="http://en.wikipedia.org/wiki/Hilbert's_tenth_problem">第十问题</a>而提出有效计算模型，进而作出了<a href="http://en.wikipedia.org/wiki/Computability_theory_(computation)">可计算理论</a>和现代计算机的奠基性工作，著名的停机问题给出了机械计算模型的能力极限，其深刻的意义和漂亮的证明使它成为可计算理论中的标志性定理之一。<a href="http://en.wikipedia.org/wiki/Alonzo_Church">丘齐</a>，跟图灵同时代的天才，则从另一个抽象角度提出了<a href="http://en.wikipedia.org/wiki/Lambda_calculus">lambda算子</a>的思想，与<a href="http://en.wikipedia.org/wiki/Turing_machine">图灵机</a>抽象的倾向于硬件性不同，丘齐的lambda算子理论是从数学的角度进行抽象，不关心运算的机械过程而只关心运算的抽象性质，只用最简洁的几条公理便建立起了与图灵机<a href="http://en.wikipedia.org/wiki/Turing_machine#Models_equivalent_to_the_Turing_machine_model">完全等价</a>的计算模型，其体现出来的数学抽象美开出了<a href="http://en.wikipedia.org/wiki/Functional_programming">函数式编程语言</a>这朵奇葩，<a href="http://en.wikipedia.org/wiki/Lisp_programming_language">Lisp</a>、<a href="http://en.wikipedia.org/wiki/Scheme_(programming_language)">Scheme</a>、<a href="http://www.haskell.org/">Haskell</a>… 这些以抽象性和简洁美为特点的语言至今仍然活跃在计算机科学界，虽然由于其本质上源于lambda算子理论的抽象方式不符合人的思维习惯从而注定无法成为主流的编程语言<sup>[2]</sup>，然而这仍然无法妨碍它们成为编程理论乃至计算机学科的最佳教本。而诞生于函数式编程语言的神奇的<a href="http://en.wikipedia.org/wiki/Y_combinator">Y combinator</a>至今仍然让人们陷入深沉的震撼和反思当中…</p>
<p>然而，这一切的一切，看似不很相关却又有点相关，认真思考其关系却又有点一头雾水的背后，其实隐隐藏着一条线，这条线把它们从本质上串到了一起，而顺着时光的河流逆流而上，我们将会看到，这条线的尽头，不是别人，正是只手拨开被不严密性问题困扰的19世纪数学界阴沉天空的天才数学家<a href="http://en.wikipedia.org/wiki/Georg_Cantor">康托尔</a>，康托尔创造性地将一一对应和对角线方法运用到无穷集合理论的建立当中，这个被希尔伯特称为“谁也无法将我们从康托尔为我们创造的乐园中驱逐出去”、被罗素称为“19世纪最伟大的智者之一”的人，他在<a href="http://www.amazon.com/Contributions-Founding-Theory-Transfinite-Numbers/dp/0875481574/ref=sr_1_4/103-3576835-0834231?ie=UTF8&amp;s=books&amp;qid=1177237889&amp;sr=8-4">集合论方面的工作</a>终于驱散了不严密性问题带来的阴霾，仿佛一道金色的阳光刺破乌云，19世纪的数学终于看到了真正严格化的曙光，数学终于得以站在了前所未有的坚固的基础之上；集合论至今仍是数学里最基础和最重要的理论之一。而康托尔当初在研究无穷集合时最具天才的方法之一——对角线方法——则带来了极其深远的影响，其纯粹而直指事物本质的思想如洪钟大吕般响彻数学和哲学的每一个角落<sup>[3]</sup>。随着本文的展开，你将会看到，刚才提到的一切，歌德尔的不完备性定理，图灵的停机问题，lambda算子理论中神奇的Y combinator、乃至著名的罗素悖论、理发师悖论等等，其实都源自这个简洁、纯粹而同时又是最优美的数学方法，反过来说，从康托尔的对角线方法出发，我们可以轻而易举地推导出哥德尔的不完备性定理，而由后者又可以轻易导出停机问题和Y combinator，实际上，我们将会看到，后两者也可以直接由康托尔的对角线方法导出。尤其是Y combinator，这个形式上绕来绕去，本质上捉摸不透，看上去神秘莫测的算子，其实只是一个非常自然而然的推论，如果从哥德尔的不完备性定理出发，它甚至比停机问题还要来得直接简单。总之，你将会看到这些看似深奥的理论是如何由一个至为简单而又至为深刻的数学方法得出的，你将会看到最纯粹的数学美。</p>
<p><b>图灵的停机问题</b><b>(The Halting Problem)</b></p>
<p><i>了解停机问题的可以直接跳过这一节，到下一节“Y Combinator”，了解后者的再跳到下一节“哥德尔的不完备性定理”</i></p>
<p>我们还是从图灵著名的停机问题说起，一来它相对来说是我们要说的几个定理当中最简单的，二来它也最贴近程序员。实际上，我以前曾写过<a href="http://blog.csdn.net/pongba/archive/2006/03/11/621723.aspx">一篇关于图灵机的文章</a>，有兴趣的读者可以从那篇开始，那篇主要是从理论上阐述，所以这里我们打算避开抽象的理论，换一种符合程序员思维习惯的直观方式来加以解释。</p>
<p><b>停机问题</b><b></b></p>
<p><i>不存在这样一个程序（算法），它能够计算任何程序（算法）在给定输入上是否会结束（停机）。</i><i></i></p>
<p>那么，如何来证明这个停机问题呢？反证。假设我们某一天真做出了这么一个极度聪明的万能算法（就叫God_algo吧），你只要给它一段程序（二进制描述），再给它这段程序的输入，它就能告诉你这段程序在这个输入上会不会结束（停机），我们来编写一下我们的这个算法吧：</p>
<p>bool God_algo(char* program, char* input)</p>
<p>{</p>
<p>if(<i>&lt;program</i>&gt; <i>halts on </i>&lt;<i>input</i>&gt;)</p>
<p>return true;</p>
<p>return false;</p>
<p>}</p>
<p>这里我们假设if的判断语句里面是你天才思考的结晶，它能够像上帝一样洞察一切程序的宿命。现在，我们从这个God_algo出发导出一个新的算法：</p>
<p>bool Satan_algo(char* program)</p>
<p>{</p>
<p>if( <i>God_algo</i>(program, program) ){</p>
<p>while(1); // loop forever!</p>
<p>return false; // can never get here!</p>
<p>}</p>
<p>else</p>
<p>return true;</p>
<p>}</p>
<p>正如它的名字所暗示的那样，这个算法便是一切邪恶的根源了。当我们把这个算法运用到它自身身上时，会发生什么呢？</p>
<p>Satan_algo(Satan_algo);</p>
<p>我们来分析一下这行简单的调用：</p>
<p>显然，Satan_algo(Satan_algo)这个调用要么能够运行结束返回（停机），要么不能返回（loop forever）。</p>
<p><i>如果它能够结束</i>，那么Santa_algo算法里面的那个if判断就会成立（因为God_algo(Santa_algo,Santa_algo)将会返回true），从而程序便进入那个包含一个无穷循环while(1);的if分支，于是这个Satan_algo(Satan_algo)调用<i>便永远不会返回（结束）了</i>。</p>
<p>而<i>如果</i><i>Satan_algo(Satan_algo)</i><i>不能结束（停机）呢</i>，则if判断就会失败，从而选择另一个if分支并返回true，即<i>Satan_algo(Satan_algo)</i><i>又</i><i>能够返回（停机）</i>。</p>
<p>总之，我们有：</p>
<p><i>Satan_algo(Satan_algo)</i><i>能够停机</i><i> </i><i>=&gt; </i><i>它不能停机</i><i></i></p>
<p><i>Satan_algo(Satan_algo)</i><i>不能停机</i><i> </i><i>=&gt; </i><i>它能够停机</i><i></i></p>
<p>所以它停也不是，不停也不是。左右矛盾。</p>
<p>于是，我们的假设，即God_algo算法的存在性，便不成立了。正如<a href="http://en.wikipedia.org/wiki/Joseph_Louis_Lagrange">拉格朗日</a>所说：“陛下，我们不需要（上帝）这个假设”<sup>[4]</sup>。</p>
<p>这个证明相信每个程序员都能够容易的看懂。然而，这个看似不可捉摸的技巧背后其实隐藏着深刻的数学原理（甚至是哲学原理）。在没有认识到这一数学原理之前，至少我当时是对于图灵如何想出这一绝妙证明感到无法理解。但后面，在介绍完了与图灵的停机问题“同构”的Y combinator之后，我们会深入哥德尔的不完备性定理，在理解了哥德尔不完备性定理之后，我们从这一同样绝妙的定理出发，就会突然发现，离停机问题和神奇的Y combinator只是咫尺之遥而已。当然，最后我们会回溯到一切的尽头，康托尔那里，看看停机问题、Y combinator、以及不完备性定理是如何自然而然地由康托尔的对角线方法推导出来的，我们将会看到这些看似神奇的构造性证明的背后，其实是一个简洁优美的数学方法在起作用。</p>
<p><b>Y Combinator</b></p>
<p><i>了解</i><i>Y combinator</i><i>的请直接跳过这一节，到下一节</i><i>“</i><i>哥德尔的不完备性定理</i><i>”</i><i>。</i><i></i></p>
<p>让我们暂且搁下但记住绕人的图灵停机问题，走进函数式编程语言的世界，走进由跟图灵机理论等价的lambda算子发展出来的另一个平行的语言世界。让我们来看一看被人们一代一代吟唱着的神奇的Y Combinator…</p>
<p>关于Y Combinator的文章可谓数不胜数，这个由师从希尔伯特的著名逻辑学家<a href="http://en.wikipedia.org/wiki/Haskell_Curry">Haskell B.Curry</a>（Haskell语言就是以他命名的，而函数式编程语言里面的Curry手法也是以他命名）“发明”出来的组合算子（Haskell是研究<a href="http://en.wikipedia.org/wiki/Combinatory_logic">组合逻辑(combinatory logic)</a>的）仿佛有种神奇的魔力，它能够算出给定lambda表达式（函数）的<a href="http://en.wikipedia.org/wiki/Fixed_point">不动点</a>。从而使得递归成为可能。事实上，我们待会就会看到，Y Combinator在神奇的表面之下，其实隐藏着深刻的意义，其背后体现的意义，曾经开出过历史上最灿烂的数学之花，所以MIT的计算机科学系将它做成系徽也就不足为奇了<sup>[5]</sup>。</p>
<p>当然，要了解这个神奇的算子，我们需要一点点lambda算子理论的基础知识，不过别担心，lambda算子理论是我目前见过的最简洁的公理系统，这个系统仅仅由三条非常简单的公理构成，而这三条公理里面我们又只需要关注前两条。</p>
<p><i>以下小节</i><i>——lambda calculus——</i><i>纯粹是为了没有接触过</i><i>lambda</i><i>算子理论的读者准备的，并不属于本文重点讨论的东西，然而要讨论</i><i>Y combinator</i><i>就必须先了解一下</i><i>lambda</i><i>（当然，以编程语言来了解也行，但是你会看到，丘齐最初提出的</i><i>lambda</i><i>算子理论才是最最简洁和漂亮的，学起来也最省事。）所以我单独准备了一个小节来介绍它。如果你已经知道，可以跳过这一小节。不知道的读者也可以跳过这一小节去</i><i>wikipedia</i><i>上面看，这里的介绍使用了</i><i>wikipedia</i><i>上的方式</i><i></i></p>
<p><b>lambda calculus</b></p>
<p>先来看一下lambda表达式的基本语法(BNF)：</p>
<p>&lt;expr&gt; ::= &lt;identifier&gt;</p>
<p>&lt;expr&gt; ::= <i>lambda</i> &lt;identifier-list&gt;. &lt;expr&gt;</p>
<p>&lt;expr&gt; ::= (&lt;expr&gt; &lt;expr&gt;)</p>
<p>前两条语法用于生成lambda表达式（lambda函数），如：</p>
<p><i>lambda</i> x y. x + y</p>
<p><i>haskell</i><i>里面为了简洁起见用</i><i>“\”</i><i>来代替希腊字母</i><i>lambda</i><i>，它们形状比较相似。故而上面的定义也可以写成：</i><i></i></p>
<p><i>\ x y. x + y</i></p>
<p>这是一个匿名的加法函数，它接受两个参数，返回两值相加的结果。当然，这里我们为了方便起见赋予了lambda函数直观的计算意义，而实际上lambda calculus里面一切都只不过是文本替换，有点像C语言的宏。并且这里的“+”我们假设已经是一个具有原子语义的运算符<sup>[6]</sup>，此外，为了方便我们使用了中缀表达（按照lambda calculus系统的语法实际上应该写成“(+ x y)”才对——参考第三条语法）。</p>
<p>那么，函数定义出来了，怎么使用呢？最后一条规则就是用来调用一个lambda函数的：</p>
<p>((<i>lambda</i> x y. x + y) 2 3)</p>
<p>以上这一行就是把刚才定义的加法函数运用到2和3上（这个调用语法形式跟<a href="http://en.wikipedia.org/wiki/Imperative_programming">命令式语言(imperative language)</a>惯用的调用形式有点区别，后者是“f(x, y)”，而这里是“(f x y)”，不过好在顺序没变:) ）。为了表达简洁一点，我们可以给(<i>lambda</i> x y. x + y)起一个名字，像这样：</p>
<p>let Add = (<i>lambda</i> x y. x + y)</p>
<p>这样我们便可以使用Add来表示该lambda函数了：</p>
<p>(Add 2 3)</p>
<p>不过还是为了方便起见，后面调用的时候一般用“Add(2, 3)”，即我们熟悉的形式。</p>
<p>有了语法规则之后，我们便可以看一看这个语言系统的两条简单至极的公理了：</p>
<p><i>Alpha</i><i>转换公理</i>：例如，“lambda x y. x + y”转换为“lambda a b. a + b”。换句话说，函数的参数起什么名字没有关系，可以随意替换，只要函数体里面对参数的使用的地方也同时注意相应替换掉就是了。</p>
<p><i>Beta</i><i>转换公理</i>：例如，“(lambda x y. x + y) 2 3”转换为“2 + 3”。这个就更简单了，也就是说，当把一个lambda函数用到参数身上时，只需用实际的参数来替换掉其函数体中的相应变量即可。</p>
<p>就这些。是不是感觉有点太简单了？但事实就是如此，lambda算子系统从根本上其实就这些东西，然而你却能够从这几个简单的规则中推演出神奇无比的Y combinator来。我们这就开始！</p>
<p><b>递归的迷思</b><b></b></p>
<p>敏锐的你可能会发现，就以上这两条公理，我们的lambda语言中无法表示递归函数，为什么呢？假设我们要计算经典的阶乘，递归描述肯定像这样：</p>
<p>f(n):</p>
<p>if n == 0 return 1</p>
<p>return n*f(n-1)</p>
<p>当然，上面这个程序是假定n为正整数。这个程序显示了一个特点，f在定义的过程中用到了它自身。那么如何在lambda算子系统中表达这一函数呢？理所当然的想法如下：</p>
<p><i>lambda</i> n. If_Else n==0 1 n*&lt;<i>self</i>&gt;(n-1)</p>
<p>当然，上面的程序假定了If_Else是一个已经定义好的三元操作符（你可以想象C的“?:”操作符，后面跟的三个参数分别是判断条件、成功后求值的表达式、失败后求值的表达式。那么很显然，这个定义里面有一个地方没法解决，那就是&lt;<i>self</i>&gt;那个地方我们应该填入什么呢？很显然，熟悉C这类命令式语言的人都知道应该填入这个函数本身的名字，然而lambda算子系统里面的lambda表达式（或称函数）是没有名字的。</p>
<p>怎么办？难道就没有办法实现递归了？或者说，丘齐做出的这个lambda算子系统里面根本没法实现递归从而在计算能力上面有重大的缺陷？显然不是。马上你就会看到Y combinator是如何把一个看上去非递归的lambda表达式像变魔术那样变成一个递归版本的。在成功之前我们再失败一次，注意下面的尝试：</p>
<p>let F = <i>lambda</i> n. IF_Else n==0 1 n*F(n-1)</p>
<p>看上去不错，是吗？可惜还是不行。因为let F只是起到一个<a href="http://en.wikipedia.org/wiki/Syntactic_sugar">语法糖</a>的作用，在它所代表的lambda表达式还没有完全定义出来之前你是不可以使用F这个名字的。更何况实际上丘齐当初的lambda算子系统里面也并没有这个语法元素，这只是刚才为了简化代码而引入的语法糖。当然，了解这个let语句还是有意义的，后面还会用到。</p>
<p><b>一次成功的尝试</b><b></b></p>
<p>在上面几次失败的尝试之后，我们是不是就一筹莫展了呢？别忘了软件工程里面的一条黄金定律：“任何问题都可以通过增加一个间接层来解决”。不妨把它沿用到我们面临的递归问题上：没错，我们的确没办法在一个lambda函数的定义里面直接（按名字）来调用其自身。但是，可不可以间接调用呢？</p>
<p>我们回顾一下刚才不成功的定义：</p>
<p><i>lambda</i> n. If_Else n==0 1 n*&lt;<i>self</i>&gt;(n-1)</p>
<p>现在&lt;self&gt;处不是缺少“这个函数自身”嘛，既然不能直接填入“这个函数自身”，我们可以增加一个参数，也就是说，把&lt;self&gt;参数化：</p>
<p><i>lambda</i> <b>self</b> n. If_Else n==0 1 n*<b>self</b>(n-1)</p>
<p>上面这个lambda算子总是合法定义了吧。现在，我们调用这个函数的时候，只要加传一个参数self，这个参数不是别人，正是这个函数自身。还是为了简单起见，我们用let语句来给上面这个函数起个别名：</p>
<p>let P = <i>lambda</i> self n. If_Else n==0 1 n*self(n-1)</p>
<p>我们这样调用，比如说我们要计算3的阶乘：</p>
<p><b>P</b>(<b>P</b>, 3)</p>
<p>也就是说，把P自己作为P的第一个参数（注意，调用的时候P已经定义完毕了，所以我们当然可以使用它的名字了）。这样一来，P里面的self处不就等于是P本身了吗？自身调用自身，递归！</p>
<p>可惜这只是个美好的设想，还差一点点。我们分析一下P(P, 3)这个调用。利用前面讲的Beta转换规则，这个函数调用展开其实就是（你可以完全把P当成一个宏来进行展开！）：</p>
<p>IF_Else n==0 1 n*<b>P</b>(n-1)</p>
<p>看出问题了吗？这里的<b>P</b>(n-1)虽然调用到了P，然而只给出了一个参数；而从P的定义来看，它是需要两个参数的（分别为<b>self</b>和n）！也就是说，为了让<b>P</b>(n-1)变成良好的调用，我们得加一个参数才行，所以我们得稍微修改一下P的定义：</p>
<p>let P = <i>lambda</i> <b>self</b> n. If_Else n==0 1 n*<b>self</b>(<b>self</b>, n-1)</p>
<p>请注意，我们在P的函数体内调用self的时候增加了一个参数。现在当我们调用P(P, 3)的时候，展开就变成了：</p>
<p>IF_Else 3==0 1 3*<b>P</b>(<b>P</b>, 3-1)</p>
<p>而<b>P</b>(<b>P</b>, 3-1)是对P合法的递归调用。这次我们真的成功了！</p>
<p><b>不动点原理</b><b></b></p>
<p>然而，看看我们的P的定义，是不是很丑陋？“n*<b>self</b>(<b>self</b>, n-1)”？什么玩意？为什么要多出一个多余的self？<a href="http://en.wikipedia.org/wiki/Don't_repeat_yourself">DRY</a>！怎么办呢？我们想起我们一开始定义的那个失败的P，虽然行不通，但最初的努力往往是大脑最先想到的最直观的做法，我们来回顾一下：</p>
<p>let P = <i>lambda</i> self n. If_Else n==0 1 n*<b>self</b>(n-1)</p>
<p>这个P的函数体就非常清晰，没有冗余成分，虽然参数列表里面多出一个self，但我们其实根本不用管它，看函数体就行了，self这个名字已经可以说明一切了对不对？但很可惜这个函数不能用。我们再来回想一下为什么不能用呢？因为当你调用P(P, n)的时候，里面的self(n-1)会展开为P(n-1)而P是需要两个参数的。唉，要是这里的self是一个“真正”的，只需要一个参数的递归阶乘函数，那该多好啊。为什么不呢？干脆我们假设出一个“真正”的递归阶乘函数：</p>
<p>power(n):</p>
<p>if(n==0) return 1;</p>
<p>return n*power(n-1);</p>
<p>但是，前面不是说过了，这个理想的版本无法在lambda算子系统中定义出来吗（由于lambda函数都是没名字的，无法自己内部调用自己）？不急，我们并不需要它被定义出来，我们只需要在头脑中“假设”它以“某种”方式被定义出来了，现在我们把这个真正完美的power传给P，这样：</p>
<p>P(<b>power</b>, 3) </p>
<p>注意它跟P(P, 3)的不同，P(P, 3)我们传递的是一个有缺陷的P为参数。而P(power, 3)我们则是传递的一个真正的递归函数power。我们试着展开P(power, 3):</p>
<p>IF_Else 3==0 1 3*<b>power</b>(3-1)</p>
<p>发生了什么？？power(3-1)将会计算出2的阶乘（别忘了，power是我们设想的完美递归函数），所以这个式子将会忠实地计算出3的阶乘！</p>
<p>回想一下我们是怎么完成这项任务的：我们设想了一个以某种方式构造出来的完美的能够内部自己调用自己的递归阶乘函数power，我们发现把这个power传给P的话，P(power, n)的展开式就是真正的递归计算n阶乘的代码了。</p>
<p>你可能要说：废话！都有了power了我们还要费那事把它传给P来个P(power, n)干嘛？直接power(n)不就得了？! 别急，之所以设想出这个power只是为了引入不动点的概念，而不动点的概念将会带领我们发现Y combinator。</p>
<p>什么是不动点？一点都不神秘。让我们考虑刚才的power与P之间的关系。一个是真正可递归的函数，一个呢，则是以一个额外的self参数来试图实现递归的伪递归函数，我们已经看到了把power交给P为参数发生了什么，对吧？不，似乎还没有，我们只是看到了，“把power加上一个n一起交给P为参数”能够实现真正的递归。现在我们想考虑power跟P之间的关系，直接把power交给P如何？</p>
<p>P(power)</p>
<p>这是什么？这叫函数的<a href="http://en.wikipedia.org/wiki/Partial_evaluation"><i>部分求值</i>(<i>partial evaluation</i>)</a>。换句话说，第一个参数是给出来了，但第二个参数还悬在那里，等待给出。那么，光给一个参数得到的是什么呢？是“还剩一个参数待给的一个新的函数”。其实也很简单，只要按照Beta转换规则做就是了，把P的函数体里面的self出现处皆替换为power就可以了。我们得到：</p>
<p>IF_Else n==0 1 n*power(n-1)</p>
<p>当然，这个式子里面还有一个变量没有绑定，那就是n，所以这个式子还不能求值，你需要给它一个n才能具体求值，对吧。这么说，这可不就是一个以n为参数的函数么？实际上就是的。在lambda算子系统里面，如果给一个lambda函数的参数不足，则得到的就是一个新的lambda函数，这个新的lambda函数所接受的参数也就是你尚未给出的那些参数。换句话来说，调用一个lambda函数可以分若干步来进行，每次只给出一部分参数，而只有等所有参数都给齐了，函数的求值结果才能出来，否则你得到的就是一个“中间函数”。</p>
<p>那么，这跟不动点定理有什么关系？关系大了，刚才不是说了，P(power)返回的是一个新的“中间函数”嘛？这个“中间函数”的函数体我们刚才已经看到了，就是简单地展开P(power)而已，回顾一遍：</p>
<p>IF_Else n==0 1 n*power(n-1)</p>
<p>我们已经知道，这是个函数，参数n待定。因此我们不妨给它加上一个“lambda n”的帽子，这样好看一点：</p>
<p><i>lambda</i> n. IF_Else n==0 1 n*power(n-1)</p>
<p>这是什么呢？这可不就是power本身的定义？（当然，如果我们能够定义power的话）。不信我们看看power如果能够定义出来像什么样子：</p>
<p>let power = <i>lambda</i> n. IF_Else n==0 1 n*power(n-1)</p>
<p>一模一样！也就是说，P(power)展开后跟power是一样的。即：</p>
<p><b>P(power) = power</b></p>
<p>以上就是所谓的<i>不动点</i>。即对于函数P来说power是这样一个“点”：当把P用到power身上的时候，得到的结果仍然还是power，也就是说，power这个“点”在P的作用下是“不动”的。</p>
<p>可惜的是，这一切居然都是建立在一个不存在的power的基础上的，又有什么用呢？可别过早提“不存在”这个词，你觉得一样东西不存在或许只是你没有找到使它存在的正确方法。我们已经看到power是跟P有着密切联系的。密切到什么程度呢？对于伪递归的P，存在一个power，满足P(power)=power。注意，这里所说的“伪递归”的P，是指这样的形式：</p>
<p>let P = <i>lambda</i> self n. If_Else n==0 1 n*<b>self</b>(n-1) // 注意，不是self(self,n-1)</p>
<p>一般化的描述就是，对任一伪递归F（回想一下伪递归的F如何得到——是我们为了解决lambda函数不能引用自身的问题，于是给理想的f加一个self参数从而得到的），必存在一个理想f（F就是从这个理想f演变而来的），满足F(f) = f。</p>
<p>那么，现在的问题就归结为如何针对F找到它的f了。根据F和f之间的密切联系（F就比f多出一个self参数而已），我们可以从F得出f吗？假设我们可以（又是假设），也就是说假设我们找到了一根魔棒，把它朝任意一个伪递归的F一挥，眼前一花，它就变成了真正的f了。这根魔棒如果存在的话，它具有什么性质？我们假设这个神奇的函数叫做Y，把Y用到任何伪递归的函数F上就能够得到真正的f，也就是说：</p>
<p>Y(F) = f</p>
<p>结合上面的F(f) = f，我们得到：</p>
<p>Y(F) = f = F(f) = F(Y(F))</p>
<p>也就是说，Y具有性质：</p>
<p><b>Y(F) = F(Y(F))</b></p>
<p>性质倒是找出来了，怎么构造出这个Y却又成了难题。一个办法就是使用抽象法，这是从工程学的思想的角度，也就是通过不断迭代、重构，最终找到问题的解。然而对于这里的Y combinator，接近问题解的过程却显得复杂而费力，甚至过程中的有些点上的思维跳跃有点如羚羊挂角无迹可寻。然而，在这整个Y combinator介绍完了之后我们将会介绍著名的哥德尔不完备性定理，然后我们就会发现，通过哥德尔不完备性定理证明中的一个核心构造式，只需一步自然的推导就能得出我们的Y combinator。而且，最美妙的是，还可以再往下归约，把一切都归约到康托尔当初提出的对角线方法，到那时我们就会发现原来同样如羚羊挂角般的哥德尔的证明其实是对角线方法的一个自然推论。数学竟是如此奇妙，我们由简单得无法再简单的lambda calculus系统的两条公理居然能够导出如此复杂如此令人目眩神迷的Y Combinator，而这些复杂性其实也只是荡漾在定理海洋中的涟漪，拨开复杂性的迷雾我们重又发现它们居然寓于极度的简洁之中。这就是数学之美。</p>
<p>让我们先来看一看Y combinator的费力而复杂的工程学构造法，我会尽量让这个过程显得自然而流畅<sup>[7]</sup>：</p>
<p>我们再次回顾一下那个伪递归的求阶乘函数：</p>
<p>let P = <i>lambda</i> self n. If_Else n==0 1 n*<b>self</b>(n-1)</p>
<p>我们的目标是找出P的不动点power，根据不动点的性质，只要把power传给P，即P(power)，便能够得到真正的递归函数了。</p>
<p>现在，关键的地方到了，由于：</p>
<p>power = P(power) // 不动点原理</p>
<p>这就意味着，power作为一个函数（lambda calculus里面一切都是函数），它是自己调用了自己的。那么，我们如何实现这样一个能够自己调用自己的power呢？回顾我们当初成功的一次尝试，要实现递归，我们是通过增加一个间接层来进行的：</p>
<p>let power_gen = <i>lambda</i> self. P(<b>self</b>(<b>self</b>))</p>
<p>还记得<b>self</b>(<b>self</b>)这个形式吗？我们在成功实现出求阶乘递归函数的时候不就是这么做的？那么对于现在这个power_gen，怎么递归调用？</p>
<p>power_gen(power_gen)</p>
<p>不明白的话可以回顾一下前面我们调用P(P, n)的地方。这里power_gen(power_gen)展开后得到的是什么呢？我们根据刚才power_gen的定义展开看一看，原来是：</p>
<p><b>P</b>(power_gen(power_gen))</p>
<p>看到了吗？也就是说：</p>
<p>power_gen(power_gen) =&gt; <b>P</b>(power_gen(power_gen))</p>
<p><b></b></p>
<p>现在，我们把power_gen(power_gen)当成整体看，不妨令为power，就看得更清楚了：</p>
<p>power =&gt; <b>P</b>(power)</p>
<p>这不正是我们要的答案么？</p>
<p>OK，我们<i>总结一下</i>：对于给定的P，只要构造出一个相应的power_gen如下：</p>
<p>let power_gen = <i>lambda</i> self. P(<b>self</b>(<b>self</b>))</p>
<p>我们就会发现，power_gen(power_gen)这个调用展开后正是P(power_gen(power_gen))。也就是说，我们的power_gen(power_gen)就是我们苦苦寻找的不动点了！</p>
<p><b>铸造</b><b>Y Combinator</b></p>
<p>现在我们终于可以铸造我们的Y Combinator了，Y Combinator只要生成一个形如power_gen的lambda函数然后把它应用到自身，就大功告成：</p>
<p>let <b>Y</b> = <i>lambda</i> F.</p>
<p>let <b><i>f_gen</i></b> = <i>lambda</i> self. F(<b>self</b>(<b>self</b>))</p>
<p>return <b>f_gen</b>(<b>f_gen</b>)</p>
<p>稍微解释一下，Y是一个lambda函数，它接受一个伪递归F，在内部生成一个f_gen（还记得我们刚才看到的power_gen吧），然后把f_gen应用到它自身（记得power_gen(power_gen)吧），得到的这个f_gen(f_gen)也就是F的不动点了（因为f_gen(f_gen) = F(f_gen(f_gen))），而根据不动点的性质，F的不动点也就是那个对应于F的真正的递归函数！</p>
<p>如果你还觉得不相信，我们稍微展开一下看看，还是拿阶乘函数说事，首先我们定义阶乘函数的伪递归版本：</p>
<p>let Pwr = <i>lambda</i> self n. If_Else n==0 1 n*self(n-1)</p>
<p>让我们把这个Pwr交给<b>Y</b>，看会发生什么（根据刚才Y的定义展开吧）：</p>
<p>Y(Pwr) =&gt;</p>
<p>let f_gen = <i>lambda</i> self. <b>Pwr</b>(self(self))</p>
<p>return f_gen(f_gen)</p>
<p>Y(Pwr)的求值结果就是里面返回的那个f_gen(f_gen)，我们再根据f_gen的定义展开f_gen(f_gen)，得到：</p>
<p>Pwr(f_gen(f_gen))</p>
<p>也就是说：</p>
<p>Y(Pwr) =&gt; f_gen(f_gen) =&gt; Pwr(f_gen(f_gen))</p>
<p>我们来看看得到的这个Pwr(f_gen(f_gen))到底是不是真有递归的魔力。我们展开它（注意，因为Pwr需要两个参数，而我们这里只给出了一个，所以Pwr(f_gen(f_gen))得到的是一个单参（即n）的函数）：</p>
<p>Pwr(<b>f_gen</b>(<b>f_gen</b>)) =&gt; If_Else n==0 1 n*<b>f_gen</b>(<b>f_gen</b>) (n-1)</p>
<p>而里面的那个<b>f_gen</b>(<b>f_gen</b>)，根据f_gen的定义，又会展开为Pwr(f_gen(f_gen))，所以：</p>
<p><b>Pwr(f_gen(f_gen))</b> =&gt; If_Else n==0 1 n* <b>Pwr(f_gen(f_gen))</b> (n-1)</p>
<p>看到加粗的部分了吗？因为<b>Pwr(f_gen(f_gen))</b>是一个接受n为参数的函数，所以不妨把它令成f（f的参数是n），这样上面的式子就是：</p>
<p><b>f</b> =&gt; If_Else n==0 1 n*<b>f</b>(n-1)</p>
<p>完美的阶乘函数！</p>
<p><b>哥德尔的不完备性定理</b><b></b></p>
<p><i>了解哥德尔不完备性定理的可以跳到下一节，</i><i>“</i><i>大道至简</i><i>——</i><i>康托尔的天才</i><i>”</i></p>
<p>然而，漫长的Y Combinator征途仍然并非本文的最终目的，对于Y combinator的构造和解释，只是给不了解lambda calculus或Y combinator的读者看的。关键是马上你会看到Y combinator可以由哥德尔不完备性定理证明的一个核心构造式一眼瞧出来！</p>
<p>让我们的思绪回到1931年，那个数学界风起云涌的年代，一个名不经传的20出头的学生，在他的博士论文中证明了一个惊天动地的结论。</p>
<p>在那个年代，希尔伯特的数学天才就像太阳的光芒一般夺目，在关于数学严格化的大纷争中希尔伯特带领的形式主义派系技压群雄，得到许多当时有名望的数学家的支持。希尔伯特希望借助于形式化的手段，抽掉数学证明中的意义，把数学证明抽象成一堆无意义的符号转换，就连我们人类赖以自豪的逻辑推导，也不过只是一堆堆符号转换而已（想起lambda calculus系统了吧：）)。这样一来，一个我们日常所谓的，带有直观意义和解释的数学系统就变成了一个纯粹由无意义符号表达的、公理加上推导规则所构成的形式系统，而数学证明呢，只不过是在这个系统内玩的一个文字游戏。令人惊讶的是，这样一种做法，真的是可行的！数学的意义，似乎竟然真的可以被抽掉！另一方面，一个形式系统具有非常好的性质，平时人们证明一个定理所动用的推导，变成了纯粹机械的符号变换。希尔伯特希望能够证明，在任一个无矛盾的形式系统中所能表达的所有陈述都要么能够证明要么能够证伪。这看起来是个非常直观的结论，因为一个结论要么是真要么是假，而它在它所处的领域/系统中当然应该能够证明或证伪了（只要我们能够揭示出该系统中足够多的真理）。</p>
<p>然而，哥德尔的证明无情的击碎了这一企图，哥德尔的证明揭示出，任何足够强到蕴含了皮亚诺算术系统（PA）的一致（即无矛盾）的系统都是不完备的，所谓不完备也就是说在系统内存在一个为真但无法在系统内推导出的命题。这在当时的数学界揭起了轩然大波，其证明不仅具有数学意义，而且蕴含了深刻的哲学意义。从那时起这一不完备性定理就被引申到自然科学乃至人文科学的各个角落…至今还没有任何一个数学定理居然能够产生这么广泛而深远的影响。</p>
<p>哥德尔的证明非常的长，达到了200多页纸，但其中很大的成分是用在了一些辅助性的工作上面，比如占据超过1/3纸张的是关于一个形式系统如何映射到自然数，也就是说，如何把一个形式系统中的所有公式都表示为自然数，并可以从一自然数反过来得出相应的公式。这其实就是编码，在我们现在看来是很显然的，因为一个程序就可以被编码成二进制数，反过来也可以解码。但是在当时这是一个全新的思想，也是最关键的辅助性工作之一，另一方面，这正是“程序即数据”的最初想法。</p>
<p>现在我们知道，要证明哥德尔的不完备性定理，只需在假定的形式系统T内表达出一个为真但无法在T内推导出（证明）的命题。于是哥德尔构造了这样一个命题，用自然语言表达就是：命题P说的是“<i>P</i><i>不可在系统</i><i>T</i><i>内证明</i>”（这里的系统T当然就是我们的命题P所处的形式系统了），也就是说“<i>我不可以被证明</i>”，跟著名的说谎者悖论非常相似，只是把“说谎”改成了“不可以被证明”。我们注意到，一旦这个命题能够在T内表达出来，我们就可以得出“P为真但无法在T内推导出来”的结论，从而证明T的不完备性。为什么呢？我们假设T可以证明出P，而因为P说的就是P不可在系统T内证明，于是我们又得到T无法证明出P，矛盾产生，说明我们的假设“T可以证明P”是错误的，根据排中律，我们得到T不可以证明P，而由于P说的正是“T不可证明P”，所以P就成了一个正确的命题，同时无法由T内证明！</p>
<p>如果你足够敏锐，你会发现上面这番推理本身不就是证明吗？其证明的结果不就是P是正确的？然而实际上这番证明是位于T系统之外的，它用到了一个关于T系统的假设“T是一致（无矛盾）的”，这个假设并非T系统里面的内容，所以我们刚才其实是在T系统<i>之外</i>推导出了P是正确的，这跟P不能在T<i>之</i><i>内</i>推导出来并不矛盾。所以别担心，一切都正常。</p>
<p>那么，剩下来最关键的问题就是如何用形式语言在T内表达出这个P，上面的理论虽然漂亮，但若是P根本没法在T内表达出来，我们又如何能证明“T内存在这个为真但无法被证明的P”呢？那一切还不是白搭？</p>
<p>于是，就有了哥德尔证明里面最核心的构造，哥德尔构造了这样一个公式：</p>
<p><b>N(n) is unprovable in T</b></p>
<p>这个公式由两部分构成，n是这个公式的自由变量，它是一个自然数，一旦给定，那么这个公式就变成一个明确的命题。而N则是从n解码出的货真价实的（即我们常见的符号形式的）公式（记得哥德尔的证明第一部分就是把公式编码吗？）。”is unprovable in T”则是一个谓词，这里我们没有用形式语言而是用自然语言表达出来的，但哥德尔证明了它是可以用形式语言表达出来的，大致思路就是：一个形式系统中的符号数目是有限的，它们构成这个形式系统的符号表。于是，我们可以依次枚举出所有长度为1的串，长度为2的串，长度为3的串… 此外根据形式系统给出的语法规则，我们可以检查每个串是否是良构的公式（well formed formula，简称wff，其实也就是说，是否符合语法规则，前面我们在介绍lambda calculus的时候看到了，一个形式系统是需要语法规则的，比如逻辑语言形式化之后我们就会看到P-&gt;Q是一个wff，而-&gt;PQ则不是），因而我们就可以枚举出所有的wff来。最关键的是，我们观察到形式系统中的证明也不过就是由一个个的wff构成的序列（想想推导的过程，不就是一个公式接一个公式嘛）。而wff构成的序列本身同样也是由符号表内的符号构成的串。所以我们只需枚举所有的串，对每一个串检查它是否是一个由wff构成的序列（证明），如果是，则记录下这个wff序列（证明）的最后一个wff，也就是它的结论。这样我们便枚举出了所有的可由T推导出的定理。然后为了表达出”X is unprovable in T”，本质上我们只需说“不存在这样一个自然数S，它所解码出来的wff序列以X为终结”！这也就是说，我们表达出了“is unprovable in T”这个谓词。</p>
<p>我们用UnPr(X)来表达“X is unprovable in T”，于是哥德尔的公式变成了：</p>
<p>UnPr( N(n) )</p>
<p>现在，到了最关键的部分，首先我们把这个公式简记为G(n)——别忘了G内有一个自由变量n，所以G现在还不是一个命题，而只是一个公式，所以谈不上真假：</p>
<p>G(n): UnPr( N(n) )</p>
<p>又由于G也是个wff，所以它也有自己的编码g，当然g是一个自然数，现在我们把g作为G的参数，也就是说，把G里面的自由变量n替换为g，我们于是得到一个真正的命题：</p>
<p>G(g): UnPr( G(g) )</p>
<p>用自然语言来说，这个命题G(g)说的就是“<i>我是不可在</i><i>T</i><i>内证明的</i>”。看，我们在形式系统T内表达出了“我是不可在T内证明的”这个命题。而我们一开始已经讲过了如何用这个命题来推断出G(g)为真但无法在T内证明，于是这就证明了哥德尔的不完备性定理<sup>[8]</sup>。</p>
<p>哥德尔的不完备性定理被称为20世纪数学最重大的发现（不知道有没有“之一”:) ）现在我们知道为真但无法在系统内证明的命题不仅仅是这个诡异的“哥德尔命题”，还有很多真正有意义的明确命题，其中最著名的就是<a href="http://en.wikipedia.org/wiki/Continuum_hypothesis">连续统假设</a>，此外哥德巴赫猜想也有可能是个没法在数论系统中证明的真命题。</p>
<p><b>从哥德尔公式到</b><b>Y Combinator</b></p>
<p>哥德尔的不完备性定理证明了数学是一个未完结的学科，永远有需要我们以人的头脑从系统之外去用我们独有的直觉发现的东西。罗杰·彭罗斯在<a href="http://www.amazon.com/Emperors-New-Mind-Roger-Penrose/dp/0140145346">《The Emperor&#8217;s New Mind》</a>中用它来证明人工智能的不可实现。当然，这个结论是很受质疑的。但哥德尔的不完备性定理的确还有很多很多的有趣推论，数学的和哲学上的。哥德尔的不完备性定理最深刻的地方就是它揭示了自指（或称自引用，递归调用自身等等）结构的普遍存在性，我们再来看一看哥德尔命题的绝妙构造：</p>
<p>G(n): UnPr( N(n) )</p>
<p>我们注意到，这里的UnPr其实是一个形式化的谓词，它不一定要说“X在T内可证明”，我们可以把它泛化为一个一般化的谓词，P：</p>
<p><b>G(n): P( N(n) )</b></p>
<p>也就是说，对于任意一个单参的谓词P，都存在上面这个哥德尔公式。然后我们算出这个哥德尔公式的自然数编码g，然后把它扔给G，就得到：</p>
<p>G(g): P( G(g) )</p>
<p>是不是很熟悉这个结构？我们的Y Combinator的构造不就是这样一个形式？我们把G和P都看成一元函数，G(g)可不正是P这个函数的不动点么！于是，<b>我们从哥德尔的证明里面直接看到了</b><b>Y Combinator</b>！</p>
<p>至于如何从哥德尔的证明联系到停机问题，就留给你去解决吧:) 因为更重要的还在后面，我们看到，哥德尔的证明虽然巧妙至极，然而其背后的思维过程仍然飘逸而不可捉摸，至少我当时看到G(n)的时候，“乃大惊”“不知所从出”，他怎么想到的？难道是某一个瞬间“灵光一现”？一般我是不信这一说的，已经有越来越多的科学研究表明一瞬间的“灵感”往往是潜意识乃至表层意识长期思考的结果。哥德尔天才的证明也不例外，我们马上就会看到，在这个神秘的构造背后，其实隐藏着某种更深的东西，这就是康托尔在19世纪80年代研究无穷集合和超限数时引入的对角线方法。这个方法仿佛有种神奇的力量，能够揭示出某种自指的结构来，而同时，这又是一个极度简单的手法，通过它我们能够得到数学里面一些非常奇妙的性质。无论是哥德尔的不完备性定理还是再后来丘齐建立的lambda calculus，抑或我们非常熟悉的图灵机理论里的停机问题，其实都只是这个手法简单推演的结果！</p>
<p><b>大道至简</b><b>——</b><b>康托尔的天才</b><b></b></p>
<p>“大道至简”这个名词或许更多出现在文学和哲学里面，一般用在一些模模糊糊玄玄乎乎的哲学观点上。然而，用在这里，数学上，这个名词才终于适得其所。大道至简，看上去最复杂的理论其实建立在一个最简单最纯粹的道理之上。</p>
<p>康托尔在无穷集合和超限数方面的工作主要集中在两篇突破性的论文上，这也是我所见过的最纯粹最美妙的数学论文，现代的数学理论充斥了太多复杂的符号和概念，很多时候让人看不到最本质的东西，当然，不否认这些东西很多也是有用的，然而，要领悟真正的数学美，像集合论和数论这种纯粹的东西，真的非常适合。不过这里就不过多谈论数学的细节了，只说康托尔引入对角线方法的动机和什么是对角线方法。</p>
<p><b>神奇的一一对应</b></p>
<p>康托尔在研究无穷集合的时候，富有洞察性地看到了对于无穷集合的大小问题，我们不能再使用直观的“所含元素的个数”来描述，于是他创造性地将一一对应引入进来，两个无穷集合“大小”一样当且仅当它们的元素之间能够构成一一对应。这是一个非常直观的概念，一一对应嘛，当然个数相等了，是不是呢？然而这同时就是它不直观的地方了。对于无穷集合，我们日常的所谓“个数”的概念不管用了，因为无穷集合里面的元素个数本就是无穷多个。不信我们来看一个小小的例子。我们说自然数集合能够跟偶数集合构成一一对应，从而<i>自然数集合跟偶数集合里面元素</i><i>“</i><i>个数</i><i>”</i><i>是一样多的</i>。怎么可能？偶数集合是自然数集合的真子集，所有偶数都是自然数，但自然数里面还包含奇数呢，说起来应该是二倍的关系不是？不是！我们只要这样来构造一一对应：</p>
<p>1 2 3 4 …</p>
<p>2 4 6 8 …</p>
<p>用函数来描述就是 f(n) = 2n。检验一下是不是一一对应的？不可思议对吗？还有更不可思议的，<i>自然数集是跟有理数集一一对应的</i>！对应函数的构造就留给你解决吧，提示，按如下方式来挨个数所有的有理数：</p>
<p>1/1 1/2 2/1 1/3 2/2 3/1 1/4 2/3 3/2 4/1 …</p>
<p>用这种一一对应的手法还可以得到很多惊人的结论，如<i>一条直线上所有的点跟一个平面上所有的点构成一一对应</i>（也就是说<i>复数集合跟实数集合构成一一对应</i>）。以致于连康托尔自己都不敢相信自己的眼睛了，这也就是为什么他在给戴得金的信中会说“我看到了它，却不敢相信它”的原因。</p>
<p>然而，除了一一对应之外，还有没有不能构成一一对应的两个无穷集合呢？有。<i>实数集合就比自然数集合要</i><i>“</i><i>大</i><i>”</i>，它们之间实际上无法构成一一对应。这就是康托尔的对角线方法要解决的问题。</p>
<p><b>实数集和自然数集无法构成一一对应？！</b><b></b></p>
<p>我们只需将实数的小数位展开，并且我们假设实数集能够与自然数集一一对应，也就是说假设实数集<a href="http://en.wikipedia.org/wiki/Countable">可列</a>，所以我们把它们与自然数一一对应列出，如下：</p>
<p>1 a<sub>10</sub>.a<sub>11</sub>a<sub>12</sub>a<sub>13</sub>…</p>
<p>2 a<sub>20</sub>.a<sub>21</sub>a<sub>22</sub>a<sub>23</sub>…</p>
<p>3 a<sub>30</sub>.a<sub>31</sub>a<sub>32</sub>a<sub>33</sub>…</p>
<p>4 …</p>
<p>5 …</p>
<p>（注：aij里面的ij是下标）</p>
<p>现在，我们构造一个新的实数，它的第i位小数不等于aii。也就是说，它跟上面列出的每一个实数都至少有一个对应的小数位不等，也就是说它不等于我们上面列出的所有实数，这跟我们上面假设已经列出了所有实数的说法相矛盾。所以实数集只能是不可列的，即不可与自然数集一一对应！这是对角线方法的最简单应用。</p>
<p><b>对角线方法</b><b>——</b><b>停机问题的深刻含义</b><b></b></p>
<p>对角线方法有很多非常奇妙的结论。其中之一就是文章一开始提到的停机问题。我想绝大多数人刚接触停机问题的时候都有一个问题，图灵怎么能够想到这么诡异的证明，怎么能构造出那个诡异的“说停机又不停机，说不停机又停机”的悖论机器。马上我们就会看到，这其实只是对角线方法的一个直接结论。</p>
<p>还是从反证开始，我们假设存在这样一个图灵机，他能够判断任何程序在任何输入上是否停机。由于所有图灵机构成的集合是一个可列集（也就是说，我们可以逐一列出所有的图灵机，严格证明见我以前的一篇文章《<a href="http://blog.csdn.net/pongba/archive/2006/03/11/621723.aspx">图灵机杂思</a>》），所以我们可以很自然地列出下表，它表示每个图灵机分别在每一个可能的输入（1,2,3,…）下的输出，N表示无法停机，其余数值则表示停机后的输出：</p>
<p>&#160;&#160;&#160;&#160;&#160;&#160; 1&#160; 2&#160; 3&#160; 4 …</p>
<p>M1&#160; N&#160; 1&#160; N&#160; N …</p>
<p>M2&#160; 2&#160; 0&#160; N&#160; 0 …</p>
<p>M3&#160; 0&#160; 1&#160; 2&#160; 0 …</p>
<p>M4&#160; N&#160; 0&#160; 5&#160; N …</p>
<p>…</p>
<p>M1，M2，M3 … 是逐一列出的图灵机，并且，注意，由于程序即数据，每个图灵机都有唯一编码，所以我们规定在枚举图灵机的时候Mi其实就代表编码为i的图灵机，当然这里很多图灵机将会是根本没用的玩意，但这不要紧。此外，最上面的一行1 2 3 4 … 是输入数据，如，矩阵的第一行代表M1分别在1，2，3，…上面的输出，不停机的话就是N。</p>
<p>我们刚才假设存在这样一个图灵机H，它能够判断任何程序在任何输入上能否停机，换句话说，H(i,j)（i是Mi的编码）能够给出“Mi(j)”是N（不停）呢还是给出一个具体的结果（停）。</p>
<p>我们现在来运用康托尔的对角线方法，我们构造一个新的图灵机P，P在1上的输出行为跟M1(1)“不一样”，在2上的输出行为跟M2(2)“不一样”，…总之P在输入i上的输出跟Mi(i)不一样。只需利用一下我们万能的H，这个图灵机P就不难构造出来，如下：</p>
<p>P(i):</p>
<p>if( <b>H</b>(i, i) == 1 ) then // Mi(i) halts</p>
<p>&#160; return 1 + Mi(i)</p>
<p>else // if H(i, i) == 0 (Mi(i) doesn’t halt)</p>
<p>&#160; return 0</p>
<p>也就是说，如果Mi(i)停机，那么P(i)的输出就是Mi(i)+1，如果Mi(i)不停机的话，P(i)就停机且输出0。这就保证了P(i)的输出行为跟Mi(i)反正不一样。现在，我们注意到P本身是一个图灵机，而我们上面已经列出了所有的图灵机，所以必然存在一个k，使得Mk = P。而两个图灵机相等当且仅当它们对于所有的输入都相等，也就是说对于任取的n，有Mk(n) = P(n)，现在令n=k，得到Mk(k)=P(k)，根据上面给出的P的定义，这实际上就是：</p>
<p>Mk(k) = P(k) = </p>
<p>&#160; 1+Mk(k) if Mk(k) halts</p>
<p>&#160; 0 if Mk(k) doesn’t halt</p>
<p>看到这个式子里蕴含的矛盾了吗？如果Mk(k)停机，那么Mk(k)=1+Mk(k)；如果Mk(k)不停机，则Mk(k)=0（给出结果0即意味着Mk(k)停机）；不管哪种情况都是矛盾。于是我们得出，不存在那样的H。</p>
<p>这个对角线方法实际上说明了，无论多聪明的H，总存在一个图灵机的停机行为是它无法判断的。这跟哥德尔定理“无论多‘完备’的形式化公理系统，都存在一个‘哥德尔命题’是无法在系统内推导出来的”从本质上其实是一模一样的。只不过我们一般把图灵的停机问题称为“可判定问题”，而把数学的称为“可证明问题”。</p>
<p>等等！如果我们把那个无法判定是否停机的图灵机作为算法的特例纳入到我们的H当中呢？我们把得到的新的判定算法记为H<sub>1</sub>。然而，可惜的是，在H<sub>1</sub>下，我们又可以相应地以同样的手法从H<sub>1</sub>构造出一个无法被它（H<sub>1</sub>）判定的图灵机来。你再加，我再构造，无论你加多少个特例进去，我都可以由同样的方式构造出来一个你无法够到的图灵机，以彼之矛，攻彼之盾。其实这也是哥德尔定理最深刻的结论之一，哥德尔定理其实就说明了无论你给出多少个公理，即无论你建立多么完备的公理体系，这个系统里面都有由你的那些公理出发所推导不到的地方，这些黑暗的角落，就是人类直觉之光才能照射到的地方！</p>
<p>本节我们从对角线方法证明了图灵的停机问题，我们看到，对角线方法能够揭示出某种自指结构，从而构造出一个“悖论图灵机”。实际上，对角线方法是一种有深远影响的方法，哥德尔的证明其实也是这个方法的一则应用。证明与上面的停机问题证明如出一辙，只不过把Mi换成了一个形式系统内的公式fi，具体的证明就留给聪明的你吧:)我们现在来简单的看一下这个奇妙方法的几个不那么明显的推论。</p>
<p><b>罗素悖论</b><b></b></p>
<p>学过逻辑的人大约肯定是知道著名的罗素悖论的，罗素悖论用数学的形式来描述就是：</p>
<p>R = {X:X不属于X};</p>
<p>这个悖论最初是从康托尔的无穷集合论里面引申出来的。当初康托尔在思考无穷集合的时候发现可以称“一切集合的集合”，这样一个集合由于它本身也是一个集合，所以它就属于它自身。也就是说，我们现在可以称世界上存在一类属于自己的集合，除此之外当然就是不属于自己的集合了。而我们把所有不属于自己的集合收集起来做成一个集合R，这就是上面这个著名的罗素悖论了。</p>
<p>我们来看R是否属于R，如果R属于R，根据R的定义，R就不应该属于R。而如果R不属于R，则再次根据R的定义，R就应该属于R。</p>
<p>这个悖论促使了集合论的公理化。后来策梅罗公理化的集合论里面就不允许X属于X（不过可惜的是，尽管如此还是没法证明这样的集合论不可能产生出新的悖论。而且永远没法证明——这就是哥德尔第二不完备性定理的结论——一个包含了PA的形式化公理系统永远无法在内部证明其自身的一致（无矛盾）性。从而希尔伯特想从元数学推出所有数学系统的一致性的企图也就失败了，因为元数学的一致性又得由元元数学来证明，后者的一致性又得由元元元数学来证明…）。</p>
<p>这里我们只关心罗素是如何想出这个绝妙的悖论的。还是对角线方法！我们罗列出所有的集合，S1,S2,S3 …</p>
<p>&#160;&#160;&#160;&#160;&#160; S1&#160; S2&#160; S3 …</p>
<p>S1&#160; 0&#160;&#160;&#160;&#160; 1&#160;&#160;&#160;&#160; 1 …</p>
<p>S2&#160; 1&#160;&#160;&#160;&#160; 1&#160;&#160;&#160;&#160; 0 …</p>
<p>S3&#160; 0&#160;&#160;&#160;&#160; 0&#160;&#160;&#160;&#160; 0 …</p>
<p>… …</p>
<p>右侧纵向列出所有集合，顶行横向列出所有集合。0/1矩阵的(i,j)处的元素表示Si是否包含Sj，记为Si(j)。现在我们只需构造一个新的0/1序列L，它的第i位与矩阵的(i,i)处的值恰恰相反：L(i) = 1-Si(i)。我们看到，这个新的序列其实对应了一个集合，不妨也记为L，L(i)表示L是否包含Si。根据L的定义，如果矩阵的(i,i)处值为0（也就是说，如果Si不包含Si），那么L这个集合就包含Si,否则就不包含。我们注意到这个新的集合L肯定等于某个Sk（因为我们已经列出了所有的集合），L = Sk。既然L与Sk是同一集合，那么它们肯定包含同样的元素，从而对于任意n，有L(n) = Sk(n)。于是通过令n=k，得到L(k) = Sk(k)，而根据L的定义，L(k) = 1- Sk(k)。这就有Sk(k) = 1-Sk(k)，矛盾。</p>
<p>通过抽象简化以上过程，我们看到，我们构造的L其实是“包含了所有不包含它自身的集合的集合”，用数学的描述正是罗素悖论！</p>
<p>敏锐的你可能会注意到所有集合的数目是不可数的从而根本不能S1,S2…的一一列举出来。没错，但通过假设它们可以列举出来，我们发现了一个与可列性无关的悖论。所以这里的对角线方法其实可以说是一种启发式方法。</p>
<p>同样的手法也可以用到证明P(A)（A的所有子集构成的集合，也叫幂集）无法跟A构成一一对应上面。证明就留给聪明的你了:)</p>
<p><b>希尔伯特第十问题结出的硕果</b><b></b></p>
<p>希尔伯特是在1900年巴黎数学家大会上提出著名的希尔伯特第十问题的，简言之就是<i>是否存在一个算法，能够计算任意</i><i><a href="http://en.wikipedia.org/wiki/Diophantine_equation">丢番图方程</a></i><i>是否有整根</i>。要解决这个问题，就得先严格定义“算法”这一概念。为此图灵和丘齐分别提出了图灵机和lambda calculus这两个概念，它们从不同的角度抽象出了“有效（机械）计算”的概念，著名的<a href="http://en.wikipedia.org/wiki/Church-Turing_thesis">图灵——丘齐命题</a>就是说<i>所有可以有效计算出来的问题都可以由图灵机计算出来</i>。实际上我们已经看到，丘齐的lambda calculus其实就是数学推理系统的一个形式化。而图灵机则是把这个数学概念物理化了。而也正因为图灵机的概念隐含了实际的物理实现，所以冯·诺依曼才据此提出了奠定现代计算机体系结构的<a href="http://en.wikipedia.org/wiki/Von_Neumann_architecture">冯·诺依曼体系结构</a>，其遵循的，正是图灵机的概念。而“程序即数据”的理念，这个发端于数学家哥德尔的不完备性定理的证明之中的理念，则早就在黑暗中预示了可编程机器的必然问世。</p>
<p><b>对角线方法</b><b>——</b><b>回顾</b><b></b></p>
<p>我们看到了对角线方法是如何简洁而深刻地揭示出自指或递归结构的。我们看到了著名的不完备性定理、停机问题、Y Combinator、罗素悖论等等等等如何通过这一简洁优美的方法推导出来。这一诞生于康托尔的天才的手法如同一条金色的丝线，把位于不同年代的伟大发现串联了起来，并且将一直延续下去…</p>
<p>P.S</p>
<p>1. lambda calculus里面的“停机问题”</p>
<p>实际上lambda calculus里面也是有“停机问题”的等价版本的。其描述就是：不存在一个算法能够判定任意两个lambda函数是否等价。所谓等价当然是对于所有的n,有f(n)=g(n)了。这个问题的证明更加能够体现对角线方法的运用。仍然留给你吧。</p>
<p>2. <a href="http://blog.csdn.net/g9yuayon">负喧琐话</a>(<a href="http://blog.csdn.net/g9yuayon">http://blog.csdn.net/g9yuayon</a>)是个非常不错的blog:)。g9的文字轻松幽默，而且有很多名人八卦可以养眼，真的灰常…灰常…不错哦。此外g9老兄还是个理论功底非常扎实的牛。所以，anyway，看了他的blog就知道啦！最初这篇文章的动机也正是看了上面的一篇<a href="http://blog.csdn.net/g9yuayon/archive/2006/09/24/1271319.aspx">关于Y Combinator的铸造过程的介绍</a>，于是想揭示一些更深的东西，于是便有了本文。</p>
<p>3. 文章起名《康托尔、哥德尔、图灵——永恒的金色对角线》其实是为了纪念看过的一本好书GEB，即《Godel、Escher、Bach-An Eternal Golden Braid》中文译名《哥德尔、埃舍尔、巴赫——集异璧之大成》——商务印书馆出版。对于一本定价50元居然能够在douban上卖到100元的二手旧书，我想无需多说。另，幸福的是，电子版可以找到:)</p>
<p>4. 其实很久前想写的是一篇《从哥德尔到图灵》，但那篇写到1/3不到就搁下了，一是由于事务，二是总觉得少点什么。呵呵，如今把康托尔扯进来，也算是完成当时扔掉的那一篇吧。</p>
<p>5. 这恐怕算是写得最曲折的一篇文章了。不仅自己被这些问题搞得有点晕头转向（还好总算走出来），更因为要把这些东西自然而然的串起来，也颇费周章。很多时候是利用吃饭睡觉前或走路的时间思考本质的问题以及如何表达等等，然后到纸上一气呵成。不过同时也锻炼了不拿纸笔思考数学的能力，呵呵。</p>
<p>6. 关于图灵的停机问题、Y Combinator、哥德尔的不完备性定理以及其它种种与康托尔的对角线之间的本质联系，几乎查不到完整系统的深入介绍，一些书甚至如《The Emperor’s New Mind》也只是介绍了与图灵停机问题之间的联系（已经非常的难得了），google和baidu的结果也是基本没有头绪。很多地方都是一带而过让人干着急。所以看到很多地方介绍这些定理和构造的时候都是弄得人晕头转向的，绝大部分人在面对如Y Combinator、不完备性定理、停机问题的时候都把注意力放在力图理解它是怎么运作的上面了，却使人看不到其本质上从何而来，于是人们便对这些东东大为惊叹。这使我感到很不痛快，如隔靴搔痒般。这也是写这篇文章的主要动机之一。</p>
<p><strong>Reference</strong></p>
<p>[1] 《数学——确定性的丧失》</p>
<p>[2] 也有观点认为函数式编程语言之所以没有广泛流行起来是因为一些实际的商业因素。</p>
<p>[3] Douglas R.Hofstadter的著作《Godel, Escher, Bach: An Eternal Golden Braid》（《哥德尔、艾舍尔、巴赫——集异璧之大成》）就是围绕这一思想写出的一本奇书。非常建议一读。</p>
<p>[4] 《数学——确定性的丧失》</p>
<p>[5] 虽然我觉得那个系徽做得太复杂，要表达这一简洁优美的思想其实还能有更好的方式。</p>
<p>[6] 关于如何在lambda calculus系统里实现“+”操作符以及自然数等等，可参见<a href="http://blog.csdn.net/g9yuayon/archive/2006/05/29/759778.aspx">这里</a>，<a href="http://blog.csdn.net/g9yuayon/archive/2006/06/12/790953.aspx">这里</a>，和<a href="http://blog.csdn.net/g9yuayon/archive/2006/08/14/1062514.aspx">这里</a>。</p>
<p>[7] g9的blog（负暄琐话）<a href="http://blog.csdn.net/g9yuayon/">http://blog.csdn.net/g9yuayon/</a> 上有一系列介绍lambda calculus的文章（当然，还有其它好文章:)），非常不错，强烈推荐。最近的两篇就是介绍Y combinator的。其中有一篇以javaScript语言描述了迭代式逐步抽象出Y Combinator的过程。</p>
<p>[8] 实际上这只是第一不完备性定理，它还有一个推论，被称为第二不完备性定理，说的是任一个系统T内无法证明这个系统本身的一致性。这个定理的证明核心思想如下：我们前面证明第一不完备性定理的时候用的推断其实就表明 Con/T -&gt; G(g) （自然语言描述就是，由系统T的无矛盾，可以推出G(g)成立），而这个“Con/T -&gt; G(g)”本身又是可以在T内表达且证明出来的（具体怎么表达就不再多说了）——只需要用排中律即可。于是我们立即得到，T里面无法推出Con/T，因为一旦推出Con/T就立即推出G(g)从而推出UnPr(G(g))，这就矛盾了。所以，Con/T无法在T内推出（证明）。</p>
<h3  class="related_post_title">你可能也会喜欢以下文章</h3><ul class="related_post"><li><a href="http://mindhacks.cn/2008/09/21/the-magical-bayesian-method/" title="数学之美番外篇：平凡而又神奇的贝叶斯方法">数学之美番外篇：平凡而又神奇的贝叶斯方法</a> (48)</li><li><a href="http://mindhacks.cn/2008/06/13/why-is-quicksort-so-quick/" title="数学之美番外篇：快排为什么那样快">数学之美番外篇：快排为什么那样快</a> (23)</li><li><a href="http://mindhacks.cn/2008/09/11/machine-learning-and-ai-resources/" title="机器学习与人工智能学习资源导引">机器学习与人工智能学习资源导引</a> (9)</li><li><a href="http://mindhacks.cn/2007/12/02/probability-theory-in-evolution/" title="数学之美番外篇：进化论中的概率论">数学之美番外篇：进化论中的概率论</a> (13)</li></ul><hr />
<h3>订阅 Mind Hacks</h3>
<a title="用Google Reader订阅" href="http://fusion.google.com/add?feedurl=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_google.gif" style="border:0" alt="订阅到 | Google" /></a> 
<a title="用鲜果订阅" href="http://www.xianguo.com/subscribe.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_xianguo.gif" style="border:0" alt="订阅到 | 鲜果" /></a>
<br/> 
<a title="用抓虾订阅" href="http://www.zhuaxia.com/add_channel.php?url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_zhuaxia.gif" style="border:0" alt="订阅到 | 抓虾" /></a> 
<a title="用有道订阅" href="http://reader.youdao.com/b.do?keyfrom=mindhacks&url=http://mindhacks.cn/feed/"><img src="http://mindhacks.cn/wp-content/uploads/2009/02/feed_yodao1.gif" style="border:0" alt="订阅到 | 有道" /></a> 
<hr />
<h3>我是你的信息过滤器</h3>
想了解作者最近在关注什么，欢迎 Follow <a href="http://weibo.com/pongba">刘未鹏pongba@微博</a>
<br/>
程序员朋友请到作者发起的 <a href="https://groups.google.com/group/pongba">TopLanguage</a> (<a href="http://mindhacks.cn/about-toplanguage/">about</a>) 社群逛逛，定有收获 :)
<br/>
想了解作者在阅读哪些书，请到 <a href="http://www.douban.com/people/pongba/">pongba@豆瓣</a>，或者直接访问以下四个豆列：<a href="http://www.douban.com/doulist/46003/">[只读经典]思维改变生活</a> | <a href="http://www.douban.com/doulist/127649/">[只读经典]思考的技术与艺术</a> | <a href="http://www.douban.com/doulist/197706/">决策与判断</a> | <a href="http://www.douban.com/doulist/176513/">机器学习与人工智能书籍资源导引</a> 。
<hr />
<p><small>
本文由 刘未鹏 发布在 <a href="http://mindhacks.cn">刘未鹏 | Mind Hacks</a>, 2006. | <a href="http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/#commenting">34 条评论</a> | 标签: <a href="http://mindhacks.cn/tags/%e6%95%b0%e5%ad%a6/" rel="tag">数学</a>, <a href="http://mindhacks.cn/tags/%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%a7%91%e5%ad%a6/" rel="tag">计算机科学</a>
<br/>
转载请注明作者，出处，以及<a href="http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/">原始超链接</a>: http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/
</small></p>]]></content:encoded>
			<wfw:commentRss>http://mindhacks.cn/2006/10/15/cantor-godel-turing-an-eternal-golden-diagonal/feed/</wfw:commentRss>
		<slash:comments>34</slash:comments>
		</item>
	</channel>
</rss>

