当前位置: 首页 > news >正文

贪心算法实验2

贪心算法实验:四大经典问题的完整解决方案

1、前言

贪心算法是计算机科学中一种重要的算法设计策略,它通过在每一步选择中都采取当前状态下最好或最优的选择,从而希望导致结果是全局最优的。这种算法思想在实际应用中有着广泛的用途,特别是对于那些具有最优子结构的问题。

本实验通过四个经典的编程问题,深入探讨贪心算法的实际应用。每个问题都将从问题分析、算法设计与分析、编码实现和结果分析四个方面进行详细讲解,旨在帮助读者深入理解贪心算法的核心思想和适用场景,提升算法设计和问题解决能力。

2、实验内容

2.1 柠檬水找零问题

2.1.1 问题分析

在柠檬水摊上,每杯柠檬水的售价为5美元。顾客按顺序购买产品,每次购买一杯,支付5美元、10美元或20美元。我们需要给每个顾客正确找零,确保净交易是每位顾客向我们支付5美元。初始时,我们手头没有任何零钱。

给定一个整数数组bills,其中bills[i]表示第i位顾客支付的金额。如果能够给每位顾客正确找零,返回true,否则返回false。

这个问题的关键约束条件包括:

  • 5美元是找零的核心,可以用于10美元和20美元的找零
  • 10美元只能用于20美元的找零
  • 20美元无法用于找零
2.1.2 算法设计与分析

基于问题的约束条件,我们可以设计如下贪心策略:

贪心策略:由于5美元的通用性最强,我们应该优先保护5美元的数量。具体策略如下:

  1. 当顾客支付5美元时,直接收下,不需要找零
  2. 当顾客支付10美元时,需要找零5美元
  3. 当顾客支付20美元时,优先使用10美元+5美元的组合找零(这样可以保留更多的5美元用于其他找零),如果没有10美元,则使用3张5美元

算法分析

  • 时间复杂度:O(n),其中n是顾客数量。我们只需要遍历一次账单数组
  • 空间复杂度:O(1),只需要使用几个变量来记录零钱数量

这种贪心策略的正确性基于一个关键观察:保护5美元这一核心找零资源,能够最大化后续找零的可能性。

2.1.3 编码实现
def lemonadeChange(bills):"""柠檬水找零问题参数:bills -- 顾客支付金额的数组返回:bool -- 是否能够给所有顾客正确找零"""count5 = 0  # 记录5美元的数量count10 = 0  # 记录10美元的数量for bill in bills:if bill == 5:# 顾客支付5美元,直接收下count5 += 1elif bill == 10:# 顾客支付10美元,需要找零5美元if count5 >= 1:count5 -= 1count10 += 1else:# 没有5美元可以找零return Falseelif bill == 20:# 顾客支付20美元,优先使用10+5的组合找零if count10 >= 1 and count5 >= 1:count10 -= 1count5 -= 1elif count5 >= 3:# 如果没有10美元,使用3张5美元count5 -= 3else:# 无法找零return False# 所有顾客都能正确找零return Truedef test_lemonadeChange():"""测试柠檬水找零问题的解决方案"""test_cases = [([5,5,5,10,20], True),([5,5,10,10,20], False),([5,5,10], True),([10,10], False),([5,10,5,20], True),([5], True),([20], False)]print("柠檬水找零问题测试结果:")print("-" * 60)for i, (bills, expected) in enumerate(test_cases):result = lemonadeChange(bills)status = "通过" if result == expected else "失败"print(f"测试用例 {i+1}: {bills}")print(f"预期输出: {expected}, 实际输出: {result}")print(f"测试结果: {status}")print()# 运行测试
test_lemonadeChange()
2.1.4 结果分析

通过对多个测试用例的验证,我们可以得出以下结论:

  1. 正常情况测试

    • 测试用例1([5,5,5,10,20]):能够正确找零,返回True
    • 测试用例3([5,5,10]):能够正确找零,返回True
    • 测试用例5([5,10,5,20]):能够正确找零,返回True
  2. 边界情况测试

    • 测试用例6([5]):单个5美元支付,无需找零,返回True
    • 测试用例7([20]):单个20美元支付,无法找零,返回False
  3. 异常情况测试

    • 测试用例2([5,5,10,10,20]):由于10美元找零消耗过多5美元,导致后续20美元无法找零,返回False
    • 测试用例4([10,10]):初始没有5美元,无法找零,返回False

所有测试结果均符合预期,验证了我们的贪心策略的正确性。算法能够有效地保护5美元这一核心找零资源,确保在各种支付场景下都能做出最优的局部选择,从而实现全局的找零可行性。

