本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个资源包提供多个经典贪心算法的Python实现,直接运行就能看到结果。jump_game.py判断数组能否跳到末尾,gas_station.py验证环形加油站路径是否可行,prim.py用Prim算法构造最小生成树,还附带对应示意图gas.jpg、jump.jpg、prim.jpg方便理解逻辑。另外包含merge_number.py(合并数字使总代价最小)和monotone_increasing_digits.py(构造不大于给定数的最大单调递增数字),都是典型贪心策略应用。所有代码注释清晰,变量命名直观,适合边学边练。配套PPT《第8课-贪心.ppt》讲清楚贪心适用场景、常见模式和容易踩的坑,比如什么时候不能用贪心而该选动态规划。整个包结构干净,没有多余依赖,下载解压后用Python3直接执行各.py文件即可验证输出,适合算法入门、面试突击或课堂演示。

1. 为什么贪心算法值得你花时间“亲手敲一遍”?

我带过不少准备算法面试的同学,也给高校讲过数据结构课。最常听到的一句话是:“贪心算法听起来简单,一做题就卡在‘为什么这一步选它’上。”不是概念没懂,而是缺一次从“纸上谈兵”到“键盘敲出结果”的完整闭环——而这恰恰是这个资源包想补上的关键一环。

贪心算法不像动态规划有状态转移方程可套,也不像回溯有清晰的递归树可画;它的力量藏在“局部最优选择能导向全局最优解”这一微妙前提里。而这个前提是否成立,不能靠直觉,必须靠构造性证明或反例验证。比如跳跃游戏中,为什么每次跳到当前能到达的最远位置就是对的?加油站问题里,为什么找到第一个“油量盈余累计非负”的起点就能保证走完一圈?这些答案,光看PPT里的结论是记不住的,只有当你亲手运行jump_game.py,把[2,3,1,1,4][3,2,1,0,4]两组输入分别跑出来,看到前者返回True、后者返回False,再打断点观察每一步max_reach的更新过程,那种“啊,原来如此”的顿悟才真正发生。

这个资源包的设计逻辑很朴素:用最小认知负荷,建立最扎实的直觉。所有代码都控制在80行以内,不引入任何第三方库(连heapq都只在prim.py里按需使用),变量名全部采用语义化命名(如current_farthest, total_surplus, min_edge_weight),注释不是解释语法,而是解释“为什么这里必须这么写”。配套的三张示意图jump.jpggas.jpgprim.jpg也不是装饰画——jump.jpg用箭头标出了每一步i位置上能跳到的范围区间;gas.jpg用折线图展示了从不同起点出发时油量盈余的累积变化趋势;prim.jpg则用颜色区分了已加入MST的节点、候选边、被拒绝的边。这些视觉线索,是你在LeetCode题解区刷一百篇文字解析都换不来的空间理解力。

它适合谁?如果你正在准备技术面试,这个包就是你的“贪心专项训练弹药库”——每个.py文件对应一道高频真题,运行即得结果,改输入即见反馈;如果你是自学算法的新手,它比教科书更友好,因为所有抽象原则(如贪心选择性质、最优子结构)都锚定在具体可执行的代码上;如果你是讲师或助教,第8课-贪心.ppt里的对比表格(贪心 vs DP vs 回溯的适用边界)、典型误区案例(如“分数背包能贪心,0-1背包不能”)、以及课堂演示脚本(如何用print()逐步展示Prim算法中边的选取过程),都是现成的教学素材。它不承诺让你秒变算法大神,但能确保你下次遇到贪心题时,第一反应不再是“试试DP”,而是冷静地问自己:“这个问题有没有贪心选择性质?我能构造反例证伪吗?”

2. 贪心算法的核心设计思想与适用边界深度拆解

2.1 贪心算法的本质:一个被严重低估的“构造性证明”过程

很多人把贪心算法当成一种“经验性策略”——看到题目觉得“好像可以每次都选最好的”,然后硬凑代码。这是最大的误区。真正的贪心设计,是一个严谨的数学构造过程,必须同时满足两个条件:

  1. 贪心选择性质(Greedy Choice Property):存在一种局部最优的选择,使得该选择加上剩余子问题的最优解,能构成原问题的最优解。注意,这不是说“任意”局部最优都行,而是“存在至少一种”可证明的局部最优。
  2. 最优子结构性质(Optimal Substructure):一个问题的最优解包含其子问题的最优解。这点贪心和DP共享,但贪心的关键在于,子问题的最优解不需要依赖于父问题的其他选择,它独立存在。

