贪心算法

  贪心算法(又称贪婪算法)是指,在堆问题求解时,总是做出当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出是在某种意义上的局部最优解

  贪心算法并不保证会得到最优解,但是在某些问题上贪心算法的解就是最优解。要会判断一个问题能否用贪心算法来计算。

一、找零问题

  假设商店老板需要找零n元钱,钱币的面额有:100元、50元、20元、5元、1元,如何找零使得所需钱币的数量最小?

t = [100, 50, 20, 5, 1]

def change(t, n):   # n指的总金额
    m = [0 for _ in range(len(t))]   # 创建一个和t一样长但全是0的列表
    # 这里假设t都是倒序排好的
    for i, money in enumerate(t):
        m[i] = n // money  # n整除money:376//100=3
        n = n % money  # n对money取余:376%100=76
    return m, n   # 如果到最后找不开,n就是找不开的钱

print(change(t, 376))    # ([3, 1, 1, 1, 1], 0)   即:300+50+20+5+1

# 如果t = [100, 50, 20, 5],则有1块钱找不开,
print(change(t, 451))    # ([4, 1, 0, 0], 1)  

二、背包问题

  一个小偷在某个商店发现有n个商品,第i个商品价值vi元,重wi千克。他希望拿走的价值尽量高,但他的背包最多只能容纳W千克的东西。他应该拿走哪些商品?

1、0-1背包

  对于一个商品,小偷要么把它完整拿走,要么留下。不能只拿走一部分,或把一个商品拿走多次。(商品为金条)

2、分数背包

  对于一个商品,小偷可以拿走其中任意一部分。(商品为金砂)

3、问题示例

  • 商品1:v1=60   w1=10
  • 商品2:v2=100   w2=20
  • 商品3:v3=120   w3=30
  • 背包容量:W=50
  • 对于0-1背包和分数背包,贪心算法是否都能得到最优解?为什么?

  答:贪心算法对分数背包可以得到最优解,对0-1背包得不到最优解,很可能背包都无法装满。

  两种背包问题都具有最优子结构性质。对0-1背包问题,考虑重量不超过W而价值最高的装包方案。如果我们将商品j从此方案中删除,则剩余商品必须是重量不超过W-wj的价值最高的方案(小偷只能从不包括商品j的n-1个商品中选择拿走哪些)。

  虽然两个问题相似,但我们用贪心策略可以求解背包问题,而不能求解0-1背包问题,为了求解部分数背包问题,我们首先计算每个商品的每磅价值vi/wi。遵循贪心策略,小偷首先尽量多地拿走每磅价值最高的商品,如果该商品已全部拿走而背包未装满,他继续尽量多地拿走每磅价值第二高的商品,依次类推,直到达到重量上限W。因此,通过将商品按每磅价值排序,贪心算法的时间运行时间是O(nlgn)。

  为了说明贪心这一贪心策略对0-1背包问题无效,考虑下图所示的问题实例。此例包含3个商品和一个能容纳50磅重量的背包。商品1重10磅,价值60美元。商品2重20磅,价值100美元。商品3重30磅,价值120美元。因此,商品1的每磅价值为6美元,高于商品2的每磅价值5美元和商品3的每磅价值4美元。因此,上述贪心策略会首先拿走商品1。但是,最优解应该是商品2和商品3,而留下商品1。拿走商品1的两种方案都是次优的。

  但是,对于分数背包问题,上述贪心策略首先拿走商品1,是可以生成最优解的。拿走商品1的策略对0-1背包问题无效是因为小偷无法装满背包,空闲空间降低了方案的有效每磅价值。在0-1背包问题中,当我们考虑是否将一个商品装入背包时,必须比较包含此商品的子问题的解与不包含它的子问题的解,然后才能做出选择。这会导致大量的重叠子问题——动态规划的标识。

4、分数背包代码实现

def fractional_backpack(goods, w):
    """
    贪心算法——分数背包
    :param goods: 商品
    :param w: 背包容量
    :return:
    """
    m = [0 for _ in range(len(goods))]   # [0, 0, 0]
    total_v = 0    # 总价值
    for i, (price, weight) in enumerate(goods):
        if w >= weight:
            m[i] = 1
            total_v += price
            w -= weight
        else:  # 不足1
            m[i] = w / weight
            total_v += m[i]*price
            w = 0
            break
    return m, total_v

print(fractional_backpack(goods, 50))   # ([1, 1, 0.6666666666666666], 240.0)   60+100+120*2/3=60+100+80=240

三、拼接最大数字问题

  有n个非负整数,将其按照字符串拼接的方式拼接为一个整体。如何拼接可以使得得到的整数最大?

  例:32,94,128,1286,6,71可以拼接出的最大整数为94716321286128

from functools import cmp_to_key   # 传递python2中sort的cmp函数

li = [32, 94, 128, 1286, 6, 71]

def xy_cmp(x, y):
    if x+y < y+x:  # 要让大的在前面,这里需要交换
        return 1
    elif x+y > y+x:
        return -1
    else:
        return 0

def number_join(li):
    li = list(map(str, li))   # 转换为字符串:['32', '94', '128', '1286', '6', '71']
    # 方法1:cmp函数
    li.sort(key=cmp_to_key(xy_cmp))  # 传递进xy_cmp函数
    # 方法2:使用快排、冒泡排序等
    return "".join(li)

print(number_join(li))    # 94716321286128

四、活动选择问题

  假设有n个活动,这些互动要占用同一片场地,而场地在某时刻只能供一个活动使用。

  每个活动都有一个开始时间si结束时间fi(题目中时间以整数表示),表示活动在[si, fi)(左闭右开)区间占用场地。

  问:安排哪些活动能够使该场地举办的活动个数最多

  

1、贪心结论和证明

  贪心结论:最先结束的活动一定是最优解的一部分

  证明:假设a是所有活动中最先结束的活动,b是最优解中最先结束的活动。

  • 如果a=b,结论成立。
  • 如果a≠b,则b的结束时间一定晚于a的结束时间,则此时用a替换掉最优解中的b,a一定不与最优解中的其他活动时间重叠,因此替换后的解也是最优解。

 2、活动选择问题实现

activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]

# 保证活动是按照结束时间排好序的(每次选择最早结束的)
activities.sort(key=lambda x: x[1])
# print(activities)

def activity_selection(a):
    """
    活动选择
    :param a: 活动
    :return:
    """
    res = [a[0]]  # 将最早结束的活动放入结果中
    for i in range(1, len(a)):   # 对列表进行遍历(除去第一个)
        if a[i][0] >= res[-1][-1]:  # 当前活动开始时间 大于等于 res最后一个活动的结束时间 因此不冲突
            res.append(a[i])
    return res

print(activity_selection(activities))   # [(1, 4), (5, 7), (8, 11), (12, 16)]

五、总结

  都是最优化问题。大大提升了解决问题的速度,但是它也有一些优化问题无法解决或得到的不是最优解。

贪心算法存在的问题:

  • 不能保证求得的最后解是最佳的;
  • 不能用来求最大或最小解问题;
  • 只能求满足某些约束条件的可行解的范围。

 

posted @ 2018-10-09 00:08  休耕  阅读(738)  评论(0编辑  收藏  举报