news 2026/8/29 17:46:03

贪心算法核心原理与实战:从霍夫曼编码到最短路径

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
贪心算法核心原理与实战:从霍夫曼编码到最短路径

1. 项目概述:贪心法——一种“短视”却高效的决策艺术

在算法设计与分析的浩瀚世界里,我们常常面临一个核心矛盾:如何在海量的可能性中,快速找到一个“足够好”的解决方案?当问题规模大到穷举所有可能性的计算成本无法承受时,我们就需要一些聪明的策略来指导我们的决策。贪心法,正是这样一位“聪明的决策者”。它不像动态规划那样瞻前顾后,计算所有子问题的解;也不像回溯法那样反复试探,走不通再回头。贪心法的哲学很简单:在每一步都做出当前看来最优的选择,并且永不回头。这种“短视”的策略听起来有些冒险,但在许多特定类型的问题上,它却能以惊人的效率找到全局最优解。

贪心法解决的核心问题是那些具有“最优子结构”和“贪心选择性质”的组合优化问题。简单来说,“最优子结构”意味着整个问题的最优解包含了其子问题的最优解;而“贪心选择性质”则保证了每一步的局部最优选择,最终能导向全局最优解。这就像我们规划一次长途旅行,如果从起点到终点的最短路径,必然由从起点到中间各点的最短路径组成,并且我们每次都选择距离下一个城市最近的路,那么最终走出来的就是最短路径。贪心法就是基于这种信念来运作的。

对于学习算法设计与分析的同学,或者任何需要解决资源调度、任务安排、路径规划等实际问题的开发者而言,理解贪心法至关重要。它不仅是算法工具箱里的一把利器,更是理解“问题结构”与“算法策略”之间深刻联系的绝佳范例。本文将带你深入贪心法的核心,从原理到证明,从经典案例到实战应用,并分享那些在教科书之外、只有亲手实现和调试才能获得的宝贵经验。

2. 贪心法的核心思想与适用条件解析

贪心法之所以有效,并非因为它总能找到最优解,而是因为它巧妙地利用了问题的特殊结构。理解其思想内核和严格的适用条件,是正确运用贪心法的前提。

2.1 “短视”决策背后的数学原理

贪心算法的基本框架可以概括为:从一个初始解(通常是空集或问题的起点)开始,通过一系列步骤构造最终解。在每一步,算法都会根据一个预先定义的“贪心准则”,从当前所有可行的选择中,挑选出看起来最优的那一个,并将其加入到当前的部分解中。一旦做出选择,就再也不去重新考虑或撤销它。

这个过程听起来很像我们日常的决策,比如在超市购物时,我们可能每次都拿最想吃的零食,直到预算花完。但算法的严谨性在于,我们需要证明这种“短视”的决策链最终导向的是全局最优,而不是一个糟糕的结果。这引出了贪心法有效的两个基石:

  1. 最优子结构:一个问题的最优解包含其子问题的最优解。这是动态规划和贪心法共有的性质。例如,在“找零钱”问题中(用最少数量的硬币凑出某个金额),如果我们已经知道凑出金额X的最优硬币组合,那么从这个组合中拿走一枚面值为c的硬币,剩下的硬币就应该是凑出金额X-c的最优组合。这个性质保证了我们可以通过组合子问题的最优解来构造原问题的最优解。

  2. 贪心选择性质:我们可以通过做出局部最优(贪心)的选择来构造一个全局最优解。这是贪心法区别于动态规划的关键。动态规划在每一步选择时,需要考虑所有可能的选择,并从中选出最优的;而贪心法则“盲目”地相信,当前这一步的最优选择,一定会被包含在某个全局最优解中。因此,贪心算法在做出选择后,只会剩下一个待解决的子问题,而不是多个。

注意:证明一个问题的贪心选择性质,通常比证明最优子结构更困难,也更具技巧性。常用的方法有“交换论证法”和“领先论证法”。简单来说,就是假设存在一个最优解,然后证明我们可以通过将贪心算法的选择“交换”进这个最优解,而不破坏其最优性,从而说明贪心选择是可行的。