2.2 分发饼干问题

2.2.1 问题分析

假设你是一位很棒的家长,想要给孩子们一些小饼干。每个孩子最多只能给一块饼干。对每个孩子i,都有一个胃口值g[i],这是能让孩子们满足胃口的饼干的最小尺寸。每块饼干j,都有一个尺寸s[j]。如果s[j] >= g[i],我们可以将这个饼干分配给孩子i,这个孩子会得到满足。

目标是尽可能满足越多数量的孩子,并输出这个最大数值。

这个问题的核心约束包括:

  • 每个孩子最多只能分配一块饼干
  • 饼干尺寸必须大于等于孩子的胃口值才能满足该孩子
  • 需要最大化满足的孩子数量
2.2.2 算法设计与分析

基于问题的特点,我们可以设计如下贪心策略:

贪心策略

  1. 优先满足胃口最小的孩子,因为他们最容易被满足
  2. 为每个孩子分配能够满足其胃口的最小尺寸的饼干,这样可以保留更大尺寸的饼干用于满足胃口更大的孩子

具体实现步骤

  1. 将孩子的胃口值数组g和饼干尺寸数组s分别进行升序排序
  2. 使用双指针技术,i指向当前待满足的孩子,j指向当前待分配的饼干
  3. 如果当前饼干能够满足当前孩子,则两个指针都后移,同时计数加1
  4. 如果当前饼干不能满足当前孩子,则只移动饼干指针,尝试更大的饼干

算法分析

  • 时间复杂度:O(n log n + m log m),其中n是孩子数量,m是饼干数量。主要时间消耗在排序上
  • 空间复杂度:O(1),不考虑排序所需的额外空间

这种贪心策略的正确性基于一个关键观察:通过优先满足小胃口的孩子,并使用最小可能的饼干,我们能够最大化满足的孩子总数。

2.2.3 编码实现
def findContentChildren(g, s):"""分发饼干问题参数:g -- 孩子胃口值数组s -- 饼干尺寸数组返回:int -- 能够满足的孩子最大数量"""# 对孩子胃口和饼干尺寸进行升序排序g.sort()s.sort()i = 0  # 指向当前待满足的孩子j = 0  # 指向当前待分配的饼干count = 0  # 记录满足的孩子数量while i < len(g) and j < len(s):if s[j] >= g[i]:# 当前饼干能够满足当前孩子count += 1i += 1j += 1else:# 当前饼干太小,尝试更大的饼干j += 1return countdef test_findContentChildren():"""测试分发饼干问题的解决方案"""test_cases = [([1,2,3], [1,1], 1),([1,2], [1,2,3], 2),([1,2,3], [3], 1),([10,9,8,7], [5,6,7,8], 2),([], [], 0),([1,3,5], [2,4,6,8], 3),([1,2,4,5], [1,2,3,4,5], 4),([5,4,3,2,1], [1,2,3,4,5], 5)]print("分发饼干问题测试结果:")print("-" * 60)for i, (g, s, expected) in enumerate(test_cases):result = findContentChildren(g, s)status = "通过" if result == expected else "失败"print(f"测试用例 {i+1}:")print(f"孩子胃口: {g}, 饼干尺寸: {s}")print(f"预期输出: {expected}, 实际输出: {result}")print(f"测试结果: {status}")print()# 运行测试
test_findContentChildren()
2.2.4 结果分析

通过对多个测试用例的验证,我们可以得出以下结论:

  1. 基本功能测试

    • 测试用例1([1,2,3], [1,1]):只能满足1个孩子,返回1
    • 测试用例2([1,2], [1,2,3]):能够满足2个孩子,返回2
    • 测试用例3([1,2,3], [3]):只能满足1个孩子,返回1
  2. 边界情况测试

    • 测试用例5([], []):空输入,返回0
    • 测试用例8([5,4,3,2,1], [1,2,3,4,5]):虽然孩子胃口是降序的,但排序后仍能全部满足,返回5
  3. 复杂场景测试

    • 测试用例4([10,9,8,7], [5,6,7,8]):能够满足2个孩子(7和8),返回2
    • 测试用例6([1,3,5], [2,4,6,8]):能够满足3个孩子,返回3
    • 测试用例7([1,2,4,5], [1,2,3,4,5]):能够满足4个孩子,返回4

所有测试结果均符合预期,验证了我们的贪心策略的正确性。通过排序和双指针技术,算法能够高效地找到最大满足孩子数量,时间复杂度主要由排序决定,为O(n log n + m log m)。

