zoukankan      html  css  js  c++  java
  • 贪心算法(2)——算法导论(22)

    1. 写在前面

    上一篇博客中,我们通过选择问题了解了贪心算法。这一篇博客将继续介绍贪心算法,主要谈谈贪心算法的原理,并简单分析一下背包问题

    2. 贪心算法原理

    通过上一篇博客中的选择问题,我们看到,贪心算法可以由如下几个步骤来实现:

    1. 确定问题的最优子结构;
    2. 设计一个递归算法;
    3. 证明如果我们做出一个贪心选择,则只剩下一个子问题;
    4. 证明贪心选择是安全的;
    5. 设计并实现贪心算法。

    对比动态规划,我们发现贪心算法和它十分相似,首先它们都必须具备最优子结构性质,然后通常都是将原问题分解为子问题,根据最优子结构性质与问题的分解,设计一个递归算法。不同之处在于,动态规划算法在对问题进行分解时,由于无法确定哪一种分解能够得到原问题的最优解,因此需要考察所有的分解情况,并且正是由于这种分解问题的不确定性,通常会导致子问题重叠,为了提高效率,通常会用一个“备忘录”去备忘子问题的解;而在贪心算法中,我们很明确的知道如何分解问题能产生最优解(或者说是很明确的知道哪种选择的结果在最优解中),因此通常贪心算法没有子问题重叠重叠问题,但我们必须要确保贪心选择的正确性。

    动态规划一样,我们也总结出贪心算法的两个特点(必要条件)。

    2.1. 最优子结构性质

    这个性质和动态规划一样,因此不再赘述:

    如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质。

    2.2. 贪心选择性质

    对于某个问题,如果我们可以通过做出局部最优(贪心)选择来构造全局最优解,那么我们就称该问题具有贪心选择性质

    3. 背包问题

    下面通过两种不同形式的背包问题,来进一步说明动态规划算法贪心算法的区别之处,进而加深对贪心算法的理解。

    首先说明一下背包问题

    给定一组物品和一个限定重量的背包,每种物品都有自己的重量和价格。在限定的总重量内,我们如何选择,才能使得背包内的物品总价格最高。

    0-1背包问题中,对于每一件物品,你只能做出二元(0-1)选择,即只能选择 选择该物品或不选择该物品,而不能只选择该物品的一部分;分数背包问题相反。

    我们先做如下说明:

    设共有(n)件物品,记为(t_1,t_2,...,t_n),其中物品(t_i)重量为(w_i),价值为(p_i);限重为(W)

    3.1. 0-1背包

    首先,我们可证明,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} = egin {cases} 0 & ext{若$T = {t_i}, w_i > w$ }\ p_i& ext{若$T = {t_i}, w_i leq w$}\ maxlimits_{t_i in T, w_i leqslant w}(p_i + P_{T-t_i, w-w_i})& ext{其他} 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] = egin {cases} 0 & ext{$w=0$或$i=0$}\ P[i-1, w] & ext{$w_i > w, i = 1, 2,..., n $}\ maxlimits_{w_i leq w} {P[i-1, w], p_i + P[i-1, w-w_i]} & ext{$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 imes W)),比一开始的递归算法要好。

    3.2. 分数背包

    分数背包同0-1背包一样,也具有最优子结构,其证明和0-1背包差不多,这里不再赘述。

    在分数背包问题中,直觉告诉我们,这样做能够总价值最高的商品:首先用平均价值最高的商品去填充背包。若背包限重还有剩余,则用平均价值第二高的商品去填充背包……之后的情况以此类推,直到背包被“填满”。先提前声明,背包一定是能被“填满”的,即所给的商品的总重量是大于背包的限重的;对于背包限重大于或等于商品总重量的“平凡”情况,没有考虑的必要。

    以上的选择策略便是该问题的贪心选择。接下来我们必须证明贪心选择是安全的。

    考虑在某种最优选择方案中,在第(k)次选择时,(t_i)是当前所剩的商品中平均价值最高的商品。假设在该最优方案的该次选择中,选择的商品(t_j)不是平均价值最高的商品,即(j eq 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(nlg 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

    3.3 0-1背包 VS 分数背包

    你可能会想,为什么不把用于分数背包问题的贪心算法也用于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背包问题中,商品是不允许"切割"的,其重量总是一个固定值。

  • 相关阅读:
    如何查看MySQL的当前存储引擎?
    避免生产环境执行更新删除语句忘记加where条件的解决方案
    物联网发展的现状
    目前行业内比较流行的开源时序数据库产品
    如何查看端口(3306)被那个程序占用
    MySQL数据库开发的36条军规
    介绍 MySQL 8 中值得关注的新特性和改进。
    IE浏览器 兼容性(IE9-11 差异说明)
    python3:(unicode error) 'utf-8' codec can't decode
    静态代码块
  • 原文地址:https://www.cnblogs.com/dongkuo/p/5596398.html
Copyright © 2011-2022 走看看