【LeetCode刷题日记】贪心算法理论与实战:455.分发饼干最优解

🔥个人主页:代码不加冰(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:LeetCode刷题日记 ,苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!


前言:

大家好,我是代码不加冰,经过一段时间的学习,我们已经学完了回溯算法相关的章节,可能刚开始的时候学的很慢,有时候一个多小时才能写完,不过这都是很正常的,没必要去否定自己,多复盘,多写几遍就可以了,我们接下来进入到贪心算法的章节,我们先介绍一下贪心算法,然后通过一个简单的题目来正式的进入到贪心算法 的专题。

摘要:

本文系统介绍了贪心算法的核心思想与典型应用场景。贪心算法通过局部最优选择 逐步构建全局解,其两大核心为贪心选择性质最优子结构。与动态规划相比,贪心算法效率更高(如O(nlogn)),但需严格证明策略的正确性。经典应用包括霍夫曼编码、最小生成树算法等。文章以**分发饼干(LeetCode 455)**为例,演示贪心策略:排序后优先用大饼干满足大胃口,通过单次遍历实现O(nlogn)时间复杂度。最后强调贪心法的局限性(如某些找零问题需动态规划)及验证方法(反例法、数学归纳)。

什么是贪心算法:

贪心算法(Greedy Algorithm)是算法中一种非常直观且高效的方法。它的核心思想是:在对问题求解时,总是做出在当前看来最好的选择。 换句话说,贪心算法不从整体最优上加以考虑,它所做出的仅仅是局部最优解 。虽然贪心算法不能对所有问题都得到整体最优解,但对许多特定问题,局部最优的选择最终能够导致整体最优。

核心理论基础

我们要掌握贪心算法,必须理解它的两个核心特质:

1. 贪心选择性质 (Greedy Choice Property)

一个全局最优解可以通过一系列局部最优(贪心)的选择来达到。也就是说,在考虑当前步骤时,我们直接做出当前利益最大的选择,而不必考虑子问题的后续结果这就是"走一步看一步,每步挑最好"。

2. 最优子结构 (Optimal Substructure)

如果一个问题的最优解包含其子问题的最优解,那么这个问题就具有最优子结构。贪心算法和动态规划(DP)都要求问题具备这个性质。

贪心算法的四大步骤

在实际做题或解决问题时,通常遵循以下步骤:

  1. 将问题分解:把大问题分解为若干个子问题(步骤)。

  2. 找出贪心策略 :确定每一步的局部最优解是什么(这一步最难,需要逻辑推理或直觉)。

  3. 求解局部最优:每一步都采取这个贪心策略,得到阶段性结果。

  4. 堆叠成全局最优 :将所有的局部最优解合并,转化为整个问题的解。

贪心法 vs 动态规划

很多人容易混淆贪心和动态规划,它们的核心区别在于"是否回头":

特性 贪心算法 (Greedy) 动态规划 (DP)
选择方式 每一步做出当前最优选择,不能回溯(不后悔)。 综合所有子问题的解做出选择,有状态转移
子问题依赖 当前的选择不依赖于未解决的子问题。 当前的选择依赖于子问题的解(需要查表)。
效率 极高(通常是 O(n \log n)$或 O(n)。 较高(通常是 O(n^2) 或 O(n \cdot W) )。
正确性 需要严格证明(不是所有问题都能用)。 只要状态转移方程正确,必然得到最优解。

经典应用场景

贪心算法在计算机科学中有非常多著名的经典应用:

  • 霍夫曼编码(Huffman Coding):每次合并频次最低的两棵树,实现最优压缩。

  • 普里姆算法(Prim)& 克鲁斯卡尔算法(Kruskal):求解图的最小生成树(MST),每次都选权值最小的边。

  • 迪杰斯特拉算法(Dijkstra):求解单源最短路径,每次选择距离源点最近的未访问节点。

  • 区间调度问题:给定多个活动和它们的开始/结束时间,如何安排能参加最多的活动(策略:每次选结束时间最早的活动)。

贪心算法的致命弱点

贪心算法最大的难点不在于代码编写,而在于"你怎么知道你的贪心策略是对的"。很多时候,主观直觉上的"最优"往往是错的。

举个栗子:找零钱问题

  • 假设币值有 [25, 10, 1] 分。如果要找 41 分,贪心策略(每次选最大的):25 + 10 + 1 + 1 + 1 + 1 + 1(7张),但实际最优解是 25 + 8(不存在8),或者如果有 [25, 20, 1] 找 40 分,贪心是 25 + 1*15(16张),而实际最优是 20 + 20(2张)。此时贪心失效,必须用动态规划。

常见的证明方法:

  1. 数学归纳法:证明第一步贪心是对的,并证明如果前 k 步是对的,第 k+1步也是对的。

  2. 反证法(替换法):假设存在一个比贪心解更优的"神仙解",然后证明可以用贪心选择逐步替换掉这个"神仙解"里的选择,而结果不会变差。

刷题小技巧 :在算法面试或竞赛中,如果难以用数学严谨证明,可以通过举反例。如果试了几个极端的边界情况都找不到反例,那么这个贪心策略大概率是可行的。


具体题目:455.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。

示例 1:

复制代码
输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。
所以你应该输出 1。

示例 2:

复制代码
输入: g = [1,2], s = [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出 2。

提示:

  • 1 <= g.length <= 3 * 104
  • 0 <= s.length <= 3 * 104
  • 1 <= g[i], s[j] <= 231 - 1

题目分析:

其实这道题就是很简单的逻辑,感觉像是小学还是幼儿园的逻辑

目的就是大的饼干给胃口大的,小的饼干给胃口小的,但是要有限制,大的饼干要能满足胃口大的,否则也是不合规的。

根据步骤来:

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。

然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量

不过要注意代码细节,很多人可能上来想着就是用两个for循环来分别遍历,其实是多余的,我们只需要用索引来替代一下,毕竟要比较的是值。

题目答案:

复制代码
class Solution {
    // 思路2:优先考虑胃口,先喂饱大胃口
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int count = 0;
        int start = s.length - 1;
        // 遍历胃口
        for (int index = g.length - 1; index >= 0; index--) {
            if(start >= 0 && g[index] <= s[start]) {
                start--;
                count++;
            }
        }
        return count;
    }
}
相关推荐
_abab1 小时前
Java面试宝典:从基础到架构2
java·面试·架构
xin_yao_xin1 小时前
Conda 环境的 CUDA PATH 配置指南
开发语言·python·conda·cuda
he___H1 小时前
基于LCEL的联想
开发语言·python·langchain
万亿少女的梦1681 小时前
基于Spring Boot与Vue的繁星技术论坛系统设计与实现
java·spring boot·mysql·vue·系统设计
阿pin1 小时前
Android随笔-神经系统Handler
android·java·开发语言·handler
布朗克1682 小时前
Go 入门到精通-09-复合类型之Map
开发语言·后端·golang·map
zh_xuan2 小时前
c++ Expected用法
开发语言·c++·expected
ch.ju2 小时前
Java程序设计(第3版)第四章——final修饰的方法和类
java·开发语言