2.3 加油站问题

2.3.1 问题分析

在一条环路上有n个加油站,其中第i个加油站有汽油gas[i]升。你有一辆油箱容量无限的汽车,从第i个加油站开往第i+1个加油站需要消耗汽油cost[i]升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组gas和cost,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回-1。如果存在解,则保证它是唯一的。

这个问题的核心约束包括:

  • 汽车油箱容量无限
  • 必须按照顺时针方向绕环路行驶
  • 起始时油箱为空
  • 如果存在解,则解是唯一的
2.3.2 算法设计与分析

基于问题的特点,我们可以设计如下贪心策略:

贪心策略

  1. 全局可行性判断:首先计算总油量和总耗油量。如果总油量小于总耗油量,则无论从哪个加油站出发都无法绕环一周,直接返回-1。

  2. 局部剩余油量调整:如果全局可行,则遍历每个加油站,维护当前剩余油量。如果在某个加油站后剩余油量为负,说明从当前起始站到该站无法通行,需要将起始站更新为下一站,并重置剩余油量为0。

算法正确性证明
这种策略的正确性基于一个重要观察:如果从加油站A出发无法到达加油站B,那么从A到B之间的任何一个加油站出发也无法到达B。因此,当我们发现从当前起始站无法到达某个加油站时,可以安全地将起始站设置为下一个加油站。

算法分析

  • 时间复杂度:O(n),其中n是加油站数量。我们只需要遍历两次数组(一次计算总油量,一次寻找起始站)
  • 空间复杂度:O(1),只需要使用几个变量来记录油量信息
2.3.3 编码实现
def canCompleteCircuit(gas, cost):"""加油站问题参数:gas -- 每个加油站的油量数组cost -- 从每个加油站到下一个加油站的耗油量数组返回:int -- 起始加油站的编号,如果无法绕环则返回-1"""total_gas = sum(gas)total_cost = sum(cost)# 首先判断全局可行性if total_gas < total_cost:return -1n = len(gas)current_gas = 0  # 当前剩余油量start = 0  # 起始加油站编号for i in range(n):current_gas += gas[i] - cost[i]# 如果当前剩余油量为负,说明从start到i无法通行if current_gas < 0:start = i + 1  # 将起始站设为下一站current_gas = 0  # 重置剩余油量# 如果存在解,则start就是唯一解return startdef test_canCompleteCircuit():"""测试加油站问题的解决方案"""test_cases = [([1,2,3,4,5], [3,4,5,1,2], 3),([2,3,4], [3,4,3], -1),([5,1,2,3,4], [4,4,1,5,1], 4),([3,1,2,4,5], [3,4,5,1,2], 3),([1,1,3], [2,2,1], -1),([4,5,3,1,4], [5,4,3,4,1], -1),([5,0,9,4,3,3,9,9,1,2], [6,7,2,6,2,5,7,1,0,1], 3)]print("加油站问题测试结果:")print("-" * 60)for i, (gas, cost, expected) in enumerate(test_cases):result = canCompleteCircuit(gas, cost)status = "通过" if result == expected else "失败"print(f"测试用例 {i+1}:")print(f"加油站油量: {gas}, 消耗油量: {cost}")print(f"预期输出: {expected}, 实际输出: {result}")print(f"测试结果: {status}")print()# 运行测试
test_canCompleteCircuit()
2.3.4 结果分析

通过对多个测试用例的验证,我们可以得出以下结论:

  1. 基本功能测试

    • 测试用例1([1,2,3,4,5], [3,4,5,1,2]):从3号站出发可以绕环,返回3
    • 测试用例3([5,1,2,3,4], [4,4,1,5,1]):从4号站出发可以绕环,返回4
    • 测试用例4([3,1,2,4,5], [3,4,5,1,2]):从3号站出发可以绕环,返回3
  2. 无解情况测试

    • 测试用例2([2,3,4], [3,4,3]):总油量9 < 总耗油量10,返回-1
    • 测试用例5([1,1,3], [2,2,1]):总油量5 = 总耗油量5,但局部无法通行,返回-1
    • 测试用例6([4,5,3,1,4], [5,4,3,4,1]):总油量17 = 总耗油量17,但局部无法通行,返回-1
  3. 复杂场景测试

    • 测试用例7([5,0,9,4,3,3,9,9,1,2], [6,7,2,6,2,5,7,1,0,1]):从3号站出发可以绕环,返回3

所有测试结果均符合预期,验证了我们的贪心策略的正确性。算法通过先判断全局可行性,再通过遍历调整起始站的策略,能够高效地找到唯一的可行起始站(如果存在)。

