动态规划与贪心算法的区别与联系

走出迷宫的人们,有的是认识路;有的是莽撞碰巧出来的;有的则是一路做着标记出来的;也有的是走遍了整个迷宫。
——证明了的贪心算法、没有证明的贪心算法、动态规划、暴力搜索的区别。

今天来谈谈经典的算法设计思路问题,涉及搜索(Searching),动态规划(DP, Dynamic Programming),贪心算法(GA, Greedy Algorithm)……至于什么回溯法(Backtracking)只能是搜索的方向问题。

经常听人说起:“搜索、动态规划、贪心算法这几种算法云云”,好像这些算法彼此之间并没有交集一样。

非也,它们的渊源大了!

从范畴上来看:

GreedyDPSearching

即,所有的贪心算法问题都能用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(nn!) 的,因为每个解都占用了O(n)的空间。然而其上的贪心算法并没有遍历解空间,因此这不是一个暴力搜索

由于任意顺序的C的所有排列的集合是一样的,因此他们的解空间与解都是一样的,所以我们直接把问题等价地化为经 O(nlogn) 的代价升序排列后的问题,即假设C已经有序。

设前 i 个人(已升序排列)能够达成的最小总用时为 f(i)

我们考虑把第i个人插入到前i-1个人的队列中去。

等等,我们是否能证明前i -1个人的最优排列也是前i个人最优排列的子排列(最优子结构的性质是否具备)?可以。假设前i人的最优排列不包含前i-1人的最优排列,则将除第i人外的人调整成前i-1人的序列可以更优,与假设矛盾,因此前i-1人的最优排列一定是前i人最优排列的子排列。

尝试将第i人插在第j人的前面,并考虑带来的影响,便有状态转移方程:

f(i)=f(i1)+minj[0,i]{(ij)Ci+k[0,j)Ck}

对于

ϕ(j)=(ij)Ci+k[0,j)Ck,j[0,i]


ϕ(j)ϕ(j1)=Ci+Cj0

即证出 ϕ(j) j 递减,即 minϕ(j)=ϕ(i)

代入可得

f(i)=f(i1)+ϕ(i)=f(i1)+k[0,i)Ck

由此证明了最佳策略为将最大的第i人放在最后

后面这个看似O(n)的求和操作也可以事先缓存,按照DP写法这个问题就变成了这样

(除了必要的排序,时空复杂度均已达到最优O(n)):

var solve = C => {
    C.sort();
    var dp = C.map(() => 0);
    var sum = C.map(e => e);
    sum.forEach((e, i) => {
        sum[i] += i > 0? sum[i - 1]: 0;
    });
    dp.forEach((e,i) => {
        dp[i] = sum[i] + (i > 0? dp[i - 1]: 0);
    });
    return {
        min: dp[dp.length - 1],
        order: C,
    }
}

对上面的数学公式再进一步则是:

f(n)=f(0)+i=1nj=0i1Cj

f(0)=C0

再按照计数原理得到 Ci 的权重为 ni

因而最后可以直接化为两个向量的数量积:
f(n)=(C0,C1,...,Cn1)(n,n1,...,1)

这似乎已经变为了人们口中的贪心算法,但究竟是从推导的什么地方开始变为具有贪心性质了呢?

回顾一下,当我们证明了 ϕ(i) 的单调性,对于第i个人的选择,一下子就从i个可选策略优化到了1个,这个时候所谓“贪心”的策略就真的变成了唯一正确的策略,所谓“只顾眼前利益的贪婪”其实是“看穿一切,一往无前的气魄”。贪心是优化了选择的策略,而非与动态规划背道而驰,贪心算法仅仅是动态规划的一个平凡态罢了。

与其将它称为贪婪,我觉得它更是智慧的象征。

多维解空间与不完全贪心

很多时候,问题的解空间并不适合用一维来描述,当解空间在一维以上,比如……还记得经典的0-1背包问题么?那就是一个典型的二维解空间问题。

对于物品 i ,具有 Vi 的价值与 Ci 的负重(代价)。

通常我们设前 i 个物品,能塞到容量为 j 的背包里的最大价值为 f(i,j)

有状态转移方程:

f(i,j)=max{f(i1,j),f(i1,jCi)+Vi}

这个时候我们看到解空间的第一维的策略是可贪心的:再计算 f(i,j) 的时候只用管 f(i1,) 而不用去管 f(i2,),f(i3,),... 由此便诞生了利用滚动数组压缩一维来降低空间复杂度的优化。

这种优化本质上是贪心的,是不完全的贪心,因为它只在某些维度上进行了贪心。

贪心算法与动态规划的效率区别

从动态规划优化到贪心算法真的提高了算法效率吗?未必。
这取决于动态规划中计算所有可选的策略的代价,如果代价是常数的,那么将其贪心优化并不会带来时间复杂度的下降(本文的两个例子都是如此),该走的解空间还是要走,只是不保存其中的一些值而已,可能会带来空间复杂度的下降。

有趣的是,当计算策略的代价并非常数(如Floyd全局最短路算法)时,往往并不只有一个策略,因而不能贪心优化。

因此,贪心算法不会降低从其对应的动态规划解法的时间复杂度,如果你发现它降低了,那么一定存在更好的解空间建模,更好的动态规划算法。

贪心算法确实可能比其对应的动态规划快不少,因为它的常数可能小得多。
但是,当你试图对同一解空间的不同点进行多次查询时,你会发现贪心可能会得不偿失,在均摊时间上输给不贪心的动态规划。

贪心优化是否失去了什么

贪心在于其抛弃了部分子结构的解。
如顺带要求出方案而不仅仅是最大价值的0-1背包问题,本来使用不优化空间的解法完全能够保留倒推回去的线索,如PAT 1068 Find More Coins 解题报告 所说一般。如果抛弃了动态规划带来的一些解,很有可能在其衍生的问题上得不偿失。

小结

既然已经得出贪心是动规的优化,那么循序渐进地解问题的思路应该先从搜索开始:先对问题的解空间建模,如果得到最优子结构的性质,写出状态转移方程,再看某些维度是否有贪心的优化余地,并且考虑一下,这些优化到底是否值得。

你可能感兴趣的:(算法)