2.2 何时能用贪心法?——问题特征识别指南

并非所有问题都适合用贪心法。误用贪心法会导致得到次优解甚至错误解。以下是判断一个问题是否可能适用贪心法的“嗅觉测试”清单:

  • 问题具有最优化目标:通常是求最大值(如最大利润、最长路径)或最小值(如最小成本、最短时间)。
  • 问题可以分解为一系列选择:整个解决方案是由一系列决策步骤构成的。
  • 存在明显的“贪心准则”:你能直观地想出一个在每一步“看起来最好”的选择标准(如:每次选单位价值最高的物品、每次选结束时间最早的任务)。
  • 尝试用反例验证:这是最有效的自查方法。快速在脑海中或纸上构造几个小规模的、非典型的测试用例,看看你的贪心准则是否会导向明显错误的结果。例如,在经典的“部分背包问题”中,贪心准则“每次选价值最高的物品”就会失败,因为一个很重但价值略高的物品可能会挤占多个轻且价值不错的物品的空间。

一个经典的正面例子是“活动选择问题”:给定一系列活动的开始和结束时间,如何选择尽可能多的互不冲突的活动?贪心准则是“每次选择结束时间最早的活动”。我们可以证明,这个选择一定会是某个最优解的一部分。而反面例子则是“0-1背包问题”,同样按“单位价值最高”贪心,就无法保证全局最优,因为物品不可分割,贪心选择可能过早占用了背包空间。

实操心得:在实际工程或面试中,当你怀疑一个问题可能用贪心法时,先别急着写代码。花几分钟时间,在白板上画几个精心设计的、边界情况丰富的例子,手动模拟你的贪心策略。这个过程不仅能帮你验证想法的正确性,往往也是面试官考察你思维严谨性的关键环节。

3. 经典贪心算法案例深度剖析

理论需要案例来具象化。下面我们深入剖析几个教科书级的贪心算法问题,不仅看怎么做,更要理解为什么这样做是对的。

3.1 霍夫曼编码——数据压缩的基石

霍夫曼编码是贪心法在数据压缩领域最辉煌的应用之一。它的目标是为一组字符(如文件中的字母)设计一套二进制编码,使得编码后的总长度最短(即期望码长最小)。这里的“贪心准则”是:每次合并频率最低的两棵树

算法步骤详解:

  1. 将每个字符看作一棵只有一个节点的树,节点的权重即为该字符的频率。
  2. 建立一个最小优先队列(通常用最小堆实现),将所有树按权重插入。
  3. 当队列中不止一棵树时,循环执行: a. 从队列中弹出权重最小的两棵树T1T2。 b. 创建一棵新树T,以T1T2作为左右子树。T的权重为T1T2权重之和。 c. 将新树T插入优先队列。
  4. 最后队列中剩下的那棵树就是霍夫曼树。从根到叶子的路径(左分支为0,右分支为1)即为每个字符的编码。

为什么贪心是有效的?关键在于,频率最低的字符,其编码应该最长。贪心算法每次合并最小的两棵树,保证了频率最低的节点在树中最深的位置。这可以通过反证法证明:如果存在一个最优编码树,其中频率最低的两个字符不是深度最深的兄弟节点,那么我们可以通过交换将它们调整到最深的位置,从而得到更优(或至少不差)的编码,这与“最优”矛盾。因此,每一步合并最小的两棵树,这个局部最优选择,最终构造出了全局最优的编码树。

实现注意事项:

  • 优先队列的选择至关重要,直接关系到算法效率。使用二叉堆可以实现 O(n log n) 的时间复杂度。
  • 编码和解码需要基于同一棵霍夫曼树。在实际压缩文件中,树的结构(或频率表)需要作为头部信息存储。
  • 对于动态数据流(字符频率未知或变化),需要使用自适应霍夫曼编码,其核心思想依然是贪心调整。

3.2 最小生成树:Prim算法与Kruskal算法

