从零开始的数据结构教程(六) 贪心算法


🍬 标题一:贪心核心思想——发糖果时的最优分配策略

贪心算法 (Greedy Algorithm) 是一种简单直观的算法策略。它在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望得到一个全局最优解。这就像你作为班主任发糖果,每次都想让眼前的孩子满意,希望这种局部最优能带来整体最优的效果。

三大适用条件

并非所有问题都适合用贪心算法解决,它通常需要满足以下三个条件:

  1. 贪心选择性质 (Greedy Choice Property):每一步的局部最优选择,都能导致最终的全局最优解。例如,在“硬币找零”问题中,如果硬币面额系统是“标准”的(如 1, 5, 10, 25 美分),那么每次都优先选择最大面额的硬币就能得到最少硬币数。
  2. 无后效性 (Optimal Substructure):当前的选择不会影响后续子问题的最优解,即后续问题的最优解不依赖于之前的选择,只依赖于当前状态。例如,在“区间调度”中,一旦你选择了最早结束的会议,后续会议的选择空间并不会因此受限,而是基于剩余时间继续做决策。
  3. 子问题可合并 (Overlapping Subproblems):虽然这是动态规划的特征之一,但在贪心算法中,这意味着局部解能够直接或以简单的方式构成全局解。例如,在霍夫曼编码中,每次合并两个最小频率的节点,最终能构建出最优的二叉树。

与 DP 的关键区别

贪心算法和动态规划都涉及将问题分解为子问题,但它们在解决方式上存在根本差异:

  • 贪心:做选择时不考虑未来,只看当前局部最优,并且一旦做出选择就不可回退。它不需要存储所有子问题的解。
  • 动态规划:会探索所有可能的局部最优解,并存储它们,通过状态转移方程来构建全局最优解。它具有“回退”机制,可以从之前的多种选择中推导出当前的最优。
# 贪心 vs 动态规划:以「硬币找零」为例
# 假设硬币面额为 [1, 5, 10, 25],找零 30
# 贪心:25 + 5 (2枚)
# DP:10 + 10 + 10 (3枚) 或 25 + 1 + 1 + 1 + 1 + 1 (6枚)# 贪心(可能无法得到最优解,例如面额 [1, 15, 25], 找零 30, 贪心会是 25+1+1+1+1+1 (6枚), 最优是 15+15 (2枚))
def coinChangeGreedy(coins, amount):coins.sort(reverse=True) # 优先使用大面额硬币count = 0for coin in coins:while amount >= coin:amount -= coincount += 1return count if amount == 0 else -1 # 如果 amount 不为 0,说明无法找零# DP(保证最优解)
def coinChangeDP(coins, amount):# dp[i] 表示凑齐金额 i 所需的最少硬币数dp = [float('inf')] * (amount + 1)dp[0] = 0 # 凑齐金额 0 需要 0 枚硬币for i in range(1, amount + 1): # 遍历所有可能的金额for coin in coins:         # 遍历所有硬币面额if i >= coin:# 如果当前金额 i 大于等于当前硬币面额 coin# 那么凑齐 i 的最少硬币数可能是:# 1. 不使用当前硬币,沿用 dp[i] (之前可能计算过)# 2. 使用当前硬币,那么剩下的金额 i - coin 需要 dp[i - coin] 枚硬币dp[i] = min(dp[i], dp[i - coin] + 1)return dp[amount] if dp[amount] != float('inf') else -1 # 如果为 inf,表示无法凑齐# print(coinChangeGreedy([1, 15, 25], 30)) # 输出 6
# print(coinChangeDP([1, 15, 25], 30))     # 输出 2

⏰ 标题二:区间调度问题——会议室的高效安排

区间调度问题是一个经典的贪心算法应用场景。

经典问题

给定若干会议的开始和结束时间 [start, end],如何安排最多的不重叠会议,使得一个会议室可以安排尽可能多的会议?

贪心策略

按结束时间排序:优先选择最早结束的会议。为什么?因为最早结束的会议会给后续会议留出最大的时间空隙,从而使得我们有机会安排更多的会议。

import java.util.Arrays;
import java.util.Comparator; // 引入 Comparator 接口class Solution {public int maxEvents(int[][] intervals) {// Step 1: 按结束时间升序排序会议// 如果结束时间相同,则按开始时间升序排序(可选,但通常不影响结果)Arrays.sort(intervals, (a, b) -> a[1] - b[1]);int count = 0; // 记录安排的会议数量int lastEndTime = Integer.MIN_VALUE; // 记录上一个已安排会议的结束时间// Step 2: 遍历排序后的会议for (int[] meeting : intervals) {int currentStart = meeting[0];int currentEnd = meeting[1];// 如果当前会议的开始时间 >= 上一个已安排会议的结束时间// 说明当前会议可以安排,且不与之前的会议重叠if (currentStart >= lastEndTime) {count++;            // 安排该会议lastEndTime = currentEnd; // 更新上一个会议的结束时间}}return count;}
}

