数据结构与算法 贪心

简介: 数据结构与算法 贪心

「贪心算法 greedy algorithm」是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期望获得全局最优解。

贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理是不同的。

  • 动态规划会根据之前阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解。
  • 贪心算法不会重新考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。

零钱兑换问题

Q:给定 𝑛 种硬币,第 𝑖 种硬币的面值为 𝑐𝑜𝑖𝑛𝑠[𝑖 − 1] ,目标金额为 𝑎𝑚𝑡 ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 −1 。

[1, 5, 10, 20, 50, 100]:在该硬币组合下,给定任意 target

def greedy(coins, target):
    coins.sort()
    i = len(coins)-1
    count = 0
  
    while target > 0:
        while i > 0 and coins[i] > target:
            i -= 1
        target -= coins[i]
        count += 1
  
    return count if target == 0 else -1

贪心优点与局限性

贪心算法不仅操作直接、实现简单,而且通常效率也很高。对于某些硬币面值组合,贪心算法并不能找到最优解。

一般情况下,贪心算法适用于以下两类问题。

  1. 可以保证找到最优解:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。
  2. 可以找到近似最优解:贪心算法在这种情况下也是可用的。对于很多复杂问题来说,寻找全局最优解是非常困难的,能以较高效率找到次优解也是非常不错的。

贪心算法特性和解题步骤

相较于动态规划,贪心算法的使用条件更加苛刻,其主要关注问题的两个性质。

  • 贪心选择性质:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。
  • 最优子结构:原问题的最优解包含子问题的最优解。

贪心问题的解决流程大体可分为以下三步。

  1. 问题分析:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。
  2. 确定贪心策略:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终能解决整个问题。
  3. 正确性证明:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要使用到数学证明,例如归纳法或反证法等。

确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,主要包含以下原因。

  • 不同问题的贪心策略的差异较大。对于许多问题来说,贪心策略都比较浅显,我们通过一些大概的思考与尝试就能得出。而对于一些复杂问题,贪心策略可能非常隐蔽,这种情况就非常考验个人的解题经验与算法能力了。
  • 某些贪心策略具有较强的迷惑性。当我们满怀信心设计好贪心策略,写出解题代码并提交运行,很可能发现部分测试样例无法通过。这是因为设计的贪心策略只是“部分正确”的,上文介绍的零钱兑换就是个典型案例。

为了保证正确性,我们应该对贪心策略进行严谨的数学证明,通常需要用到反证法或数学归纳法。然而,正确性证明也很可能不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行 Debug ,一步步修改与验证贪心策略。

贪心典型例题

贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题。

  • 硬币找零问题:在某些硬币组合下,贪心算法总是可以得到最优解。
  • 区间调度问题:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
  • 分数背包问题:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
  • 股票买卖问题:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
  • 霍夫曼编码:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最小的两个节点合并,最后得到的霍夫曼树的带权路径长度(即编码长度)最小。
  • Dijkstra 算法:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。

分数背包

给定 𝑛 个物品,第 𝑖 个物品的重量为𝑤𝑔𝑡[𝑖 − 1]、价值为 𝑣𝑎𝑙[𝑖 − 1] ,和一个容量为 𝑐𝑎𝑝的背包。每个物品只能选择一次,但可以选择物品的一部分,价值根据选择的重量比例计算,问在不超过背包容量下背包中物品的最大价值。

代码
def greedy(weights, values, cap):
    items = [Item(key, val) for key, val in zip(weights, values)]
    items.sort(key=lambda item: item.value/item.weight, reverse=True)
    n = len(items)
    i = 0
    val = 0
    while cap > 0 and i < n:
        if items[i].weight < cap:
            val += items[i].value
            cap = cap - items[i].weight
        else:
            val += items[i].value/items[i].weight * cap
            cap = 0
        i += 1
    return val

最大容量问题

输入一个数组 ℎ𝑡 ,数组中的每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。容器的容量等于高度和宽度的乘积(即面积),其中高度由较短的隔板决定,宽度是两个隔板的数组索引之差。请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。

def greedy(ht):
    i, j = 0, len(ht)-1
    res = 0
    while i < j:
        cap = min(ht[i], ht[j]) * (j - i)
        if cap > res:
            res = cap
        if ht[i] > ht[j]:
            j -= 1
        elif ht[i] == ht[j]:
            ix, jx = ht[i+1] - ht[i], ht[j-1] - ht[j]
            if ix >= jx:
                i += 1
            else:
                j -= 1
        else:
            i += 1
    return res

最大切分乘积问题

给定一个正整数 𝑛 ,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少。

总结以上,可推出以下贪心策略。

  1. 输入整数 𝑛,从其不断地切分出因子3,直至余数为 0、1、2 。
  2. 当余数为0时,代表𝑛是 3 的倍数,因此不做任何处理。
  3. 当余数为2时,不继续划分,保留之。
  4. 当余数为1时,由于2 × 2 > 1 × 3,因此应将最后一个3替换为2 。
代码
def greedy(num):
    import math
    if num <= 3:
        return num-1
    else:
        a = num//3
        b = num%3
        if b == 1:
            return int(math.pow(3, a-1) * 2 * 2)
        if b == 2:
            return int(math.pow(3, a) * 2)