在连通加权无向图中寻找一棵包含所有顶点、且边权之和最小的树(最小生成树,MST),是网络设计、电路布线等领域的核心问题。Prim和Kruskal算法从不同角度诠释了贪心思想。

Prim算法(“加点法”)贪心准则:每次从未加入生成树的顶点中,选择一个与当前树距离最近的顶点加入

  1. 从任意顶点s开始,将其加入集合A(代表已在MST中的顶点)。
  2. 维护一个最小优先队列,存储所有连接AV-A(未加入顶点)的边,键值为边权。
  3. A未包含所有顶点时: a. 从队列中取出权值最小的边(u, v),其中uA中,v不在。 b. 将边(u, v)加入MST,将顶点v加入集合A。 c. 将与v相连、且另一端不在A中的边加入或更新优先队列。
  4. 算法结束,得到的就是最小生成树。

Kruskal算法(“加边法”)贪心准则:每次从未选择的边中,选择一条权值最小且不会与已选边构成环的边

  1. 将所有边按权值从小到大排序。
  2. 初始化一个并查集,每个顶点自成一个集合。
  3. 按顺序遍历排序后的边: a. 检查当前边(u, v)的两个端点是否属于同一个集合(用并查集Find操作)。 b. 如果不属于,则选择这条边加入MST,并将uv所在的集合合并(用并查集Union操作)。 c. 如果属于,则跳过(选择它会形成环)。
  4. 当选择的边数达到|V|-1时,算法结束。

两种算法的对比与选型:

特性Prim算法Kruskal算法
核心思想从一点开始,逐步扩张生成树按边权排序,逐步合并森林
数据结构优先队列(最小堆)边排序 + 并查集
时间复杂度O(|E| log |V|) (二叉堆)O(|E| log |E|) (排序占主导)
适用场景稠密图(|E| 接近 |V|^2)稀疏图(|E| 远小于 |V|^2)
贪心证明关键切割性质:对于图的任意一个切割,横跨切割的最小权边必然属于某棵MST。Prim每一步都在当前切割中选最小边。循环性质:对于图的任意一个环,环上权值最大的边一定不属于任何MST。Kruskal从不选会成环的边,且每次都选当前最小的安全边。

实操心得:在面试或竞赛中,如果图是稠密的,通常使用Prim(尤其是用邻接矩阵实现);如果是稀疏的,Kruskal的代码往往更简洁易懂。并查集的实现效率对Kruskal算法影响巨大,务必掌握路径压缩和按秩合并这两种优化。

3.3 单源最短路径:Dijkstra算法

Dijkstra算法用于求解带非负权重的有向或无向图中,从单个源点到所有其他顶点的最短路径。它的贪心准则与Prim算法神似:每次从未确定最短路径的顶点中,选择一个当前距离源点最近的顶点,并确认其最短路径

算法步骤与正确性直观理解:

  1. 初始化:设置源点s的距离为0,其他顶点距离为无穷大。所有顶点标记为“未确定”。
  2. 循环执行,直到所有顶点“确定”: a. 从“未确定”顶点中,选出距离s最小的顶点u。 b. 将u标记为“确定”(贪心选择:此时dist[u]就是su的最短距离)。 c. 对u的每个邻居v,进行“松弛”操作:如果dist[u] + w(u, v) < dist[v],则更新dist[v]为这个更小的值。
  3. 算法结束,dist数组存储了从源点s到所有顶点的最短距离。

为什么贪心选择u是正确的?因为所有权重非负。假设存在一条从su的更短路径,那么这条路径上第一个“未确定”的顶点x,其距离dist[x]必然小于dist[u](因为边权非负,路径长度单调不减)。但这与“u是当前未确定顶点中距离最小的”矛盾。因此,dist[u]必然已经是最短距离。

与Prim算法的异同:两者结构非常相似,都维护一个优先队列,都每次取出最小元素。但核心操作不同:

  • Prim:更新的是“连接到当前树的边的权重”,关注的是顶点到整个树集合的距离。
  • Dijkstra:更新的是“通过当前顶点中转的路径长度”,关注的是顶点到源点的距离。
  • 数据结构:Prim的优先队列键值是顶点到树的最短边权;Dijkstra的键值是顶点到源点的当前最短距离估计值。