这两个性质缺一不可。举个反例:0-1背包问题。它有最优子结构(选或不选第i件物品后的剩余容量问题),但没有贪心选择性质。按单位价值排序后优先装高价值物品,很容易被构造出反例(如容量10,物品A:价值60/重量10,物品B:价值50/重量5,物品C:价值50/重量5。贪心选A得60,但选B+C得100)。而分数背包可以,因为你可以把物品切开,局部最优(单位价值最高)直接贡献全局最优。

这个资源包里的所有题目,都经过了这两个性质的显式验证。比如jump_game.py
- 贪心选择性质:在位置i,我们选择跳到i + nums[i]所能覆盖的最远位置。证明思路是:假设存在一个更优解,它在某步没有跳到当前最远位置,那么我们可以把它替换成跳到最远位置,新路径不会更差(因为覆盖范围更大,后续选择空间不减小)。
- 最优子结构:能否从位置i跳到终点,只取决于从i+1i+nums[i]范围内是否存在一个位置j,使得从j能跳到终点。这个子问题的解不依赖于前面怎么跳过来的。

2.2 何时该果断放弃贪心?三类经典“陷阱题”识别指南

贪心的失败往往比成功更有教学价值。第8课-贪心.ppt里专门用一页总结了三大“贪心幻觉”场景,我结合实操经验补充细节:

陷阱一:状态间存在强耦合,无法独立决策
- 典型表现:当前选择会显著改变后续所有选项的权重或可行性,且这种改变无法被一个简单的数值(如“当前最远距离”)完全刻画。
- 实例monotone_increasing_digits.py看似是贪心(从高位开始,遇到下降就降位并填9),但它成功的关键在于降位操作是确定性的、不可逆的。一旦高位d[i] > d[i+1]d[i]必须减1,后面全置9,这个操作不产生分支。如果题目改成“最多修改k位数字使其单调递增”,这就变成了DP问题,因为每次修改会影响后续可修改次数的分配。
- 实操心得:当你发现“选A还是选B”会导致后续可用选项集合发生质变(比如从10个变成2个,且这2个还互相排斥),立刻警惕,画个小规模反例测试。

陷阱二:目标函数非线性,局部最优无法叠加
- 典型表现:最终答案不是各步收益的简单求和,而是涉及乘积、最大值、最小值等运算。
- 实例merge_number.py(合并数字使总代价最小)的目标是让所有合并步骤的代价之和最小。代价定义为两数之和。这看起来像贪心(每次合并最小的两个数),但它其实是哈夫曼编码的变种,严格满足贪心选择性质。但如果你把代价改成“两数之积”,贪心就失效了——合并[2,3,4],先合2&3得6,再合6&4得24,总代价23 + 64 = 6+24=30;而先合3&4得12,再合2&12得24,总代价34 + 212 = 12+24=36。两者不同,说明不存在唯一最优策略,需要DP枚举。
- 实操心得:在写贪心前,先问自己:“如果我把前k步的局部最优解固定下来,剩下的子问题是否依然独立?它的最优解加进来,是否必然构成全局最优?” 如果答案模糊,先写个暴力DFS验证小数据。

陷阱三:约束条件形成全局环路,破坏无后效性
- 典型表现:问题存在环形结构或周期性约束,使得“从起点出发”的假设不成立,必须考虑所有可能的起点。
- 实例gas_station.py正是这类问题的典范。它表面是环形,但通过一个精妙转化规避了环路:如果总油量 >= 总消耗,则必存在解;且解一定是某个“盈余累计首次由负转非负”的位置。这个结论的证明本身就是一个贪心思想的应用——它把环形问题拆解为线性扫描,并利用了“若从i出发失败,那么i到失败点之间的所有位置都不可能是起点”这一关键观察。这个观察,就是贪心选择性质的体现。
- 实操心得:遇到环形题,不要硬套“破环成链”,先计算全局可行性(总和比较)。如果全局可行,再用“盈余累计”扫描找起点;如果全局不可行,直接返回-1。这个模式在很多环形调度题中通用。