变形题

  • 需要多间会议室时:这通常会转化为其他问题,例如“最小箭射气球”(LeetCode 452),它寻找最少数量的“箭头”能射爆所有气球(代表区间),本质上是寻找最少数量的区间来覆盖所有给定区间。
  • 带权值的区间:如果每个会议有不同的“价值”或“收益”,需要最大化总收益,那么纯粹的贪心可能不再适用,通常需要结合动态规划来解决(例如“最大收益的兼职工作”)。

🏃 标题三:跳跃游戏——从起点到终点的最少步数

跳跃游戏系列问题也是贪心算法的经典应用。

两类经典问题

  1. 能否到达终点(LeetCode 55):给定一个非负整数数组 nums,每个元素 nums[i] 代表你在位置 i 最多可以向前跳跃的长度。判断你是否能从第一个位置跳到最后一个位置。

    def canJump(nums):max_reach = 0 # 记录当前能到达的最远位置# 遍历数组,直到到达终点或无法继续前进for i in range(len(nums)):if i > max_reach: # 如果当前位置 i 已经超出了之前能到达的最远位置return False  # 说明无法到达终点# 更新能到达的最远位置:当前位置 i + 从当前位置能跳的距离 nums[i]max_reach = max(max_reach, i + nums[i])if max_reach >= len(nums) - 1: # 如果能到达或越过终点return True                # 则成功到达return True # 如果循环结束(即已经遍历完数组),说明可以到达终点
    
  2. 最少跳跃次数(LeetCode 45):给定一个非负整数数组 nums,同上,求到达最后一个位置的最少跳跃次数

    def jump(nums):if len(nums) <= 1:return 0jumps = 0          # 跳跃次数current_end = 0    # 当前跳跃能到达的最远边界farthest = 0       # 在当前跳跃范围内,下一步能到达的最远位置for i in range(len(nums) - 1): # 遍历到倒数第二个位置,因为最后一个位置不需要再跳# 更新在当前跳跃范围内能到达的最远位置farthest = max(farthest, i + nums[i])if i == current_end: # 如果当前位置 i 达到了当前跳跃的边界jumps += 1       # 进行一次跳跃current_end = farthest # 更新下一次跳跃的边界为当前能到达的最远位置# 注意:这里不需要检查 farthest 是否能到达终点,# 因为 for 循环条件保证了最终 current_end 会达到或超过 len(nums) - 1return jumps
    

关键洞察

贪心地在每一步选择能跳最远的选项(但不是盲目地跳到最远,而是规划好下一次跳跃的边界)。


🛒 标题四:高频面试题——分发糖果(双向贪心)

问题

在一个班级里,有 n 个孩子,他们的能力值用整数数组 ratings 表示。你需要给这些孩子分发糖果,并满足以下两个条件:

  1. 每个孩子至少分到 1 颗糖果。
  2. 能力值比其相邻孩子高的孩子,必须分到比相邻孩子更多的糖果。

求最少需要分发多少颗糖果。

双向遍历策略

这个问题不能简单地从左到右或从右到左遍历一次,因为它有双向的依赖关系。需要进行双向贪心

  1. 从左到右遍历:确保右边的孩子比左边高的,能获得更多糖果。
  2. 从右到左遍历:确保左边的孩子比右边高的,能获得更多糖果。
  3. 取两者最大值:最终每个孩子分到的糖果数,是两次遍历结果的最大值
def candy(ratings):n = len(ratings)# left 数组:从左到右遍历,保证 ratings[i] > ratings[i-1] 时,left[i] > left[i-1]left = [1] * n # 初始化每个孩子至少1颗糖for i in range(1, n):if ratings[i] > ratings[i - 1]:left[i] = left[i - 1] + 1# right 变量:从右到左遍历,保证 ratings[i] > ratings[i+1] 时,当前孩子获得更多糖# total:总糖果数,先加上 left 数组的最后一个元素right = 1total = left[n - 1] # 最后一个孩子的糖果数,至少是 left[n-1]for i in range(n - 2, -1, -1): # 从倒数第二个孩子开始,向左遍历if ratings[i] > ratings[i + 1]:right += 1 # 如果当前孩子比右边高,right 增加else:right = 1  # 否则,重置 right 为 1 (因为右边孩子可能比自己高或相等)# 当前孩子实际获得的糖果数,是 left[i] 和 right 两者中的最大值# 这样才能同时满足左右两个方向的条件total += max(left[i], right)return total
  • 时间复杂度 O ( n ) O(n) O(n),因为进行了两次线性遍历。
  • 空间复杂度 O ( n ) O(n) O(n),因为使用了 left 数组。可以进一步优化到 O ( 1 ) O(1) O(1) 空间,但代码会更复杂。