警告:Dijkstra算法不能处理含有负权边的图。因为负权边会破坏“一旦顶点被标记为确定,其最短距离就不再改变”这一前提。在负权图中,可能需要使用Bellman-Ford或SPFA算法。

4. 贪心算法的设计范式与实现技巧

掌握了经典案例后,我们可以抽象出设计贪心算法的一般步骤和实现中的关键技巧。

4.1 从问题到贪心策略的四步设计法

当你面对一个新问题时,可以遵循以下步骤来尝试设计贪心算法:

  1. 问题建模:将实际问题抽象为组合优化问题的形式。明确什么是“解”,什么是“可行解”,什么是“最优解”(目标函数)。例如,活动选择问题中,“解”是一个活动子集,“可行解”是子集中活动互不冲突,“最优解”是可行解中活动数量最多的那个。

  2. 寻找贪心选择准则:这是最具创造性的部分。思考在构造解的每一步,根据什么标准从候选集中做出选择。常见的准则有:

    • 最早结束时间(活动选择)
    • 最小权重/最大价值密度(部分背包)
    • 最短处理时间(任务调度以最小化平均完成时间)
    • 最大覆盖/影响范围(区间覆盖、广播问题) 多尝试几种不同的准则,并用小例子测试。
  3. 证明贪心选择性质(关键步骤):你必须(至少在逻辑上)证明,每一步的贪心选择都安全地包含在某个最优解中。如果无法严格证明,就需要高度警惕。常用的证明方法:

    • 交换论证:假设存在一个最优解O,你的贪心算法第一步选择了G。证明可以将O中的某个元素替换为G,得到的新解O‘仍然最优且包含G。
    • 领先论证:证明贪心算法在任何一步所保持的“部分解”,都不比任何最优解在同一阶段的“部分解”差。
    • 归纳法:证明第一步贪心选择正确,并且剩下的子问题与原问题具有相同性质,可以递归应用贪心策略。
  4. 构建最优子结构:证明在做出贪心选择后,剩下的问题是一个与原问题结构相同但规模更小的子问题。这样,将贪心选择与子问题的最优解合并,就能得到原问题的最优解。

4.2 数据结构的选择与优化

贪心算法的效率很大程度上依赖于其使用的数据结构。选择不当,一个O(n log n)的算法可能退化为O(n²)。

  • 优先队列(堆):这是贪心算法最亲密的伙伴。无论是Dijkstra、Prim,还是需要频繁取出最小/最大元素的场景,二叉堆都能提供O(log n)的插入和取出操作。在Python中,heapq模块提供了最小堆的实现;在C++中,std::priority_queue;在Java中,PriorityQueue

    • 技巧:有时我们只需要取出最小值,但更新队列中元素的值。标准的堆不支持高效的decrease-key操作。一个实用的变通方法是:即使值更新了,我们也直接将新值(连同节点标识)再次插入堆中。当从堆中取出元素时,检查其值是否已经过时(与当前记录的最新值不符),如果过时就丢弃它,继续取下一个。这增加了堆的大小,但避免了实现复杂的数据结构,在很多时候是可接受的。
  • 排序:像Kruskal算法、区间调度等问题,第一步往往是对所有候选元素(边、活动)进行排序。排序的复杂度O(n log n)通常是整个算法的瓶颈,但也决定了后续步骤的简单性。

    • 技巧:排序时,除了主关键字(如结束时间),往往还需要考虑次关键字(如开始时间)来打破平局,确保算法在边界情况下的确定性。
  • 并查集:Kruskal算法的灵魂。高效的并查集(带路径压缩和按秩合并)能让FindUnion操作接近常数时间。

    • 实现要点
      class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n # 按秩合并 def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] def union(self, x, y): rootX, rootY = self.find(x), self.find(y) if rootX == rootY: return False # 按秩合并 if self.rank[rootX] < self.rank[rootY]: self.parent[rootX] = rootY elif self.rank[rootX] > self.rank[rootY]: self.parent[rootY] = rootX else: self.parent[rootY] = rootX self.rank[rootX] += 1 return True