2.3 资源包中各算法的贪心策略映射表

下表将资源包中的每个Python文件,与其背后的贪心核心思想、适用条件及易错点进行精准映射,帮你建立系统性认知:

Python文件 核心贪心策略 关键适用条件 常见误判点 第8课-贪心.ppt对应页码
jump_game.py 覆盖范围最大化:在当前位置,选择能扩展最远可达边界的下一步。 数组元素代表“能力”,目标是单向抵达终点,无回头路。 误以为要跳到nums[i]值最大的位置(错!应跳到i+nums[i]最大的位置);忽略i超过max_reach时的提前终止判断。 P12-贪心模式:区间覆盖类
gas_station.py 盈余累计临界点:找到第一个使“从该点出发的油量盈余累计值永不小于0”的起点。 环形路径,总油量≥总消耗,每站有独立油量与消耗。 试图暴力枚举所有起点(O(n²));未理解“若从i出发在j处失败,则i到j-1均不可能是起点”的剪枝逻辑。 P15-贪心模式:环形调度类
prim.py 最小边生长:每次从未加入MST的节点中,选择一条连接已集与未集的权重最小的边。 图连通、无向、边权非负,目标是生成树(n-1条边)。 混淆Prim与Kruskal(后者是按边排序,前者是按点扩展);未正确维护“未访问节点到已集的最小距离”数组,导致重复添加或遗漏。 P18-贪心模式:图论生成树类
merge_number.py 最小代价合并:总是合并当前列表中数值最小的两个数。 合并代价为两数之和,目标是最小化总代价。 误用简单排序+首尾合并(错!必须每次取最小两个);未意识到这等价于构建哈夫曼树,需用堆优化至O(n log n)。 P21-贪心模式:合并代价类
monotone_increasing_digits.py 高位优先修正:从左到右扫描,遇下降则高位减1,后续全置9。 构造≤给定数的最大单调递增数,数字序列可视为字符串处理。 在降位后未将后续所有位设为9(导致结果非最大);未处理高位减1后产生前导零的情况(如100→99)。 P24-贪心模式:数字构造类

这张表不是死记硬背的清单,而是你调试代码时的“思维检查表”。当你gas_station.py输出错误结果时,立刻对照“常见误判点”栏,十有八九能找到bug所在。

3. 核心代码逐行解析与实操要点详解

3.1 jump_game.py:覆盖范围的艺术,一行代码定生死

def canJump(nums):
    max_reach = 0  # 当前能到达的最远索引
    for i in range(len(nums)):
        if i > max_reach:  # 当前位置已超出可达范围,无法继续
            return False
        max_reach = max(max_reach, i + nums[i])  # 更新最远可达位置
        if max_reach >= len(nums) - 1:  # 提前终止:已能到达或超过终点
            return True
    return True  # 循环结束,说明全程可达(此行实际不会执行,因上面已覆盖)

这段20行不到的代码,浓缩了贪心算法的精髓。我们来拆解每一行背后的“为什么”:

  • max_reach = 0:初始化是关键。它代表的是“在遍历到索引i之前,我们凭借前面所有跳跃所能达到的最远位置”。它不是一个静态的“能力值”,而是一个动态的“覆盖边界”。
  • if i > max_reach::这是整个算法的“安全阀”。它意味着:即使我们把前面所有能跳的步数都用上,也根本跳不到位置i。既然连i都到不了,后面的i+1i+2更不可能。这是贪心算法能O(n)解决的根本原因——它允许我们提前剪枝,无需尝试所有路径。 如果你删掉这行,代码在[0,1]输入下会陷入无限循环(i=1max_reach仍为0,永远进不了if)。
  • max_reach = max(max_reach, i + nums[i]):这才是真正的“贪心选择”。i + nums[i]是站在位置i上,单次跳跃能达到的最远索引。我们并不关心nums[i]本身多大,只关心它能帮我们把边界推到哪里。max()函数确保了我们的“覆盖范围”始终是历史最优。
  • if max_reach >= len(nums) - 1::这是性能优化。一旦max_reach达到了最后一个索引(len(nums)-1),我们就立刻返回True。不必等到遍历完所有元素。对于[2,3,1,1,4],当i=1时,max_reach更新为max(2, 1+3)=4,等于5-1,立即返回。