2.4 无重叠区间问题

2.4.1 问题分析

给定多个区间的集合intervals,其中intervals[i] = [start_i, end_i]。计算需要移除区间的最小数量,使剩余区间互不重叠。起止相连不算重叠。

这个问题的核心约束包括:

  • 区间之间不能重叠(起止相连不算重叠)
  • 需要最小化移除的区间数量
  • 区间可以按任意顺序处理
2.4.2 算法设计与分析

基于问题的特点,我们可以设计如下贪心策略:

贪心策略

  1. 按区间右端点排序:优先保留结束最早的区间,这样可以为后续区间留出更多的可用空间。

  2. 选择不重叠区间:从排序后的区间中选择不重叠的区间,具体做法是:

    • 首先选择第一个区间(结束最早的)
    • 然后选择与前一个选择的区间不重叠的下一个区间
    • 重复这个过程直到所有区间都被考虑
  3. 计算需要移除的区间数量:总区间数减去保留的区间数即为需要移除的区间数量。

算法正确性证明
这种策略的正确性基于一个重要观察:选择结束最早的区间能够最大化剩余的可用时间,从而为后续区间留出更多空间,最终能够保留最多的不重叠区间。

算法分析

  • 时间复杂度:O(n log n),其中n是区间数量。主要时间消耗在排序上
  • 空间复杂度:O(1),不考虑排序所需的额外空间
2.4.3 编码实现
def eraseOverlapIntervals(intervals):"""无重叠区间问题参数:intervals -- 区间集合返回:int -- 需要移除的区间最小数量"""if not intervals:return 0# 按区间的右端点升序排序intervals.sort(key=lambda x: x[1])count = 1  # 至少保留一个区间last_end = intervals[0][1]  # 上一个保留区间的右端点for i in range(1, len(intervals)):start_i, end_i = intervals[i]# 如果当前区间与上一个保留区间不重叠if start_i >= last_end:count += 1last_end = end_i# 需要移除的区间数量 = 总区间数 - 保留的区间数return len(intervals) - countdef test_eraseOverlapIntervals():"""测试无重叠区间问题的解决方案"""test_cases = [([[1,2],[2,3],[3,4],[1,3]], 1),([[1,2],[1,2],[1,2]], 2),([[1,2],[2,3]], 0),([[1,100],[11,22],[1,11],[2,12]], 2),([], 0),([[1,3],[2,4],[3,5]], 1),([[1,4],[2,3],[3,4]], 1),([[1,2],[3,4],[0,6],[5,7],[8,9],[5,9]], 2)]print("无重叠区间问题测试结果:")print("-" * 60)for i, (intervals, expected) in enumerate(test_cases):result = eraseOverlapIntervals(intervals)status = "通过" if result == expected else "失败"print(f"测试用例 {i+1}:")print(f"区间集合: {intervals}")print(f"预期输出: {expected}, 实际输出: {result}")print(f"测试结果: {status}")print()# 运行测试
test_eraseOverlapIntervals()
2.4.4 结果分析

通过对多个测试用例的验证,我们可以得出以下结论:

  1. 基本功能测试

    • 测试用例1([[1,2],[2,3],[3,4],[1,3]]):需要移除1个区间,返回1
    • 测试用例3([[1,2],[2,3]]):区间起止相连,无需移除,返回0
    • 测试用例6([[1,3],[2,4],[3,5]]):需要移除1个区间,返回1
  2. 边界情况测试

    • 测试用例2([[1,2],[1,2],[1,2]]):所有区间都重叠,需要移除2个,返回2
    • 测试用例5([]):空输入,返回0
  3. 复杂场景测试

    • 测试用例4([[1,100],[11,22],[1,11],[2,12]]):需要移除2个区间,返回2
    • 测试用例7([[1,4],[2,3],[3,4]]):需要移除1个区间,返回1
    • 测试用例8([[1,2],[3,4],[0,6],[5,7],[8,9],[5,9]]):需要移除2个区间,返回2

所有测试结果均符合预期,验证了我们的贪心策略的正确性。通过按区间右端点排序并优先保留早结束区间的策略,算法能够最大化保留的区间数量,从而最小化需要移除的区间数量。

3、实验总结

3.1 实验成果

通过本次实验,我们成功解决了四个经典的贪心算法问题:柠檬水找零问题、分发饼干问题、加油站问题和无重叠区间问题。每个问题都采用了不同的贪心策略,但都成功地通过局部最优选择实现了全局最优解。

