news 2026/10/14 11:57:55

贪心算法从入门到进阶:原理、反例与刷题实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
贪心算法从入门到进阶:原理、反例与刷题实战指南

今天是算法学习day27,主题锁定贪心算法。学到这里我发现一个有趣的现象:身边很多朋友一提起贪心,第一反应是"不就是每次选最大的嘛",但真到做题的时候,要么想不出策略,要么想出来了却不敢写。这篇文章是我从day1刷到day27之后,对贪心算法的一次系统整理,包含原理、题型拆解、代码模板和踩坑记录,适合正在刷题、准备面试、或者刚学完基础数据结构想进阶的人参考。

1. 贪心算法不是"每次都选最大的":本质与正确性门槛

1.1 先从一个"贪心会翻车"的例子说起

假设我们有面额为1、15、25的硬币,要凑出30块钱。如果按"每次拿最大的面额"这个思路走,你会先拿25,剩5,然后拿五枚1元,总共6枚硬币。但最优解明明是两枚15元,2枚硬币搞定。

这就是很多人对贪心的第一个误解:贪心不等于"每一步都拿最大/最快/最贵"。"每次都拿最大面额"只是贪心在特定场景下的一种表现,真正的贪心是——每一步都做出当前状态下的最优选择,并且相信这些局部最优选择拼起来就是全局最优。

在硬币面额为1、15、25的系统里,这种"每次拿最大"的策略显然不成立。而在我国常见的人民币找零场景里,因为面额设计特殊(1、5、10、20、50、100),贪心策略反而恰好总是对的。这说明什么?说明贪心能否成立,取决于问题本身的结构,而不是你贪不贪。

1.2 贪心成立的两个必要条件

要把贪心算法从"猜想"变成"可用的算法",必须满足两个条件:

贪心选择性质:每一步的局部最优选择,最后能导向全局最优解。关键在于这个选择一旦做出,就不能反悔,也不需要回头修正前面的选择。

最优子结构:一个全局最优解,一定包含了子问题的最优解。也就是说,你先把第一步的最优选掉了,剩下的是一个规模更小的同类型问题,而你在这个子问题上继续用同样的贪心策略,仍然能拿到最优。

我自己的理解方式是:贪心像是爬一座"只有一个山头"的山。只要你每一步都朝最陡的方向爬,最终一定到山顶。但如果地形是连绵起伏的多峰结构,你朝最陡方向走,很容易被困在某个小山包上,这就是"局部最优不等于全局最优"。

动态规划之所以可靠,是因为它把所有可能的分支都记录下来,最后取最优。贪心不做穷举,它只走一条路,所以必须有一条"这条路一定通向全局最优"的保证。这种保证不能靠感觉,要有证明。

1.3 证明贪心正确性的两种常用套路

面试和竞赛中,最常见的证明方法有两种:

交换论证法。假设某个最优解和贪心选择不同,那我尝试把这个最优解里的某个元素和贪心选择的元素交换一下。如果交换之后解不会变差,就说明贪心选择的元素完全有资格出现在某个最优解里。反复做这种交换,就能把一个任意最优解"变"成贪心解。区间调度类题目经常用这个方法。

数学归纳法。证明第i步贪心选择之后,剩余子问题仍然保留最优性质,然后归纳到整个问题。

不过说实话,日常刷题很少真的要写完整证明。我自己的做法是:先用小规模例子验证,再用反例思维去攻击这个策略——如果我能构造出一个反例,说明贪心不成立;如果构造不出来,而且直觉和常见结论都支持,那就大概率可以写。

2. 从最"无脑"的三道题建立手感:分发饼干、柠檬水找零、最大子数组和

2.1 分发饼干:排序后双指针,喂"最不浪费"的那个

题目场景很朴素:有一群孩子,每个人的胃口值不同;有一堆饼干,每块饼干的大小不同。一块饼干只能喂一个胃口小于等于它大小的孩子,问最多能满足几个孩子。