提示:运行时可在循环内加print(f"i={i}, nums[i]={nums[i]}, max_reach={max_reach}"),观察[3,2,1,0,4]的执行过程:i=0max_reach=3i=1max_reach=max(3,1+2)=3i=2max_reach=max(3,2+1)=3i=3i=3 > max_reach=3?不,等于,所以继续;i=4i=4 > max_reach=3,触发return False。这个过程清晰展示了“边界停滞”是如何导致失败的。

3.2 gas_station.py:环形破局,从全局到局部的两次跃迁

def canCompleteCircuit(gas, cost):
    total_surplus = 0  # 全局油量盈余
    current_surplus = 0  # 从start出发的当前盈余
    start = 0  # 候选起点

    for i in range(len(gas)):
        total_surplus += gas[i] - cost[i]
        current_surplus += gas[i] - cost[i]

        if current_surplus < 0:  # 从start出发,在i处耗尽油量
            start = i + 1  # 下一个可能的起点是i+1
            current_surplus = 0  # 重置当前盈余

    return start if total_surplus >= 0 else -1

这段代码的精妙之处在于它完成了两次关键的“问题转化”:

第一次转化:环形 → 全局可行性判断
- total_surplus的计算是前置的、决定性的。如果total_surplus < 0,意味着无论从哪出发,总油量都不够总消耗,直接return -1。这一步把一个O(n²)的环形搜索问题,降维到了O(1)的全局判断。这是贪心算法“大局观”的体现——先看整体是否可能,再谈细节如何实现。

第二次转化:环形 → 线性扫描
- current_surplus模拟了从start出发的旅程。当它在位置i变为负数,说明从starti这段路,油不够烧。关键洞察来了:starti之间的任何一个位置j,都不可能是有效起点。 为什么?因为从startj的盈余(假设为X)一定是非负的(否则早就在j之前失败了),而从ji的盈余是current_surplus - X,既然current_surplus < 0,那current_surplus - X必然更小。所以j出发只会更早失败。因此,我们安全地将start跳到i+1,并重置current_surplus

注意:start = i + 1这行代码是核心。它不是随意的,而是基于上述数学证明的必然选择。运行gas = [1,2,3,4,5], cost = [3,4,5,1,2]时,你会看到start从0跳到3,最终返回3。手动验证:从索引3(gas=4, cost=1)出发,4-1=3;到4(5-2=3,累计6);到0(1-3=-2,累计4);到1(2-4=-2,累计2);到2(3-5=-2,累计0);回到3,完成一圈。这个过程印证了算法的正确性。

3.3 prim.py:最小生成树的“生长”逻辑,堆与数组的权衡

import heapq

def prim_mst(graph, start=0):
    n = len(graph)
    visited = [False] * n
    min_heap = [(0, start)]  # (weight, node)
    mst_edges = []
    total_weight = 0

    while min_heap and len(mst_edges) < n - 1:
        weight, u = heapq.heappop(min_heap)
        if visited[u]:
            continue
        visited[u] = True
        total_weight += weight

        # 将u的所有邻接边加入堆
        for v, w in graph[u]:
            if not visited[v]:
                heapq.heappush(min_heap, (w, v))

    return mst_edges, total_weight

Prim算法常被误解为“纯贪心”,其实它是贪心与数据结构的完美结合。核心思想是“生长”:从一个点开始,每次把离当前MST最近的一个点拉进来。

  • min_heap = [(0, start)]:初始化堆,权重0表示从start到自身的“距离”。这是启动生长的种子。
  • if visited[u]: continue:这是堆实现中的经典“延迟删除”。因为一个节点可能被多次加入堆(当它与多个已访问节点相连时),我们只处理第一次弹出的、且未被访问过的节点。这是保证O(E log V)时间复杂度的关键。
  • for v, w in graph[u]:graph[u]是一个邻接表,存储元组(neighbor, edge_weight)。这里体现了图的存储方式对算法实现的影响。资源包采用邻接表而非邻接矩阵,就是为了稀疏图的效率。