📊 总结表:贪心算法适用场景

问题类型典型例题贪心策略
区间问题无重叠区间(LeetCode 435)结束时间排序,优先选择最早结束的。
分配问题分发糖果(LeetCode 135)双向遍历(从左到右,从右到左)满足约束。
跳跃问题跳跃游戏 II(LeetCode 45)维护当前能到达的最远位置,并在必要时进行跳跃。
字符串构造重构字符串(LeetCode 767)优先使用剩余最多的字符,避免连续。
硬币/货币硬币找零(标准面额系统)优先使用最大面额的硬币。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/82931.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CPP中CAS std::chrono 信号量与Any类的手动实现

前言 CAS&#xff08;Compare and Swap&#xff09; 是一种用于多线程同步的原子指令。它通过比较和交换操作来确保数据的一致性和线程安全性。CAS操作涉及三个操作数&#xff1a;内存位置V、预期值E和新值U。当且仅当内存位置V的值与预期值E相等时&#xff0c;CAS才会将内存位…

Axure设计案例——科技感对比柱状图

想让数据对比展示摆脱平淡无奇&#xff0c;瞬间抓住观众的眼球吗&#xff1f;那就来看看这个Axure设计的科技感对比柱状图案例&#xff01;科技感设计风格运用独特元素打破传统对比柱状图的常规&#xff0c;营造出一种极具冲击力的视觉氛围。每一组柱状体都仿佛是科技战场上的士…

怒更一波免费声音克隆和AI配音功能

宝子们&#xff01; 最近咱软件TransDuck的免费声音克隆和AI配音功能被大家用爆啦&#xff01;感谢各位自来水疯狂安利&#xff01;&#xff01; DD这里也是收到好多用户提的宝贵建议&#xff01;所以&#xff0c;连夜肝了波更新&#xff01; 这次重点更新使用克隆音色进行A…

UDP协议原理与Java编程实战:无连接通信的奥秘

1.UDP协议核心原理 1. 无连接特性&#xff1a;快速通信的基石 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是TCP/IP协议族中无连接的轻量级传输层协议。与TCP的“三次握手”建立连接不同&#xff0c;UDP通信无需提前建立链路&#xff0c;发送…

vue-seamless-scroll 结束从头开始,加延时后滚动

今天遇到一个大屏需求&#xff1a; 1️⃣初始进入页面停留5秒&#xff0c;然后开始滚动 2️⃣最后一条数据出现在最后一行时候暂停5秒&#xff0c;然后返回1️⃣ 依次循环&#xff0c;发现vue-seamless-scroll的方法 ScrollEnd是监测最后一条数据消失在第一行才回调&#xff…

[Protobuf] 快速上手:安全高效的序列化指南

标题&#xff1a;[Protobuf] (1)快速上手 水墨不写bug 文章目录 一、什么是protobuf&#xff1f;二、protobuf的特点三、使用protobuf的过程&#xff1f;1、定义消息格式&#xff08;.proto文件&#xff09;(1)指定语法版本(2)package 声明符 2、使用protoc编译器生成代码&…

uniapp调用java接口 跨域问题

前言 之前在Windows10本地 调试一个旧项目&#xff0c;手机移动端用的是Uni-app&#xff0c;vue的版本是v2。后端是java spring-boot。运行手机移动端的首页请求后台接口老是提示错误信息。 错误信息如下&#xff1a; Access to XMLHttpRequest at http://localhost:8080/api/…

[ Qt ] | Qlabel使用

目录 属性 setTextFormat 插入图片 设置图片根据窗口大小实时变化 边框和对其方式 ​编辑 设置缩进 设置伙伴 Qlabel可以用来显式图片和文字 属性 text textFormat Qlabel独有的机制&#xff1a;buddy setTextFormat 插入图片 设置图片根据窗口大小实时变化 Qt中表…

Springboot 项目一启动就获取HttpSession