我第一眼看到这个题,脑子里冒出的策略就是:小饼干喂小胃口。为什么不是大饼干喂大胃口?因为如果我把大饼干拿去喂了胃口小的孩子,那胃口大的孩子可能就没饼干吃了;反过来,小饼干本来也喂不了大胃口的孩子,所以它唯一的用处就是喂尽量小的胃口。这个"最小浪费"思路就是贪心选择性质。

实现上很简单,排序之后两个指针一起走:

int findContentChildren(vector<int>& g, vector<int>& s) { sort(g.begin(), g.end()); sort(s.begin(), s.end()); int i = 0, j = 0; while (i < g.size() && j < s.size()) { if (s[j] >= g[i]) { i++; // 满足一个孩子 } j++; // 无论是否满足,饼干都要往后走 } return i; }

复杂度是O(n log n),瓶颈在排序。这道题让我意识到,贪心题往往第一步是排序——排序让"当前最优"变得可计算。如果不排序,你无法确定哪个饼干是"恰好够用"的。

2.2 柠檬水找零:唯一正确的动作是"大票优先找零"

这道题说的是卖柠檬水,一杯5块钱。顾客排着队来买,可能给5元、10元、20元,你要保证每个人都能正确找零,初始时你手头一分钱没有,判断能不能给所有顾客找开。

这题的贪心策略藏在一个特别朴素的地方:收到10元时,你只能找一张5元;收到20元时,你有两种找法,一种是一张10加一张5,另一种是三张5。应该优先用10元去凑20的找零,因为5元是"万能小钞",10元只能在找20时才用到,一旦你用掉三张5,后面来了一个10元,你可能就找不开了。

bool lemonadeChange(vector<int>& bills) { int five = 0, ten = 0; for (int b : bills) { if (b == 5) five++; else if (b == 10) { if (five == 0) return false; five--; ten++; } else { // 20 if (ten > 0 && five > 0) { ten--; five--; } else if (five >= 3) { five -= 3; } else { return false; } } } return true; }

注意这里有一个隐藏的坑:有人会觉得,收到20元时如果手里有10元,可以先把10元留着,"万一后面还有人给20呢"。实际上这是反的——你这次找零都过不去的话,后面根本没有"万一"。贪心算法的特点是只关心"当下能不能过",因为每一步都是当前状态下的最优决策,而这题恰恰证明了:优先用10元是当前状态下最不消耗稀缺资源的选择。

2.3 最大子数组和:你分不清它是DP还是贪心

给定一个整数数组,找出一个具有最大和的连续子数组。这题最著名的解法是Kadane算法,核心就一行:

int maxSubArray(vector<int>& nums) { int cur = 0, best = INT_MIN; for (int x : nums) { cur = max(x, cur + x); best = max(best, cur); } return best; }

这个式子的贪心解释特别漂亮:一旦当前的累加和cur变成负数,它对后面任何位置的贡献都是负的,所以果断把它丢掉,从当前元素重新开始累加。负数像一段糟糕的关系,及时止损才是最优。

但有意思的是,这道题也经常被归为动态规划,因为cur本质上是一个状态,转移方程是dp[i] = max(nums[i], dp[i-1] + nums[i])。它其实是"贪心思想和DP状态表示恰好重叠"的那类题。这类题给了我一个很重要的启示:算法分类不是死板的,关键是看透题目的结构,而不是纠结标签。

3. 移动类贪心:跳跃游戏、加油站和"可达范围"

3.1 跳跃游戏:维护每步能走到的最远位置

题目:给定一个非负整数数组,每个数字代表你在当前位置最多能往后跳几步,初始在下标0,判断能否到达最后一个下标。

这个题如果走"每次跳最远"的思路,反而容易错。举个反例:数组是[3, 4, 1, 0, 0, 1, 1]。从下标0按"跳最远"的贪心会跳到3,而下标3是0,直接卡死;但最优路径是先跳到1,再一路跳到终点。这说明"看起来最贪心"的动作不一定是最优的。

正确的贪心策略是:维护一个"当前能到达的最远下标"maxReach,遍历过程中不断用它覆盖到的位置去更新它。你不需要决策具体跳到哪里,只需要知道"这一步能覆盖的范围有多大"。

bool canJump(vector<int>& nums) { int maxReach = 0; for (int i = 0; i < nums.size(); i++) { if (i > maxReach) return false; // 这个位置根本到不了 maxReach = max(maxReach, i + nums[i]); if (maxReach >= nums.size() - 1) return true; } return true; }

为什么这样是贪心?因为maxReach是遍历到当前位置时能到达的最远位置,我们每次都选择更新到最大,相当于把"可达范围"这个状态最大化。只要这个最大范围没有断档,就一定存在一条路径走到范围内任何一点。这是"覆盖范围最大化"型贪心,重点是范围而不是具体落点。

3.2 跳跃游戏II:最少跳数里的"区间分段"思想

进阶版是在保证能到达终点的前提下,求最少跳数。我一开始想的贪心是"每次跳到能跳最远的位置",又被反例教育了。正确的思路是按"一步能覆盖的区间"来切分:

遍历当前步覆盖的区间[i, end],期间不断记录下一步能达到的最远位置maxPos。当遍历到end时,说明这步覆盖区间的所有位置都已经处理完,必须跳一步,于是end = maxPos,步数加一。

int jump(vector<int>& nums) { int steps = 0, end = 0, maxPos = 0; for (int i = 0; i < nums.size() - 1; i++) { maxPos = max(maxPos, i + nums[i]); if (i == end) { end = maxPos; steps++; } } return steps; }

这个解法的精妙之处在于:你并没有真的模拟"跳到了哪",而是把每一步看作一次"区间推进"。当前区间能覆盖到的所有点,都是这一步可以落脚的候选点,而下一步区间是这些候选点能延伸到的并集。每走完一个区间,必然需要消耗一步。这就是典型的"过程最优"贪心。

3.3 加油站:先判断全局可行性,再找唯一起点

沿环路上有若干个加油站,每个站能加油,每段路要消耗油,问从哪个站出发能绕一整圈回到起点。

这题的贪心策略非常经典。先对整个数组求total = sum(gas[i] - cost[i]),如果total < 0,说明总油量都不够总消耗,无论从哪出发都不可能。接下来用curSum记录从候选起点开始的累积剩余油量,一旦curSum < 0,就把起点设置为i + 1并把curSum归零。

int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { int total = 0, curSum = 0, start = 0; for (int i = 0; i < gas.size(); i++) { total += gas[i] - cost[i]; curSum += gas[i] - cost[i]; if (curSum < 0) { start = i + 1; curSum = 0; } } return total >= 0 ? start : -1; }

为什么要在这个位置重置起点?逻辑是这样的:假设从start出发,走到i时累积油量变负了,那就说明从start到i之间的任意一个加油站k作为起点,都不可能越过i这个位置。因为从k出发到i时,油量只会更少——你等于把start到k这一段本来就没攒下的油给跳过了,剩余积累不可能比从start出发更多。所以唯一值得尝试的新起点就只能是i+1。这个"累积和为负就重置"的思路,在很多前缀和、差分相关的问题里都能看到。

4. 区间问题专项:排序决策是贪心的主战场

4.1 无重叠区间:为什么必须按结束时间排序

题目是:给定一个区间的集合,求最少需要移除多少个区间,才能让剩下的区间互不重叠。

大多数第一次做区间题的人,第一反应是按start排序。然后你会构造出一个反例:区间A是[1, 100],区间B是[2, 3],区间C是[3, 4]。如果按start排序,你会先看到A,然后B、C全都被A盖住,判断结果为只能保留1个区间;但正确答案是保留B和C,移除A,答案是移除1个。

正确的贪心策略是:按end升序排序,优先保留结束最早的区间。理由用交换论证很容易说明:在所有互不重叠的最优方案里,如果把第一个区间换成所有区间里end最小的那个,后面的区间也不会被这个新区间挡住,因为新区间的结束时间不晚于原来那个。这就是"结束最早的区间给后面留下了最大空间"。

int eraseOverlapIntervals(vector<vector<int>>& intervals) { sort(intervals.begin(), intervals.end(), [](auto& a, auto& b) { return a[1] < b[1]; }); int count = 1; int lastEnd = intervals[0][1]; for (int i = 1; i < intervals.size(); i++) { if (intervals[i][0] >= lastEnd) { count++; lastEnd = intervals[i][1]; } } return intervals.size() - count; }

做题时我发现一个关键词规律:看到"最少移除""最多保留""最多能安排"这类字眼,基本就是贪心区间题。这类题排序键的选择是生死线——排错一个键,整个算法就废了。

4.2 合并区间、引爆气球:排序键怎么选

合并区间这题是按start排序,然后检查当前区间是否和前一个合并结果重叠,重叠就扩展end,不重叠就开启新区间。它有时候被划进贪心,但我觉得它更像是"排序加扫描",因为每一步并没有"做选择",只是在如实合并。

真正值得对比的是"用最少的箭引爆气球"这类题:在二维平面里,多个区间(气球)如果共享某个x坐标,就能被同一支箭引爆,求最少箭数。这题其实是无重叠区间问题的镜像,同样按end排序,然后计数:只要当前气球左端不超过当前箭的x坐标(也就是之前保留区间的end),就可以共用一支箭;否则必须新开一支箭并更新位置。

这些题的共性套路非常清晰:

  • 第一步:明确排序键,到底按start还是按end,取决于你希望"留下谁"。
  • 第二步:维护当前合并/保留区间的右端点。
  • 第三步:遇到重叠做决策,不重叠就重置。

4.3 区间类贪心的本质:一维上的资源分配

不管是会议安排、课程表排课、最少箭矢,还是无重叠区间,本质都是"在一条时间线上尽可能多地安排互不冲突的事件"。这类问题的标准答案是:每次选结束最早的那个事件。这个结论可以追溯到活动安排问题,几乎是贪心算法教科书里最经典的模型。

我个人的经验是,碰到区间题先不要急着写代码,先在纸上画出几个区间,用排序键不同的方案各跑一遍,哪个能通过最极端的情况,哪个就是对的。区间题是最容易通过画图发现反例的题型,一定要利用这个优势。

5. 贪心和动态规划怎么选:day27之前我踩过的识别坑

5.1 关键差别:做完选择之后,需不需要回头看

我前二十多天动态规划刷得比较多,所以学贪心的时候一直在想:什么题该用DP,什么题该用贪心?后来总结出一个特别直接的口头标准:如果这一步做了选择,后续不需要因为未来信息而修改这个选择,那大概率可以贪心;如果当前选择会限制未来的可能性,那就要DP。

我做了一个对比表,方便你考前复习:

维度贪心算法动态规划
决策方式每步一个选择,不做回头修正记录多个状态,从转移中取最优
依赖子问题只需要一个子问题(被贪心选中的那个)需要多个重叠子问题的解
正确性依据贪心选择性质,需要额外证明状态转移穷举,正确性更容易验证
复杂度通常O(n)或O(n log n)通常O(n^2)或更高
适用场景局部最优能推出全局最优存在重叠子问题和最优子结构

这个表是给别人看的,我自己判断时只问一个问题:"全局最优 = 每一步都局部最优"这个等式成立吗?成立就是贪心,不成立就是DP,或者至少需要更复杂的手段。

5.2 最经典的对比:0-1背包和分数背包

同样是背包问题,0-1背包(物品不可分割)通常要DP,分数背包(物品可以切任意大小)用贪心就拿单位价值最高的先装。差别就一个:可不可分割。

为什么不可分割时贪心会错?因为一个体积大、单位价值高的物品,可能占用大量空间,导致你装不下几个小物品,最后总价值反而不如配合小物品的组合。分数背包里没有这个问题,因为剩下的空间理论上总能被切碎的物品填满,单位价值高的物品优先自然最优。

我刷题时经常用这道对比题当"分水岭":能一眼说出这两者的区别,就说明你理解贪心的边界了。

5.3 股票买卖系列:同一个场景三种算法

股票买卖的题目同样能说明这个问题:

  • 只买卖一次(求最大一次差价):维护历史最低价,当前价格减去历史最低价就是当前卖出的利润,取最大值。这一步天然是贪心。
  • 可以买卖多次:把每一段上涨的利润都累加起来,只要prices[i] > prices[i-1]就赚这一段。这是贪心的"吃掉所有正收益"思路。
  • 最多买卖两次:这就要DP了,因为第二次交易依赖于第一次交易的结果,当前决策会影响后续机会。

同一个背景,稍微改动限制条件,算法就完全不同。这也是为什么我不建议死背题,而是要在做题时主动追问"这个限制条件改变了什么"。

5.4 我的判别流程(可复用的思考路径)

我现在拿到一道题,会按以下顺序快速过一遍:

  1. 读题后先问:题目求的是"最多/最少/最大/最小"吗?如果不是,贪心的概率下降。
  2. 自己尝试给出一个"当前状态下的最优动作",比如排序后取最小的、覆盖范围最远的、结束时间最早的。
  3. 立刻尝试构造反例。如果构造不出来,把策略写下来;如果很快构造出反例,就放弃贪心。
  4. 写代码时先写暴力/DP版跑小数据,用随机数据对拍。这一步能挡掉大部分假贪心。
  5. 如果对拍通过,再反推一遍"为什么这个策略成立",确保不是碰巧。

这套流程让我在day27之后基本告别了"贪心写了个寂寞"的窘境。

6. 贪心刷题中的常见反例与个人检验方法

6.1 三个让我差点翻车的反例

反例一:特殊面额找零。前面已经说过,面额是1、15、25时,凑30用"每步取最大"会拿到6枚硬币,而最优是2枚。它的教训是:贪心的成立依赖面额结构,不能把一个场景里的成功经验盲目迁移到另一个场景。

反例二:跳跃游戏"每次跳最远"策略。数组[3, 4, 1, 0, 0, 1, 1]里,索引0能跳到3、1、2,其中3最远但值为0,跳过去就死;跳到1才是正解。这说明"当前步最远"不等于"全局可达最优位置"。正确贪心必须维护一个覆盖范围,而不是定格在某一个跳跃点。

反例三:区间问题按start排序贪心。区间A[1,100]、区间B[2,3]、区间C[3,4],按start排序先选A,只能得到1个区间,最优却是B和C两个。这说明区间题的排序键决定一切。

我把这三个反例贴在笔记首页,每次写贪心之前先看一眼,提醒自己三个口诀:看面额结构、看覆盖范围、看排序键。

6.2 验证一个贪心策略是否为真的三条经验

经验一,一定要对拍。我大部分贪心翻车都是靠暴力解法对拍揪出来的。你不需要用大样本,每组几百个随机小数据就够了,一旦出现不一致,立刻能定位错误策略。

经验二,从"交换论证"的角度自问。如果我的贪心解和某个最优解不同,能不能把这个最优解里的元素和我的贪心元素交换,并且结果不变差?如果答案是肯定的,说明策略有戏;如果交换后明显变差,那基本就是假贪心。

经验三,注意那些"排序一次+扫描一遍"的问题。这类题的错误往往不是扫描逻辑,而是排序键。我会把"按start排"和"按end排"各跑一遍测试数据,用实际输出说话。

6.3 day27之后的贪心学习路线

如果今天这篇内容你全部吃透了,下一步可以按这个顺序继续:

  • 第一梯队(打基础):分发饼干、柠檬水找零、买卖股票2、最大子数组和。这些题代码短、陷阱少,适合建立"贪心=局部最优决策"的直觉。
  • 第二梯队(练思维):跳跃游戏1和2、加油站、无重叠区间、用最少箭矢引爆气球。它们核心都是"范围/排序",是面试高频。
  • 第三梯队(挑战边界):摆动序列、单调递增的数字、监控二叉树。这类题需要更强的推导能力,尤其是监控二叉树这种在树上做贪心的,能彻底检验你是否真的理解了贪心的适用条件。

刷题节奏上,我建议贪心这部分每天三到五题,不要贪多。贪心题的代码往往很短,但每一道都要能说清楚三步:为什么排序?为什么选这个键?为什么这个局部决策不影响全局。能把这三句话说明白,才是真的会了。

最后说点我自己的体会

学了27天,最大的感受是贪心算法是所有非暴力算法里"看起来最容易、实际上最需要严谨"的一种。它不像动态规划那样状态一目了然,也不像回溯那样有模板可套,它的难点全在一个"为什么"上。所以我现在每做完一道贪心题,都会强制自己在注释里写一行"贪心依据",写不出来就再回头想。这个习惯让我翻车次数少了很多。

如果你正在学习算法,也建议你把今天讲的反例记下来,在自己想当然的时候拿出来泼一盆冷水。贪心算法真的不难,难的是永远对"局部最优"保持警惕。

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

局域网自建AI Agent平台:Docker部署DeepSeek Harness实战

1. 为什么要在局域网里自建 AI Agent 平台 1.1 从“调用云端 API”到“把模型搬进机房”的转变 过去一年&#xff0c;我身边不少做企业内部工具的朋友都经历过同一个阶段&#xff1a;一开始大家都是直接调用公有云上的大模型 API&#xff0c;写几行代码就能跑通一个问答机器人…

作者头像 李华
网站建设 2026/10/14 11:57:03

共聚焦显微镜技术对比:LSCM与SDCM原理与应用

1. 光学显微技术发展背景在生命科学和材料研究领域&#xff0c;共聚焦显微镜技术已经成为观察微观结构的黄金标准。传统宽场显微镜由于受到离焦光的干扰&#xff0c;成像分辨率和对比度存在明显局限。共聚焦技术的核心突破在于通过光学切片能力&#xff0c;实现了三维空间的高分…

作者头像 李华
网站建设 2026/10/14 11:56:43

Grok Bot从0到1:多机器人协同自动化实战指南

1. 先搞清楚这套东西到底在解决什么问题1.1 从"一个人干所有事"到"一群机器人分工协作"我最早接触自动化机器人是在一个内容运营的小团队里&#xff0c;当时每天要处理的事情特别杂&#xff1a;社群消息回复、定时推送、数据抓取、内容分发、关键词监控。一…

作者头像 李华
网站建设 2026/10/14 11:56:36

AI数据库与向量库的区别:从RAG到Agent的数据底座全解析

最近半年&#xff0c;技术圈里冒出一个高频词——AI数据库。身边不少团队拿着"AI数据库"的预算和需求来咨询&#xff0c;结果一聊发现&#xff0c;他们要的其实是一个向量数据库&#xff0c;甚至只是某个中间件里加了个向量索引。这个误区比想象中普遍&#xff1a;很…

作者头像 李华
网站建设 2026/10/14 11:56:34

CTF杂项解题必备5个Windows本地exe工具链

简介&#xff1a;本资源聚焦CTF网络安全竞赛中的杂项&#xff08;Misc&#xff09;题型实战需求&#xff0c;面向初学者至进阶选手&#xff0c;提供一套开箱即用的Windows端辅助工具集&#xff0c;覆盖隐写分析、音频解码、图像逐帧处理、十六进制可视化、网络流量捕获及信号识…

作者头像 李华
网站建设 2026/10/14 11:56:19

MySQL图书管理系统数据库设计:6张表建表与借还书SQL实战

简介&#xff1a;这份文档面向计算机专业学生与数据库初学者&#xff0c;围绕基于MYSQL的图书管理系统展开完整的数据库设计&#xff0c;可用于课程设计、毕业设计选题或数据库原理的实战练习。压缩包内仅含1个docx文件&#xff0c;约852KB&#xff0c;以文字与图表形式系统呈现…

作者头像 李华