实操心得:在竞赛或面试中实现贪心算法,先想清楚步骤,再选择数据结构。不要一上来就写代码。用注释把算法的每一步逻辑写清楚,然后再填充具体的数据结构操作。这能极大减少错误。对于需要优先队列的场景,如果语言的标准库不支持decrease-key,提前想好上面提到的“懒惰删除”策略。

5. 贪心法的局限性、常见陷阱与问题排查

贪心法并非银弹,它的高效性建立在问题严格的数学性质之上。忽略这些性质,就会掉入陷阱。

5.1 典型失败案例与原因分析

  1. 0-1背包问题

    • 问题:有n件物品和一个容量为C的背包。物品i有重量w_i和价值v_i。如何选择装入背包的物品,使得总价值最大?物品不可分割。
    • 错误贪心:按单位价值v_i / w_i从高到低贪心选择。
    • 反例:背包容量C=10。物品1: (w=6, v=8, 单位价值1.33),物品2: (w=5, v=5, 单位价值1.0),物品3: (w=5, v=5, 单位价值1.0)。贪心会选择物品1,然后背包剩余容量4,无法再装任何物品,总价值8。而最优解是选择物品2和3,总价值10。
    • 失败原因:物品不可分割,贪心选择可能过早占用背包空间,阻止了后续更优组合的放入。这破坏了“贪心选择性质”。
  2. 图着色问题(求最小色数)

    • 问题:给一个无向图的顶点着色,要求有边相连的顶点颜色不同,求最少需要的颜色数。
    • 错误贪心:按顺序遍历顶点,给每个顶点分配其邻接顶点中未使用的最小颜色编号(顺序贪心着色)。
    • 反例:存在特定的图结构和顶点顺序,使得顺序贪心着色使用的颜色远多于最优解。例如,一个“星型”图加一条边,特定的顺序可能导致中心节点最后着色,用了很多颜色。
    • 失败原因:这是一个NP难问题。贪心策略无法考虑到全局的顶点关联关系,局部最优无法保证全局最优。
  3. 旅行商问题(TSP)的最近邻贪心

    • 问题:访问所有城市一次并回到起点,求最短回路。
    • 错误贪心:从起点开始,每次前往最近的未访问城市。
    • 反例:很容易构造出例子,使得最近邻贪心得到的回路比最优解长很多。因为它可能早期做出一些“短视”的连接,导致后期不得不进行非常长的连接来完成回路。
    • 失败原因:TSP不满足贪心选择性质。当前最近的城市,可能并不是全局最优回路中的下一个城市。

5.2 贪心算法调试与验证指南

当你实现了一个贪心算法,如何确保它是正确的?特别是当严格的数学证明比较困难时。

  1. 暴力法对小规模数据:这是最可靠的方法。对于n较小(比如n<=20)的问题,编写一个暴力枚举所有可能解的算法(回溯、位运算枚举),与你的贪心算法结果进行对比。随机生成大量测试用例进行比对。如果在小规模数据上结果一致,算法正确的概率就大大增加。

  2. 对拍:在竞赛中,如果你怀疑贪心策略,但又有另一个能保证正确但较慢的算法(如动态规划),可以编写一个随机数据生成器,让两个程序跑同样的输入,对比输出。这是发现反例的利器。

  3. 边界测试

    • 空输入:你的算法能处理吗?
    • 极值输入:所有元素都相同、完全逆序、已经有序的情况。
    • 权重/时间相等:当贪心准则涉及比较,且出现相等情况时,你的算法行为是否确定?结果是否依然最优?(有时需要定义次要准则来打破平局)。
  4. 逻辑复查与证明尝试:即使不能完成严格证明,也要尝试用自然语言说服自己。问自己:如果我不做这个贪心选择,而选择另一个,会不会让结果更好?为什么不会?尝试构造一个让贪心失败的反例,如果构造不出来,信心就会增强。

