1-算法 - 贪心算法

about

贪心算法,又称贪婪算法,指的是,在对问题求解时,总是做出在当前看来是最好的选择。
也就是说,贪心算法不从整体最优进行考虑,而是只考虑当前(局部)的最优解。

贪心算法并不保证会得到最优解,但是在某些问题上,贪心算法的解就是最优解,这也就意味着,遇到相关问题时,我们要会判断解决当前问题,能否使用贪心算法来解决问题。

找零问题

假设商店老板需要找零n元钱,钱币的面额有:100、50、20、5、1元,且各个面额钱数量很多,总之能找开!
那么,问题来了:如何找零使得所需钱币的数量最少?
实现:

def greed(t, n):
    """
    贪心算法实现找零问题
    :param t: 不同面额的金钱列表,这个列表需要降序排序,要从最大面额的开始找
    :param n: n元钱,表示要对n元钱找零
    :return: 找零结果,根据传过来的值可以预期找零应该是 3张10的;1张50的,1张20的,1张5块的;一张1块的。
    """
    t.sort(reverse=True)  # 降序排序
    m = [0 for _ in t]
    for i, money in enumerate(t):
        """
        思路是:
            第一次循环,先用100的找
                m[i] = n // money 表示 376整除100等于3,表示100的需要找三张
                n %= money  找完100的,还剩多少呢?还剩76
            第二次循环:
                用76整除50等于1,也就是50的需要1张,零头还剩26块
            第三次循环:
                用26整除20等于1,表示20的也需要1张,零头还剩6块
            第三次循环:
                用6整除5等于1,表示5块的需要1张,零头还剩1
            第四次循环:
                1整除1等于1,表示1块需要1张,此时n就等于0,表示找开了,循环也结束了
        """
        m[i] = n // money
        n %= money
        # print(m, n, money)
    return m, n


t = [100, 50, 20, 5, 1]
print(greed(t, 376))  # ([3, 1, 1, 1, 1], 0)  # 返回的m表示找零结果是3张10的;1张50的,1张20的,1张5块的;一张1块的,而 n 值0表示找开了
# print(greed(t, 376.5))  # ([3.0, 1.0, 1.0, 1.0, 1.0], 0.5),0.5表示省5毛钱找不开,因为我们的t中只定义到了元,没有定义角

找零问题中,贪心就贪心在,先尽可能的给最大面额的,然后次之,这样最后找零的金钱数量是最少的。

背包问题

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

问题来了,他应该如何分配,才能让拿走的商品价值最高?

背包问题还可以进一步细分:

  • 0-1背包:有n种商品,且每种商品只有1个。这也就意味着,对于一个商品,小偷要么把它完整拿走,要么留下,不能只拿走部分,或者把一个商品拿走多次,比如商品为金条。
  • 分数背包:对于一个商品,小偷可以拿走其中任意一部分,比如商品为金砂。

思考:对于上面两种背包来说,贪心算法是否都能得到最优解?为什么?
首先来分析分数背包。
现有(价值v;重量千克):

  • 金砂:v1=120;w1=10kg
  • 银砂:v1=100;w1=20kg
  • 铜砂:v1=80;w1=30kg
  • 背包容量:W=50kg

根据常识,我们优先拿走所有的金砂,其次是所有银砂,如果背包还有空的话,再把铜砂也拿走。所以,结果就是金砂拿走10kg;银砂拿走20kg;此时背包容量还剩20kg,所以拿走铜砂20kg,背包满了,走人....

再来分析0-1背包。
现有(价值v;重量千克),如一根金条重10kg,价值120:

  • 一根金条:v1=120;w1=10kg
  • 一根银条:v1=100;w1=20kg
  • 一根铜条:v1=80;w1=30kg
  • 背包容量:W=50kg

根据常识,我们还是优先拿金条,然后是银条,此时背包容量还剩20kg,铜条这里就装不下了,此时的价值是120+100=220
这背包不满啊,我们再尝试其他的组合,拿银条和铜条,二者加一块正好是50kg,那价值几何?100+80=180
还剩最后的组合,拿金条和铜条,其价值120+80=200

这么一算,贪心算法就不一定适用了,因为该算法得到的结果不一定是最优解(背包不一定是满的)。

综合考虑,贪心算法只能解决分数背包的问题。来看下代码示例吧。

示例:

#           金砂        银砂      铜砂
goods = [(120, 10), (100, 20), (80, 30)]  # 每个商品元组(价值,重量)
goods.sort(key=lambda x: x[0] / x[1], reverse=True)  # 根据单价降序排序,因为要从价值最高的开始拿,尽可能的多拿,所以要将最有价值的排在首位


def fractional_backpack(goods, w):
    """
    分数背包实现
    :param goods: 商品列表
    :param w: 背包重量
    :return:
    m: 存储每种商品拿多少,它是介于0~1之间数字,1表示拿走当前商品的所有,零点几表示拿走一部分
    """
    m = [0 for _ in range(goods.__len__())]
    total_v = 0  # 拿走商品的总价值
    for i, (price, weight) in enumerate(goods):
        if w >= weight:  # 表示背包容量大于当前商品的重量,可以将这个商品都拿走
            m[i] = 1  # 拿走所有商品
            w -= weight  # 背包容量要减去拿走商品的重量
            total_v += price  # 拿走当前商品的总价值
        else:  # 表示当前背包容量不足以拿走当前完整的商品,只能拿走一部分,这一部分是多少,是需要计算的
            m[i] = round(w / weight, 2)  # 当前背包的容量除以当前商品的重量,也就是拿走一部分
            w = 0  # 拿走的这一部分商品,正好填满背包的容量,也就是背包满了
            total_v += m[i] * price  # 部分商品的价值
            break  # 背包满了,推出就行了
    return total_v, m