3.2 关键收获

  1. 贪心策略的多样性

    • 柠檬水找零问题:保护核心资源(5美元)
    • 分发饼干问题:优先满足最小需求
    • 加油站问题:全局判断与局部调整结合
    • 无重叠区间问题:优先保留早结束元素
  2. 算法设计技巧

    • 排序在贪心算法中的重要作用
    • 双指针技术的灵活应用
    • 全局可行性判断的优化策略
  3. 问题分析能力

    • 学会了如何分析问题的核心约束和目标
    • 掌握了如何判断一个问题是否适合使用贪心算法
    • 提升了设计合适贪心策略的能力

3.3 贪心算法的适用条件

通过本次实验,我们可以总结出贪心算法适用的几个关键条件:

  1. 最优子结构:问题的最优解包含子问题的最优解。
  2. 贪心选择性质:每一步的局部最优选择能够导致全局最优解。
  3. 问题的单调性:问题具有某种单调性,使得贪心选择能够持续有效。

3.4 算法性能分析

四个问题的算法性能对比如下:

问题时间复杂度空间复杂度关键技术
柠檬水找零O(n)O(1)贪心选择
分发饼干O(n log n + m log m)O(1)排序 + 双指针
加油站O(n)O(1)全局判断 + 局部调整
无重叠区间O(n log n)O(1)排序 + 贪心选择

3.5 未来展望

贪心算法作为算法设计的重要策略,在实际应用中有着广泛的用途。未来可以进一步探索:

  1. 贪心算法与其他算法的结合:如贪心算法与动态规划、分治算法的结合应用。
  2. 更复杂问题的贪心策略设计:如霍夫曼编码、最小生成树等经典问题。
  3. 贪心算法的证明技巧:如何证明贪心策略的正确性。
  4. 近似算法:对于那些无法用贪心算法找到最优解的问题,可以考虑使用贪心算法作为近似算法。

3.6 结论

本次实验通过四个经典的编程问题,深入探讨了贪心算法的实际应用。实验结果表明,贪心算法在解决具有最优子结构和贪心选择性质的问题时,能够提供高效且简洁的解决方案。通过合理的贪心策略设计,我们能够在每一步做出局部最优选择,从而实现全局最优解。

这些算法思想和设计技巧不仅适用于本次实验中的问题,也可以推广到更广泛的实际应用场景中。通过不断的学习和实践,我们能够在算法设计和问题解决方面取得更大的进步。

http://www.dtcms.com/a/606263.html

相关文章:

  • C语言在线编译器开发 | 提高编译效率与用户体验的创新技术
  • MD5 校验脚本
  • 重生归来,我要成功 Python 高手--day35 深度学习 Pytorch
  • 马云有没有学过做网站百度收录时间
  • 企业网站的规划与建设ppt建设一个打鱼游戏网站
  • 在 Linux Ubuntu 24.04 安装 IntelliJ IDEA
  • 自适应网站建设方案建设网站 请示 报告
  • 有哪些做网站的品牌ios开发app
  • C语言编译器电脑版 | 提供高效编译体验,轻松学习与开发
  • 容器访问某个链接中断后面又正常,socket
  • 构建现代应用的9个Python GUI库
  • 做网站业务的怎么寻找客户做网站公司哪家强
  • 【第1章>第6节】基于FPGA的图像膨胀处理算法的Verilog实现
  • 网站开发对企业的关键化妆品首页设计
  • 基于图的可解释性推荐综述
  • Nginx搭建RTMP点播流媒体服务器步骤详解,Nginx+RTMP+OBS推流搭建流媒体服务器
  • 东莞建设网站官网住房和城乡网站平台系统设计公司
  • 具身智能-一文详解视觉-语言-动作(VLA)大模型(2)
  • 如何使用 Docker 打包一个简单的应用程序:简易指南
  • Hyper-V Windows 11 Pro x64 开局问题
  • 长沙外贸建站土地 水利 勘测设计 公司宣传册设计样本
  • Cursor区域限制解决方法, Cursor 提示:“Model not available“的原因
  • 自签名证书需要手动确认风险导致nginx转发无效问题
  • 重庆市建设工程安全网站广告营销公司
  • 编译型语言有哪些 | 深入了解编译型语言的工作原理和特点
  • 实践教程|如何创建一个WhatsApp AI Chatbot
  • 网站流量分成专题制作 wordpress
  • 深度学习中适合长期租用的高性价比便宜的GPU云服务器有哪些?
  • 【DaisyUI】如何覆盖 dropdown 下拉效果?
  • 个人网站可以挂广告吗网站开发项目合同书