常见问题排查表:

问题现象可能原因排查方向
结果不是最优解1. 问题本身不具备贪心选择性质。
2. 贪心准则设计错误。
1. 尝试用暴力法寻找反例。
2. 重新审视问题模型,尝试其他贪心准则(如按结束时间、按开始时间、按时长等)。
算法在某些测试点超时1. 数据结构效率低(如用线性查找代替优先队列)。
2. 存在冗余计算或循环。
1. 分析时间复杂度,检查最耗时的操作(通常是排序或优先队列操作)。
2. 使用性能分析工具定位热点代码。
算法结果不稳定(同一输入多次运行结果不同)1. 使用了不稳定的排序,且相等元素的顺序影响结果。
2. 涉及随机数或未初始化的变量。
1. 确保排序是稳定的,或明确定义相等时的比较规则。
2. 检查所有变量是否被正确初始化,消除随机性。
程序运行时错误(如索引越界)1. 对空输入或边界情况处理不当。
2. 循环条件或指针操作有误。
1. 添加输入有效性检查。
2. 在循环开始和结束时打印关键变量值进行调试。

6. 贪心法在实际工程与面试中的应用拓展

理解了经典算法和设计模式后,我们来看看贪心思想如何解决更贴近实际的问题,以及在技术面试中如何被考察。

6.1 现实场景中的贪心策略

  1. 缓存淘汰策略(LRU - Least Recently Used)

    • 问题:缓存空间有限,当需要载入新数据而缓存已满时,需要淘汰一个旧数据。
    • 贪心准则:淘汰最久未使用的数据。这个策略基于一个合理的局部假设:最近被使用的数据,在不久的将来更有可能再次被使用。虽然这不是理论上最优的(最优需要预知未来),但在实际中效果非常好,实现简单高效。
  2. 任务调度与资源分配

    • 问题:有多个任务和有限的机器资源,如何安排以最小化完成所有任务的总时间(makespan)或平均完成时间?
    • 贪心策略(最小化平均完成时间):按处理时间从短到长排序并依次执行(SPT规则)。这可以证明是最优的。因为让短任务先完成,可以减少更多任务的等待时间。
    • 贪心策略(负载均衡):有一批任务和m台相同的机器,每次将当前任务分配给当前负载最轻的机器。这是一个在线贪心算法,虽然不一定全局最优,但简单且在实际分布式系统中广泛使用。
  3. 数据流中的频率统计(Misra-Gries算法)

    • 问题:海量数据流一次流过,内存有限,如何找出出现频率超过一定阈值(如1/k)的所有元素?
    • 贪心思想:维护一个最多包含k-1个(元素,计数)对的集合。当新元素到来时,如果它在集合中,计数加1;如果不在且集合未满,则加入;如果不在且集合已满,则将集合中所有元素的计数减1,并移除计数为0的元素。这是一个在有限空间内近似找出频繁项的经典贪心算法。

6.2 技术面试中的贪心问题破解思路

面试中的贪心问题往往不会直接告诉你“用贪心法”。你需要自己识别并设计策略。以下是一个通用的解题框架:

  1. 澄清问题与约束:首先与面试官确认问题的所有细节。输入是什么?输出是什么?优化目标是什么?(最大化还是最小化?)有什么特殊约束吗?(如非负权重、不可分割等)

  2. 提出暴力解法并分析:先想一个最直观的解法(通常是回溯或枚举),并指出其指数级的时间复杂度。这展示了你的分析能力,并自然引出对更优解的需求。

  3. 寻找贪心线索:问自己:

    • 这个问题能分解成一系列选择吗?
    • 有没有一个显而易见的、每一步“最好”的选择标准?(如:最早结束、最小代价、最大收益密度)
    • 尝试举一个小例子,手动模拟这个贪心准则,看它是否可行。
  4. 尝试证明或证伪:向面试官阐述你认为的贪心准则,并尝试进行推理证明(即使不严谨)。常用的说法是:“我认为可以按X排序,然后每次选择Y。因为如果我不选当前这个Y,而选了另一个,那么...(分析交换或替换后的结果不会更好)”。如果发现反例,就调整准则。

  5. 设计算法与数据结构:确定贪心准则后,设计算法步骤。思考需要什么数据结构来高效支持你的操作(排序?优先队列?)。讨论时间空间复杂度。

  6. 编写代码:用清晰的代码实现。注意变量命名和边界条件处理。

  7. 测试与讨论:用面试官给的例子或自己设计的边缘案例(空、重复、极值)来测试代码。讨论算法的局限性(比如,如果约束改变,是否还适用)。