print(fractional_backpack(goods, 50))  # (273.6, [1, 1, 0.67])

分数背包能使拿走的商品价值最大化。

数字拼接问题

有n个非负整数,将其按照字符串拼接的方式拼接为一个整数,如何拼接可以使拼接出的整数最大?
如32, 94, 128, 1286, 6, 71,这几个值比较判断......最终得到的结果是94716321286128。

问题:字符串类型的数字如何比较大小?
比较两个字符串类型的整数大小,利用的是其在ASCII编码的位置来决定大小的。

字符串的编码比较机制就是从字符串第一位开始比较,如果相等,再比较第二位,以此类推,得出较大的字符串。
对于长度不同的字符串,前面的都相等,后面的有字符的大:

print('6' > '32')  # True
print('96' > '87')  # True
print('128' > '1276')  # True
print('128' > '1286')  # False


上面的比较逻辑都没问题,但如果用于作为数字拼接问题的解决算法,就存在问题了。

首先,对于长度相等的没问题。但对于长度不相等的肯定有问题了,如下面的两组数字进行拼接:

a, b = "128" "1286"    # b>a拼接结果如下:
b + a = "1286128"  # 拼接结果要比a+b大
a + b = "1281286"
# 上面这种情况没问题

# 但下面这种情况就有问题了
c, d = "728" "7286"  # d>c拼接结果
d + c = "7286728"  # d>c的拼接结果要比c>d大
c + d = "7287286"

对于上面存在的问题,怎么解决呢?也好办,我们将两组字符串进行拼接后比较:

# before
c, d = '728', '7286'
r = c + d if c > d else d + c
print(r)  # 7286728

# after
c, d = '728', '7286'
r = c + d if c + d > d + c else d + c
print(r)  # 7287286

按照优化后的思路实现代码:

from functools import cmp_to_key


def my_cmp(x, y):
    """ 对比两个值返回对比结果 """
    if x + y < y + x:
        return 1
    elif x + y > y + x:
        return -1
    else:  # 相等
        return 0


def number_join(li):
    """
    数字拼问题
    :param li: 带拼接的数字列表
    :return: 拼接后的字符串
    """
    # 首先,将列表中的整数都转换为字符串,方便后后续比较
    li = list(map(str, li))
    # 然后对列表进行降序排序,将最大的放到最前面
    # 这一步关键就是根据值的大小进行交换,如: ["32", "94"] --> ["94", "32"]
    li.sort(key=cmp_to_key(my_cmp))
    # 上面一步执行完,得到的降序列表就是待拼接前的列表了
    # print(li)  # ['94', '71', '6', '32', '1286', '128']
    # return ''.join(li)  # 94716321286128
    return ','.join(li)  # 为了便于观察,这里以逗号分进行分割


li = [32, 94, 128, 1286, 6, 71]
print(number_join(li))  # 94,71,6,32,1286,128

上面使用的是内置排序算法,你也可以使用其他的排序算法来完成排序。
参考:

活动选择问题

假设有n个活动要举行,这些活动要占用同一片场地,但场地在同一时刻只能举办一个活动。
每个活动都有一个开始时间si和结束时间fi(这里的时间以整数表示)。
问题:如下图,有11场活动,如何安排才能使这个场地能够举办的活动最多?

image

贪心结论:最先结束的活动一定是最优解的一部分。
证明:假设活动a是所有活动中最先结束的活动,活动b是最优解中最先结束的活动。

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

代码示例:

activities = [
    (1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)
]  # 元组(活动开始时间,活动结束时间)


def activity_selection(a):
    """
    活动选择问题
    :param a: 所有活动列表
    :return m: 选择出的活动列表
    """
    # 首先根据最先结束的活动进行降序排序活动列表
    a.sort(key=lambda x: x[1])
    m = [a[0]]  # 将最先结束的活动先放到m中
    for i in range(1, len(a)):  # 最先结束的活动放到了m中,这里直接从第二个开始循环,挨着看每个活动跟m中的最后一个活动是否冲突
        # 在循环中判断,当前活动和m中的最后一个活动进行时间对比
        # 如果当前活动的开始时间小于m中的最后一个活动的结束时间,表示冲突了,这个当前活动就被抛弃掉
        # 如果当前活动的开始时间大于等于m中的最后一个活动的结束时间,表示不冲突,可以将这个活动添加到m中
        # 也允许这种活动:8-10点,10-12点,即允许在结束的时间点,开始下一场活动
        if a[i][0] >= m[-1][1]:
            m.append(a[i])
    return m


print(activity_selection(activities))  # [(1, 4), (5, 7), (8, 11), (12, 16)]
posted @ 2018-06-19 17:00  听雨危楼  阅读(1848)  评论(0编辑  收藏  举报