实操心得:prim.jpg示意图里,用红色高亮了每次被选中的边。你可以对照代码,在heapq.heappush前加print(f"Adding edge {u}->{v} with weight {w}"),观察边是如何被“候选”和“确认”的。你会发现,算法并非按边的原始权重顺序选择,而是按“该边连接的未访问节点到已访问集合的最短距离”来选择,这正是Prim区别于Kruskal的本质。

3.4 merge_number.pymonotone_increasing_digits.py:贪心在数字世界的两种面孔

这两道题展示了贪心策略在不同数据结构上的应用。

merge_number.py(哈夫曼式合并):

import heapq

def merge_stones(stones):
    if len(stones) <= 1:
        return 0
    heapq.heapify(stones)  # 小顶堆
    total_cost = 0
    while len(stones) > 1:
        a = heapq.heappop(stones)
        b = heapq.heappop(stones)
        cost = a + b
        total_cost += cost
        heapq.heappush(stones, cost)
    return total_cost
  • 关键点:必须用heapq。如果用sorted()每次排序,时间复杂度会退化到O(n² log n)。堆能在O(log n)内完成插入和删除。
  • 为什么贪心成立?因为每一次合并的代价都会被后续所有合并“继承”。为了让总代价最小,我们必须让小数字尽可能多地参与合并(即出现在哈夫曼树的深层),而大数字参与次数少(在浅层)。每次合并最小的两个,正是实现这一目标的最优策略。

monotone_increasing_digits.py(高位修正法):

def monotoneIncreasingDigits(N):
    s = list(str(N))
    n = len(s)
    # 从右往左找第一个下降点
    i = n - 1
    while i > 0 and s[i-1] <= s[i]:
        i -= 1
    # 如果没找到,说明已是单调递增
    if i == 0:
        return N
    # 找到下降点s[i-1] > s[i],将s[i-1]减1
    s[i-1] = str(int(s[i-1]) - 1)
    # 将i及之后所有位设为'9'
    for j in range(i, n):
        s[j] = '9'
    return int(''.join(s))
  • 关键点:扫描方向是从右向左。因为我们要找的是“第一个破坏单调性”的位置,而高位的改动影响最大。如果从左向右扫,遇到1231,会在索引3发现3>1,但此时高位123已经固定,无法通过降位3来挽救。
  • 为什么降位后全置9?因为我们要构造“不大于N的最大”单调数。s[i-1]减1后,s[i]到末尾的数字已经失去了约束(它们可以自由取最大值),而最大值就是9。例如123112291229 < 1231且是最大的满足条件的数。

4. 配套资源深度利用指南与避坑实战手册

4.1 第8课-贪心.ppt:不只是讲义,更是你的“防坑说明书”

这份PPT的价值,远超一般教学幻灯片。它是我根据五年面试官经验和上千份学员错题本提炼出的“贪心雷区地图”。以下是几个关键页的深度解读和使用建议:

  • P7页:“贪心 vs DP vs 回溯”三维对比表
    表格从“问题特征”、“核心思想”、“时间复杂度”、“空间复杂度”、“典型代表题”五个维度对比。不要只看结论,要动手填空。比如遮住“典型代表题”一栏,根据左侧特征,自己写出对应的LeetCode题号(如“问题特征:存在重叠子问题,且最优解依赖于子问题的最优解” → 应该填“53. 最大子序和”或“70. 爬楼梯”)。这个过程能强制你内化概念边界。

  • P11页:“贪心选择性质”的三种证明方法
    列出了“交换论证法”、“数学归纳法”、“反证法”及其适用场景。重点练习“交换论证法”,这是面试中最常被要求口头阐述的。以jump_game为例,练习这样说:“假设存在一个最优解S,它在位置i没有跳到i+nums[i]。那么我们可以构造一个新解S’,将S中在i的跳跃改为跳到i+nums[i]。由于i+nums[i]是i能到达的最远点,S’的覆盖范围不小于S,因此S’也是最优解。这证明了存在一个在i处做出‘贪心选择’的最优解。”

  • P28页:“课堂演示脚本:Prim算法可视化”
    这页提供了详细的print语句插入指南。例如,在heapq.heappop后打印f"Popped node {u} with weight {weight}",在heapq.heappush后打印f"Pushed candidate edge to {v} with weight {w}"强烈建议你照着脚本,在prim.py里逐行添加这些打印,并用graph = [[(1,2), (2,3)], [(0,2), (2,1)], [(0,3), (1,1)]]这样的小图运行。你会亲眼看到MST是如何像一棵树一样,从一个根节点开始,一层层向外生长的。