重点回顾

  • 贪心算法通常用于解决最优化问题,其原理是在每个决策阶段都做出局部最优的决策,以期望获得全局最优解。
  • 贪心算法会迭代地做出一个又一个的贪心选择,每轮都将问题转化成一个规模更小的子问题,直到问题被解决。
  • 贪心算法不仅实现简单,还具有很高的解题效率。相比于动态规划,贪心算法的时间复杂度通常更低。
  • 在零钱兑换问题中,对于某些硬币组合,贪心算法可以保证找到最优解;对于另外一些硬币组合则不然,贪心算法可能找到很差的解。
  • 适合用贪心算法求解的问题具有两大性质:贪心选择性质和最优子结构。贪心选择性质代表贪心策略的有效性。
  • 对于某些复杂问题,贪心选择性质的证明并不简单。相对来说,证伪更加容易,例如零钱兑换问题。
  • 求解贪心问题主要分为三步:问题分析、贪心策略确定、正确性证明。其中,贪心策略确定是核心步骤,正确性证明往往是难点。
  • 分数背包问题在 0‑1 背包的基础上,允许选择物品的一部分,因此可使用贪心算法求解。贪心策略的正确性可以使用反证法来证明。
  • 最大容量问题可使用穷举法求解,时间复杂度为𝑂(𝑛2 ) 。通过设计贪心策略,每轮向内移动短板,可将时间复杂度优化至𝑂(𝑛) 。
  • 在最大切分乘积问题中,我们先后推理出两个贪心策略:≥ 4 的整数都应该继续切分、最优切分因子为 3 。代码中包含幂运算,时间复杂度取决于幂运算实现方法,通常为𝑂(1) 或 𝑂(log 𝑛) 。


目录
相关文章
|
存储 缓存 NoSQL
数据库性能优化中的缓存优化
数据库性能优化中的缓存优化
|
9月前
|
人工智能 监控 JavaScript
Playwright初学指南 (3):深入解析交互操作
本文深度解析Playwright如何通过智能等待、自动重试等机制解决Web自动化中60%的交互失败问题。从基础点击/输入到高级拖拽/iframe操作,提供企业级解决方案和性能优化技巧,帮助开发者实现98%的操作成功率,打造稳定高效的自动化测试体系。
|
数据可视化 Python
解决已经导入wordcloud还显示ModuleNotFoundError: No module named ‘wordcloud‘的问题
解决已经导入wordcloud还显示ModuleNotFoundError: No module named ‘wordcloud‘的问题
1187 0
|
小程序 API 调度
HarmonyOS Next快速入门:Stage模型和应用及组件级配置
本教程介绍HarmonyOS Next应用开发快速入门,重点讲解Stage模型及其配置。Stage模型自API 9起推出,是当前主推的模型,包含AbilityStage与WindowStage等类,支持UIAbility组件实现用户交互。UIAbility是系统调度的基本单元,可包含多页面实现功能模块。此外,教程还涉及配置文件`app.json5`和`module.json5`的应用图标、标签设置规则,帮助开发者高效构建HarmonyOS应用。
306 0
|
安全 网络安全 数据安全/隐私保护
访问控制列表(ACL)是网络安全管理的重要工具,用于定义和管理网络资源的访问权限。
访问控制列表(ACL)是网络安全管理的重要工具,用于定义和管理网络资源的访问权限。ACL 可应用于路由器、防火墙等设备,通过设定规则控制访问。其类型包括标准、扩展、基于时间和基于用户的ACL,广泛用于企业网络和互联网安全中,以增强安全性、实现精细管理和灵活调整。然而,ACL 也存在管理复杂和可能影响性能的局限性。未来,ACL 将趋向智能化和自动化,与其他安全技术结合,提供更全面的安全保障。
1184 4
|
应用服务中间件 PHP nginx
Docker-compose 编排lnmp(dockerfile) 完成Wordpress
通过使用Docker Compose,我们可以轻松编排LNMP环境并部署WordPress。本文详细介绍了各组件的Dockerfile和配置文件编写,并通过docker-compose.yml文件实现了整个环境的自动化部署。这种方法不仅简化了部署过程,还提高了环境的可移植性和一致性。希望本文能帮助你更好地理解和使用Docker Compose来管理和部署复杂的应用程序。
819 4
|
运维 监控 安全
物联网卡:物联网卡为什么不能使用在手机上
物联网卡(IoT SIM卡)通常是为物联网设备设计的,这些设备包括但不限于智能家居设备、可穿戴设备、工业监控设备等。它们与用于智能手机的SIM卡有所不同,主要是因为设计目标、功能限制、资费结构以及网络接入策略上的差异。以下是物联网卡不能直接在手机上使用的主要原因:
|
芯片 异构计算
第三章 硬件描述语言verilog(三)功能描述-时序逻辑
第三章 硬件描述语言verilog(三)功能描述-时序逻辑
851 0
第三章 硬件描述语言verilog(三)功能描述-时序逻辑
|
测试技术 开发者 Python
pyautogui,一个超酷的 Python 库!
pyautogui,一个超酷的 Python 库!
915 4