在上一篇博客中,咱们经过选择问题了解了贪心算法。这一篇博客将继续介绍贪心算法,主要谈谈贪心算法的原理,并简单分析一下背包问题。html
经过上一篇博客中的选择问题,咱们看到,贪心算法能够由以下几个步骤来实现:python
对比动态规划,咱们发现贪心算法和它十分类似,首先它们都必须具有最优子结构性质,而后一般都是将原问题分解为子问题,根据最优子结构性质与问题的分解,设计一个递归算法。不一样之处在于,动态规划算法在对问题进行分解时,因为没法肯定哪种分解可以获得原问题的最优解,所以须要考察全部的分解状况,而且正是因为这种分解问题的不肯定性,一般会致使子问题重叠,为了提升效率,一般会用一个“备忘录”去备忘子问题的解;而在贪心算法中,咱们很明确的知道如何分解问题能产生最优解(或者说是很明确的知道哪一种选择的结果在最优解中),所以一般贪心算法没有子问题重叠重叠问题,但咱们必需要确保贪心选择的正确性。算法
和动态规划同样,咱们也总结出贪心算法的两个特色(必要条件)。安全
这个性质和动态规划同样,所以再也不赘述:测试
若是一个问题的最优解包含其子问题的最优解,咱们就称此问题具备最优子结构性质。spa
对于某个问题,若是咱们能够经过作出局部最优(贪心)选择来构造全局最优解,那么咱们就称该问题具备贪心选择性质。设计
下面经过两种不一样形式的背包问题,来进一步说明动态规划算法与贪心算法的区别之处,进而加深对贪心算法的理解。code
首先说明一下背包问题:htm
给定一组物品和一个限定重量的背包,每种物品都有本身的重量和价格。在限定的总重量内,咱们如何选择,才能使得背包内的物品总价格最高。blog
在0-1背包问题中,对于每一件物品,你只能作出二元(0-1)选择,即只能选择 选择该物品或不选择该物品,而不能只选择该物品的一部分;分数背包问题相反。
咱们先作以下说明:
设共有\(n\)件物品,记为\(t_1,t_2,...,t_n\),其中物品\(t_i\)重量为\(w_i\),价值为\(p_i\);限重为\(W\)。
首先,咱们可证实,0-1背包问题具备最优子结构性质:
由于,对于某种最优选择方案,假设\(t_i\)属于其中,若是咱们拿走\(t_i\),那么原问题则变为子问题:从商品\(t_1, ...,t_{i-1},t_{i+1}, ...,t_n\)中选择某些物品,限重为\(W-p_i\)。在子问题的全部选择方案中,要想让其中的某种方案与\(t_i\)组合成原问题的一个最优方案,该方案必须是子问题的一个最优方案。用剪切-粘贴法能够很容易证实,再也不赘述。
根据上面的分析,咱们先这么考虑,设\(P_{T, w}\)为物品集合为\(T\),限重\(w\)时,选择的物品的最高总价值,咱们能够很容易用一个递归式去表示最优解:
\[ P_{T, w} = \begin {cases} 0 & \text{若$T = \{t_i\}, w_i > w$ }\\ p_i&\text{若$T = \{t_i\}, w_i \leq w$}\\ \max\limits_{t_i \in T, w_i \leqslant w}(p_i + P_{T-t_i, w-w_i})&\text{其余} \end{cases} \]
下面给出此递归式的Python实现:
def knapsack_0_1(T, w): if len(T) == 1: if T[0][2] <= w: return T[0][1] return 0 maxValue = 0 for i, t in enumerate(T): if t[2] <= w: value = t[1] + knapsack_0_1(T[:i] + T[i + 1:], w - T[i][2]) if maxValue < value: maxValue = value return maxValue
咱们做以下测试:
if __name__ == '__main__': ''' 1号商品:$60 - 10kg 2号商品:$100 - 20kg 3号商品:$120 - 30kg 限重 50kg ''' T = [(1, 60 , 10), (2, 100, 20), (3, 120, 30)] W = 50 print(knapsack_0_1(T, W))
打印结果:220
从新审视上述递归式和以上的实现代码,咱们发现其压根就不是动态规划算法,而只是简单的递归算法。由于它不知足动态规划问题的一个必要条件:子问题重叠。实际上它也没有充分利用最优子结构性质,而致使“重复”求解了许多问题。其时间复杂度为\(O(n!)\)。
要用上动态规划算法,咱们能够这么去考虑,设\(P[i, w]\)表示物品\(t_1, ...,t_i\)在限重为\(w\)时,可以选择的物品的最高价值。咱们能够用以下递归式去表示\(P[i, w]\):
\[ P[i,w] = \begin {cases} 0 & \text{$w=0$或$i=0$}\\ P[i-1, w] & \text{$w_i > w, i = 1, 2,..., n $}\\ \max\limits_{w_i \leq w} \{P[i-1, w], p_i + P[i-1, w-w_i]\} & \text{$i = 1, 2,..., n$} \end{cases} \]
简单解释一下上述递归式:当\(w = 0\)或\(i = 0\)时,显然\(P[i, w] = 0\);当\(w_i > w\)时,由于没法将物品\(t_i\)放入背包(即不能选择\(t_i\)),所以\(P[i, w] = P[i-1, w]\);第三种状况,既能够选择\(t_i\)也能够不选择\(t_i\),所以须要在这两者之间找出最大值。咱们的目标是求出\(P[n, W]\)。
下面给出一个自底向上的Python实现代码:
def knapsack_0_1(T, w): P = [[0] * (w + 1)for i in range(len(T) + 1)] for i, t in enumerate(T): i = i + 1 # i从0开始迭代,所以必须加上1 for j in range(1, w + 1): if t[2] > j: P[i][j] = P[i - 1][j] else: P[i][j] = max(P[i - 1][j], t[1] + P[i-1][j - t[2]]) return P
咱们作一样的测试:
if __name__ == '__main__': ''' 1号商品:$60 - 10kg 2号商品:$100 - 20kg 3号商品:$120 - 30kg 限重 50kg ''' # 注意:下面咱们将重量都同步缩减为原来的0.1倍,不影响结果。 T = [(1, 60 , 1), (2, 100, 2), (3, 120, 3)] W = 5 print(knapsack_0_1(T, W)[len(T)][W])
打印结果为:220
分析上述动态规划算法,咱们发现其时间复杂度为:\(O(n \times W)\),比一开始的递归算法要好。
分数背包同0-1背包同样,也具备最优子结构,其证实和0-1背包差很少,这里再也不赘述。
在分数背包问题中,直觉告诉咱们,这样作可以总价值最高的商品:首先用平均价值最高的商品去填充背包。若背包限重还有剩余,则用平均价值第二高的商品去填充背包……以后的状况以此类推,直到背包被“填满”。先提早声明,背包必定是能被“填满”的,即所给的商品的总重量是大于背包的限重的;对于背包限重大于或等于商品总重量的“平凡”状况,没有考虑的必要。
以上的选择策略即是该问题的贪心选择。接下来咱们必须证实贪心选择是安全的。
考虑在某种最优选择方案中,在第\(k\)次选择时,\(t_i\)是当前所剩的商品中平均价值最高的商品。假设在该最优方案的该次选择中,选择的商品\(t_j\)不是平均价值最高的商品,即\(j \neq i\)。咱们能够采用剪切-粘贴法,即考虑用\(t_i\)去替代\(t_j\)(若\(w_i \geq w_j\),则取重量为\(w_j\)的部分\(t_i\)去替代;若\(w_i < w_j\),则取所有的\(t_i\)去替代 ),很明显,替代后的方案比以前的方案更优。所以假设不成立,即\(j = i\),即在第\(k\)次选择时,选择的商品\(t_j\)是剩余商品中平均价值最高的商品,因为\(k\)具备任意性,所以贪心选择是安全的。
有了上述的分析,咱们能够很容易设计出一个贪心算法,首先将全部商品按平均价值递减的顺序排序,而后再采用如上贪心选择策略作出选择,这样能够在\(O(n\lg n)\)的时间内解决分数背包问题。
下面给出Python实现代码:
def knapsack_fraction(T, w): T.sort(key = lambda t: t[1] / t[2], reverse= True) p = 0 for t in T: if w <= t[2]: p += w * t[1] / t[2] return p else: p += t[1] w -= t[2]
做以下测试:
if __name__ == '__main__': ''' 1号商品:$60 - 10kg 2号商品:$100 - 20kg 3号商品:$120 - 30kg 限重 50kg ''' T = [(1, 60 , 10), (2, 100, 20), (3, 120, 30)] W = 50 print(knapsack_fraction(T, W))
打印:240.0
你可能会想,为何不把用于分数背包问题的贪心算法也用于0-1呢?事实上,是不行的。从咱们测试的例子中,就能看出问题。
在分数背包中,最优方案背包里的物品是:10kg的1号商品,20kg的2号商品和20kg的3号商品。注意此时3号商品只取了20kg,它被“分割”了,而在0-1背包问题中,这种“分割”是不容许的。
换个角度说,若是咱们对0-1背包问题一样采用如上贪心算法,而且还要保证不能“分割”物品,那么最终咱们只能将10kg的1号商品,20kg的2号商品,其总价值为$160,背包还有20kg的载重空间被白白浪费了。
再换个角度,若是把上面的对分数背包贪心选择安全性的证实套用到0-1背包问题中,咱们就会发现,在证实中用\(t_i\)去替代\(t_j\)的作法不必定可以成功,缘由仍是由于0-1背包问题中,商品是不容许"切割"的,其重量老是一个固定值。