4.2 三张示意图jump.jpggas.jpgprim.jpg:让抽象逻辑“看得见”

这些图不是摆设,而是你理解算法的“视觉锚点”。

  • jump.jpg:图中用不同颜色的箭头标出了每个位置i的跳跃范围[i, i+nums[i]]请用尺子量一下,从位置0出发的箭头长度,是否真的等于nums[0] 这个动作能帮你建立“索引”与“距离”的直观联系。图中还用虚线框标出了max_reach的动态变化过程,观察虚线框是如何一步步向右推进的。

  • gas.jpg:这是一张折线图,横轴是加油站索引,纵轴是“从起点0出发的累计盈余”。图中清晰地标出了几个关键点:全局最低点(对应total_surplus最小)、以及从不同起点出发时,折线的平移效果。重点观察:为什么从索引3出发的折线,其最低点恰好在x轴之上? 这就是算法中start = i + 1的几何意义——我们是在寻找那个能让整条折线“抬升”到x轴之上的起点。

  • prim.jpg:图中用三种颜色区分:绿色节点是已加入MST的,蓝色边是当前候选边(连接绿蓝),灰色边是已被拒绝的(因为有更小的边连接同一对节点)。请对照prim.py代码,找出图中哪一条蓝色边对应代码中heapq.heappush的某一次调用? 这个练习能帮你打通“代码逻辑”与“图论直观”的最后一公里。

4.3 常见问题速查表与独家避坑技巧

以下是在真实教学和面试辅导中,学员踩过的最多、最痛的坑,附带一针见血的解决方案:

问题现象 根本原因 一键修复方案 我的独家技巧
jump_game.py[0,1] 返回 True(应为 False 忘记检查 i > max_reach 的边界条件,导致 i=1max_reach 仍为0,循环继续。 for 循环第一行,紧接 if i > max_reach: return False “零索引陷阱”口诀:只要数组第一个元素是0,jump_game 几乎必挂。把这个当测试用例,每次写完先跑它。
gas_station.py 返回 0,但实际应从 3 开始 total_surplus 计算错误,或 current_surplus 重置逻辑有误。 print(total_surplus)print(f"i={i}, current_surplus={current_surplus}") 在循环内输出。 “盈余守恒”验证法:手动计算 gascost 的总和,确保 total_surplus 输出值与你心算一致。不一致,说明输入数据读取就有问题。
prim.py 输出的 mst_edges 为空,或 total_weight 为0 图不连通,或 start 节点没有邻接边,导致 min_heap 过早为空。 while 循环前加 print(f"Graph size: {n}, Start: {start}, Neighbors of start: {graph[start]}") “单点测试”法:先用一个只有两个节点的图测试:graph = [[(1,5)], [(0,5)]]。确保它能正确输出一条边和权重5。这是所有复杂图的基石。
merge_number.pystones = [1,2,3,4] 上输出 19(应为 19?等等,重新算!) 混淆了“合并代价”和“最终数字”。[1,2,3,4]:先合1+2=3(代价3),得[3,3,4];再合3+3=6(代价6),得[6,4];再合6+4=10(代价10);总代价3+6+10=19。正确! 不要怀疑算法,先用纸笔算一遍小例子。 “代价累加”可视化:在每次 heapq.heappop 后,打印 f"Merge {a} and {b}, cost {cost}, new total {total_cost}"。看着数字累加,安全感倍增。
monotone_increasing_digits.pyN=100 输出 99(正确),但对 N=10 输出 9(也正确),对 N=11 却输出 11(正确)… 一切正常? 这其实是正确的!11 本身就是单调递增的。 当输出看起来“太简单”时,别急着改代码,先确认题目要求:“不大于N的最大单调递增数”。11 符合要求。 “边界测试”铁律:必须测试 N=1, N=9, N=10, N=99, N=100, N=12321。这些数字覆盖了所有边界情况。

注意:所有修复方案都指向一个核心原则——print()作为你的“显微镜”。资深工程师和新手最大的区别,不在于会不会写代码,而在于敢不敢、会不会用最朴素的print()去照亮代码执行的每一个幽暗角落。这个资源包的所有代码,都为你预留了print()的插入位置,大胆去用。

5. 从“运行成功”到“融会贯通”的进阶路径

拿到这个包,运行一遍python jump_game.py看到True,只是旅程的起点。真正的价值,在于如何用它撬动更深层的算法能力。这是我给学员设计的三步进阶法:

第一步:逆向工程(1小时)
选择一个你最熟悉的题目(比如jump_game.py),然后做一件“反直觉”的事:把代码里的关键变量名全部改成无意义的名字,比如max_reach改成x, i改成idx, nums改成arr。然后,不看原代码,只凭print()输出的日志,尝试把逻辑重新“翻译”回人类语言。这个过程会强迫你剥离语法糖,直击算法骨架。你会发现,x = max(x, idx + arr[idx]) 这一行,其本质就是“扩张边界”,与变量名无关。

第二步:变异测试(2小时)
对每个.py文件,进行三次“破坏性修改”,然后观察结果:
- 破坏1:逻辑篡改:在gas_station.py里,把if current_surplus < 0: 改成 if current_surplus <= 0:。运行,看哪个测试用例会失败?为什么?
- 破坏2:边界删除:在jump_game.py里,删掉if i > max_reach: return False。运行[0,1],会发生什么?(答案:IndexError,因为i=1时试图访问nums[1],但max_reach=0i已越界)。
- 破坏3:数据污染:在prim.pygraph输入里,故意加一条负权边,比如graph[0].append((1, -1))。运行,结果是否还正确?(Prim要求非负权,负权会破坏贪心选择性质)。

这些“破坏”,不是为了制造bug,而是为了给你一把刻刀,让你亲手雕刻出算法成立的精确条件。

第三步:横向迁移(3小时)
找一道你没做过的、但感觉“好像能用贪心”的新题(比如LeetCode 45. 跳跃游戏II,求最少跳跃次数),不看任何题解,只用这个资源包里的思想工具箱去尝试
- 它有贪心选择性质吗?(有,每次跳到当前能到达的最远位置,能最小化跳跃次数)
- 它的最优子结构是什么?(从位置i到终点的最少跳跃次数,取决于从i+1i+nums[i]范围内所有位置到终点的最少跳跃次数的最小值)
- 如何复用jump_game.py的框架?(把max_reach升级为current_max_reachnext_max_reach,用“层”的概念模拟BFS)

当你能独立完成这三步,这个资源包就不再是一个“练习包”,而成了你算法思维里的一个“器官”——它会自主工作,帮你识别模式、规避陷阱、生成解法。

最后分享一个小技巧:把prim.jpggas.jpgjump.jpg这三张图,设置成你电脑桌面的壁纸。每天开机,它们都在提醒你:算法不是抽象的符号,而是有形状、有颜色、有生长逻辑的生命体。当你下次看到一道新题,下意识在脑中勾勒出它的“图景”时,你就已经超越了绝大多数人。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个资源包提供多个经典贪心算法的Python实现,直接运行就能看到结果。jump_game.py判断数组能否跳到末尾,gas_station.py验证环形加油站路径是否可行,prim.py用Prim算法构造最小生成树,还附带对应示意图gas.jpg、jump.jpg、prim.jpg方便理解逻辑。另外包含merge_number.py(合并数字使总代价最小)和monotone_increasing_digits.py(构造不大于给定数的最大单调递增数字),都是典型贪心策略应用。所有代码注释清晰,变量命名直观,适合边学边练。配套PPT《第8课-贪心.ppt》讲清楚贪心适用场景、常见模式和容易踩的坑,比如什么时候不能用贪心而该选动态规划。整个包结构干净,没有多余依赖,下载解压后用Python3直接执行各.py文件即可验证输出,适合算法入门、面试突击或课堂演示。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