示例:经典的“跳跃游戏”问题问题:给定一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。 贪心策略:不关注具体跳几步,而是关注最远可到达范围。我们维护一个变量farthest,表示当前能跳到的最远下标。遍历数组,如果当前位置ifarthest范围内,就更新farthest = max(farthest, i + nums[i])。如果farthest已经能覆盖最后一个下标,则返回成功;如果遍历到某个i时,i > farthest,说明卡住了,返回失败。 这个策略的贪心在于:在可到达的范围内,选择那个能让我跳得更远的点作为起跳点之一。我们并不需要知道具体从哪个点起跳,只需要知道最远能到哪。这比回溯或动态规划(O(n²))高效得多(O(n))。

贪心法是一种思想,其价值在于将复杂问题简化为一系列局部决策。掌握它,不仅能让你写出高效的代码,更能锻炼你分析问题结构、寻找问题关键特征的能力。在实际工作中,很多启发式算法和近似算法都蕴含着贪心的思想。下次当你遇到一个需要做出一系列选择的问题时,不妨先问问自己:如果我每次都选眼前最好的,结果会怎样?也许,答案就在其中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/29 17:42:59

猿辅导2020校招后端笔试解析:合并区间、拓扑排序与二分答案实战

2020年秋招季&#xff0c;我投了猿辅导的后端研发岗。笔试通知来得比较突然&#xff0c;当天下午还在实验室调模型&#xff0c;看到邮件后草草翻了翻题就上了考场。这套笔试&#xff08;二&#xff09;做完之后我印象很深&#xff0c;不是因为难&#xff0c;而是因为它的出题风…

作者头像 李华
网站建设 2026/8/29 17:41:26

基于SpringBoot的文玩商城系统设计与实现(程序+文档+讲解)

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/8/29 17:36:51

基于SpringBoot的财务报销审批管理系统(源码+lw+部署文档+讲解等)

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/8/29 17:33:48

OpenAI伦理主管离职:AI治理的困境与工程实践

关于“OpenAI 伦理主管 Chlo Bakalar 为什么离开”&#xff0c;与其去猜一个内部人事八卦&#xff0c;不如把它当作一个 AI 治理的观察样本。这件事真正值得关注的点不是“谁走了”&#xff0c;而是&#xff1a;一个专门负责 AI 伦理的高管&#xff0c;为什么会在行业最关键的治…

作者头像 李华
网站建设 2026/8/29 17:31:00

STM32+K210物联网害虫识别植物养护系统设计与实现

简介&#xff1a;嵌入式系统与边缘AI结合正成为物联网智能设备的重要技术方向。其核心原理是通过多种传感器采集环境数据&#xff0c;结合AI视觉算法进行目标识别&#xff0c;再由主控芯片完成决策控制&#xff0c;最后借助无线通信将数据上云&#xff0c;实现远程管理。这种架…

作者头像 李华
网站建设 2026/8/29 17:30:54

DeepSeek API涨价背后:从token计费到本地部署的应对策略

1. 背景&#xff1a;DeepSeek 为什么敢涨价&#xff0c;开发者在讨论什么&#xff1f;1.1 事件背景与本文范围最近&#xff0c;DeepSeek 相关话题在开发者社区的热度很高。先是 API 价格体系的调整引发大量讨论&#xff0c;接着是本地部署、Codex 接入、VSCode 接入、企业微信接…

作者头像 李华