不知在你眼中,动态规划 在众多算法中处于什么地位呢?是ACM比赛中不可或缺的技巧之一,又或者是征战POJ 水题必备的利器?倘若果真如此,那我多少有些羡慕你,因为我至始至终都没有领悟动态规划的精髓。
自我阅读《算法导论》已久,我对书上给出的解决特定问题所用到的算法并无太多不明朗之处,而且在遇到相似的问题时,可以很快意识到自己熟知的某个算法 能够高效的解决这个问题。但是我不太明朗的地方在于:动态规划并非是解决某个特定问题的算法,他是解决一类问题的算法思想(寻求问题的最优解)。或许我们 能够很轻松的理解某个基于动态规划而形成的算法(比如说Bellman-Ford算法),但是理解算法背后动态规划的思想则需要颇费一番功夫。
我们还是从最简单的0-1背包问题说起,因为很多人都是从这个问题开始接触动态规划。在背包问题上,给出最详细,最清晰解答的当之无愧是背包问题九讲 。但是作者自己也明言因为存在思维上的跳跃,他的语言向来不以易于理解为长,所以若不是已经对动态规划有了完善认识的高手和准高手们,思考起来还是颇为费力的。
0-1背包问题:有N件物品和一个容量为V的背包。对于i≤N,第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
在未系统学习算法之前呢,估计面对这种问题时我会毫不犹豫的直接使用强力法,将N种物品的所有组合枚举出来,然后依次比较在不超过容量V的情况下各组合的总价值。没有悬念的,这是非常天真以及笨拙的做法,其时间复杂度为O(2^N)。
那么如何使用动态规划呢?对于i≤N,考虑将前i件物品放入容量为V的背包中这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一 个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为前i-1件物品放入容量为V的背包中的最大价值;如果放第i件物品,那么问题就转化 为第i件物品的价值加上前i-1件物品放入剩下的容量为V-c[i]的背包中的最大价值。
上面这段话是来自于背包问题九讲的讲解(我略去了转移方程),其正确性当然是毋庸置疑的。但是有一点,并且是很重要的一点是,我们为什么会这样思考 呢?换句话讲,我脑海中的疑问并不是会不会解决这个问题,而是解决这个问题的思想究竟是如何创造出来的。一想到有可能自己纯粹只是理解算法的内容,而不能 理解算法本身来历的前中后末,以及产生的灵感,我就有些坐卧难安。就说现在,如果要我解释0-1背包问题,我也能说的头头是道,但是一旦别人问起我是如何 想到这个方法的,我便脑中一愣,呆若木鸡。
知其然知其所以然 ,这是在算法学习中久经考验的黄金法则。如果不能做到知其所以然,知其然也是白搭。对于算法而言,不仅在于你会不会运用这个算法,更重要的在于是否理解这个算法的思想。在这种条件下,也许在某些时日之后,你也能创造出属于自己的算法。
事实上,对于同一个问题,如果我们多琢磨琢磨,总能有意外的收获。思考所带来的裨益性无可争议,我相信思考得越多,对问题的解法就理解得越为深刻。比如让 我们使背包问题更为具体些,假设这些物品都是珠宝,分别为钻石,玛瑙,翡翠,水晶等等。我们希望在总价值最大的情况下,珠宝类别的种类同时也最多(物品中 存在重复的类别)。这样在求解过程中我们不得不更为详细的记录当前背包中的物品类别,比较的过程也变得复杂了。但是有一点不变的是,我们仍然可以使用动态 规划来求解。不仅是背包问题,在最短路径问题之中,动态规划也得到了广泛的使用。就像前文所说的那样,动态规划是为了解决最优化这一类问题的算法思想。
熟读《算法导论》的读者都知道,最优子结构和重叠子问题是动态规划能否使用的两点主要特征。0-1背包问题就具备这样的性质,我们总是构造子问题的最 优解,并且最终的结果中也包含子问题的最优解。我们把每次对子问题求解的结果用一个数组存储起来,就避免了重复求解的过程。但并不是所有最优化的问题都能 用动态规划求解,比如书中列举出的无权最长路径便不存在最优子结构,也就是说子问题中的最优解相互干扰,合并为原问题之后的解不能满足原问题所要求的限 制。所以这类问题通常被我们划分为NP完全问题,即迄今为止还未能发现更高效的算法解决此类问题,其时间复杂度不能降为多项式时间。
动态规划之所以难以理解,一方面在于不容易确定问题的最优子结构,只要我们能迅速确立其转移方程,问题多半也就解决了。另一方面在于其自底向上的思想 与我们习惯的思维方式是违背的,我们往往会从全局的层次上考虑问题,自顶向下来思考子问题的解,这样我们很容易陷入递归分解问题的困境,以至于对动态规划 有些捉摸不透了。
我一向以为,贯穿这无数美妙算法之中的算法思想归根到底来自于两点:一是分治 , 二是递归(其实严格来讲,分治的算法思想也能解决递归问题)。分治的思想是把原本庞大的问题分解为几个较小的问题,分而治之后再合并,最为典型的例子自然 是归并排序了。递归的思想是把原来的问题转换为一个递推式,如果我们想求原问题,那么我们需要先解决子问题,在求解子问题的时候我们发现先需要解决子问题 的子问题,然后依此类推。递归算法中的翘楚自然就属于动态规划了。
那么分治思想与递归思想的同异之处又是什么呢?相同之处自然都是将原本的问题转化为更小的问题。相异之处在于分治算法遵从平均主义,总是将问题分解为 几个规模等同的子问题,若写成函数属于f(n) = af(n/b) + g(n)的形式;而递归思想则只是将原问题转化为单独较为简单的子问题,转化的速度远远不及分治,若写成函数则属于f(n) = f(n-c) + f(c) + g(n)的形式(两个函数中的a, b ,c均为常数,函数g(n)为合并子问题的开销)。
在我看来,算法学习的曲线是相当陡峭和漫长的。这不仅仅体现在要理解每个具体的算法结构,更要理解分治与递归这些处于更底层的算法思想。倘若能明确其算法思想,理解动态规划也不在话下,而其他问题自然也迎刃而解了。