在 Spring Boot 项目中&#xff0c;HttpSession 是有状态的&#xff0c;通常只有在用户发起 HTTP 请求并建立会话后才会创建。因此&#xff0c;在项目启动时&#xff08;即应用刚启动还未处理任何请求&#xff09;是无法获取到 HttpSession 的。 方法一&#xff1a;使用 HttpS…

Step9—Ambari Web UI 初始化安装 (Ambari3.0.0)

Ambari Web UI 安装 如果还不会系统性的部署&#xff0c;或者前置内容不熟悉&#xff0c;建议从Step1 开始阅读。不通版本针对于不同操作系统可能存在差异&#xff01;这里我也整理好了 https://doc.janettr.com/install/manual/ 1. 进入 Ambari Web UI 并登录 在浏览器中访…

热门大型语言模型(LLM)应用开发框架

我们来深入探索这些强大的大型语言模型&#xff08;LLM&#xff09;应用开发框架&#xff0c;并且我会尝试用文本形式描述一些核心的流程图&#xff0c;帮助您更好地理解它们的工作机制。由于我无法直接生成图片&#xff0c;我会用文字清晰地描述流程图的各个步骤和连接。 Lang…

机器学习数据降维方法

1.数据类型 2.如何选择降维方法进行数据降维 3.线性降维&#xff1a;主成分分析&#xff08;PCA&#xff09;、线性判别分析&#xff08;LDA&#xff09; 4.非线性降维 5.基于特征选择的降维 6.基于神经网络的降维 数据降维是将高维数据转换为低维表示的过程&#xff0c;旨在保…

太阳系运行模拟程序-html动画

太阳系运行模拟程序-html动画 by AI: <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>交互式太阳系…

2025年全国青少年信息素养大赛 scratch图形化编程挑战赛 小低组初赛 内部集训模拟题解析

2025年信息素养大赛初赛scratch模拟题解析 博主推荐 所有考级比赛学习相关资料合集【推荐收藏】 scratch资料 Scratch3.0系列视频课程资料零基础学习scratch3.0【入门教学 免费】零基础学习scratch3.0【视频教程 114节 免费】 历届蓝桥杯scratch国赛真题解析历届蓝桥杯scr…

grid网格布局

使用flex布局的痛点 如果使用justify-content: space-between;让子元素两端对齐&#xff0c;自动分配中间间距&#xff0c;假设一行4个&#xff0c;如果每一行都是4的倍数那没任何问题&#xff0c;但如果最后一行是2、3个的时候就会出现下面的状况&#xff1a; /* flex布局 两…

通义灵码2.5——基于MCP实现我的12306火车票智能查询小助手

本文因排版显示问题&#xff0c;为保证阅读体验&#xff0c;请大家访问&#xff1a; 通义灵码2.5——基于MCP打造我的12306火车票智能查询小助手-CSDN博客 前沿技术应用全景图 本项目作为通义灵码2.5的标杆实践案例&#xff0c;展现了AI辅助开发在复杂业务系统中的革命性突破…

Unity Button 交互动画

在UGUI的Button组件中&#xff0c;有一个过渡动画表现的功能。可以对按钮的不同交互状态添加交互反馈动画&#xff0c;来提高玩家的交互体验。 交互状态 名称 描述 Normal 正常情况 Highlighted 高亮显示&#xff0c;例如鼠标触碰到按钮点击范围 Pressed 按钮被按下的时…

钉钉热点实时推送助理-思路篇

以下是针对热点实时推送助理的功能描述&#xff0c;结合机器学习技术栈与用户场景的通俗化解释&#xff1a; 快速体验的话直接用钉钉扫描下方二维码体验 1. 核心功能 &#xff08;1&#xff09;热点抓取引擎 类比&#xff1a;像蜘蛛爬取全网信息&#xff08;网络爬虫信息抽取…

remote: error: hook declined to update refs/heads.....

gitee拉取分支&#xff0c;修改上传出现的问题&#xff0c;折腾了好久&#xff0c;浅浅记录. 1. 首次克隆仓库 # 克隆仓库&#xff08;使用 HTTPS 或 SSH&#xff09; git clone ------------ cd xxx-project2. 配置正确的用户信息&#xff08;关键步骤&#xff01;&#xff…

使用Vue + Element Plus实现可多行编辑的分页表格

需求背景&#xff1a; 在现代前端开发中&#xff0c;表格作为数据展示和交互的重要组件&#xff0c;在各类管理系统、数据平台中有着广泛的应用。随着用户对数据操作便捷性要求的不断提高&#xff0c;具备灵活编辑功能的表格成为了开发中的常见需求。特别是在需求处理大…