走出迷宫的人们,有的是认识路;有的是莽撞碰巧出来的;有的则是一路做着标记出来的;也有的是走遍了整个迷宫。
——证明了的贪心算法、没有证明的贪心算法、动态规划、暴力搜索的区别。
今天来谈谈经典的算法设计思路问题,涉及搜索(Searching),动态规划(DP, Dynamic Programming),贪心算法(GA, Greedy Algorithm)……至于什么回溯法(Backtracking)只能是搜索的方向问题。
经常听人说起:“搜索、动态规划、贪心算法这几种算法云云”,好像这些算法彼此之间并没有交集一样。
非也,它们的渊源大了!
从范畴上来看:
Greedy⊂DP⊂Searching
即,所有的贪心算法问题都能用DP求解,更可以归结为一个搜索问题,反之不成立。
从动规到贪心
不管是动态规划还是贪心本质上都是一个搜索,这一点少有人发出疑问的。
然而却时常有人争论某问题是贪心的还是动归的,一个问题是贪心的还是动归的有联系但并不对立。
看一个具体的例子:
食堂里有n个人要在一个队列里排队买饭,每个人花费的时间分别为 Ci ,求如何安排队列的顺序可以使得所有人的总用时最小,最小为多少?
比如有三个人,分别用时3,2,5,那么最佳的排序应该是2,3,5,总用时是2 + 5 + 10 = 17,对此,你可以简单地验证一下其他5种的组合来验证这个答案是正确的。
这个问题在暴力搜索算法中需要消耗O(n!)的时间来生成所有的排列并从中获得最小值。
也许我们需要优化一下……正常人略作思考便可以猜出让耗时短的人优先的贪心优化策略。
于是就有了这样一段代码(JavaScript),可以直接在现代浏览器的控制台内使用(推荐Chrome)。
var solve = C => {
C.sort();
var sum = 0;
C.forEach((e, i) => sum += e * (C.length - i));
return {
min: sum,
order: C
};
}
solve([3, 2, 5]); // Object {min: 17, order: Array[3]}
这个问题的解空间是C数组的所有排列的集合,解空间的大小显然是O(n!)的,实际上其占用的存储空间是 O(n⋅n!) 的,因为每个解都占用了O(n)的空间。然而其上的贪心算法并没有遍历解空间,因此这不是一个暴力搜索。
由于任意顺序的C的所有排列的集合是一样的,因此他们的解空间与解都是一样的,所以我们直接把问题等价地化为经 O(nlogn) 的代价升序排列后的问题,即假设C已经有序。
设前 i 个人(已升序排列)能够达成的最小总用时为
我们考虑把第i个人插入到前i-1个人的队列中去。
等等,我们是否能证明前i -1个人的最优排列也是前i个人最优排列的子排列(最优子结构的性质是否具备)?可以。假设前i人的最优排列不包含前i-1人的最优排列,则将除第i人外的人调整成前i-1人的序列可以更优,与假设矛盾,因此前i-1人的最优排列一定是前i人最优排列的子排列。
尝试将第i人插在第j人的前面,并考虑带来的影响,便有状态转移方程: