☰
贪心算法实战指南:从区间调度到工程优化的边界与抉择
2026/10/13 5:42:33 网站建设 项目流程

1. 贪心算法不是"碰运气":从一道调度题看它的真实边界

很多人第一次接触贪心算法,脑子里留下的印象就是"每一步都选当前最好的,最后结果就是最好的"。这个理解对了一半,但恰恰是错的那一半,让无数人在笔试和实际项目里栽了跟头。我见过太多人写完贪心代码,跑通了样例就提交,结果被一组精心设计的测试数据直接打穿。问题不在于贪心本身,而在于没有搞清楚它到底在什么条件下成立。

贪心算法的本质是一种局部最优推导全局最优的决策策略。它把一个问题拆成一系列选择,每一步都做出当前看起来最优的决定,并且做出选择后不再回头修改。这个"不回头"是它和动态规划最根本的区别——动态规划会保留所有子问题的解,最后再组合出全局最优;贪心则是走一步丢一步,赌的就是当前这一步不会影响后续的最优性。

那这个赌注什么时候能赢?数学上有一个通用的判定框架叫拟阵,但那个东西太抽象,实际做题和工程中我们更常用两个更接地气的判断依据:

  • 贪心选择性质:每一步的局部最优选择,一定能导向某个全局最优解。换句话说,存在一个最优解,它包含了当前这一步的贪心选择。
  • 最优子结构:一个问题的最优解,包含它子问题的最优解。这一点贪心和动态规划是共享的。

拿经典的区间调度问题来举例。假设你有一堆会议,每个会议有开始时间和结束时间,同一时间只能开一个会,问最多能安排多少个会议。贪心的策略是:按结束时间从早到晚排序,每次选结束最早且和已选会议不冲突的那个。

为什么是按结束时间排,而不是按开始时间或者会议时长排?这里就是"为什么"的关键。按开始时间排,你可能会选一个开始很早但持续到天荒地老的会议,把后面所有会议都堵死了。按时长排,你可能会选一个很短但卡在中间、把左右两边都切断的会议。而按结束时间排,你选完这个会议后,留给后面的时间窗口是最大的——结束越早,后面能塞进去的会议就越多。这个"留给后面的空间最大"就是贪心选择性质在这个问题上的具体体现。

我用一段代码把这个逻辑固化下来,你可以直接拿去改:

def max_meetings(intervals): # intervals: list of (start, end) if not intervals: return 0 # 按结束时间升序 intervals.sort(key=lambda x: x[1]) count = 1 last_end = intervals[0][1] for start, end in intervals[1:]: if start >= last_end: # 不冲突 count += 1 last_end = end return count

这段代码的时间复杂度是 O(n log n),瓶颈在排序;空间复杂度 O(1),除了排序几乎不占额外空间。实测下来,十万级别的区间数据跑完不到一秒,非常稳。

但这里有个坑我必须提前说:如果问题变成"每个会议有不同权重,求最大权重和",贪心就失效了。比如会议A权重100但时间很长,会议B和C权重各60但时间短且不冲突,贪心按结束时间选可能选到B和C得到120,也可能因为排序细节选到A得到100,而真正的最优解需要比较这两种组合。这时候就得请动态规划出场。所以贪心不是万能钥匙,它是一把形状很特殊的钥匙,只能开特定形状的锁。

提示:判断一道题能不能用贪心,最快的办法是试着构造一个反例。如果你能找到一个场景,让贪心选择导致后续无法达到最优,那这道题就不能用贪心。构造反例的能力,比背模板重要得多。

2. 从零手推三个经典模型:区间、背包与霍夫曼的贪心逻辑

光知道判定条件还不够,真正让你在实战中反应迅速的,是把几个经典模型的贪心逻辑内化到肌肉记忆里。下面我挑三个最有代表性的模型,把它们的推导过程完整走一遍,而不是直接甩结论。

2.1 区间覆盖:为什么"按左端点排序"和"按右端点排序"是两回事

区间问题是一大类,很多人把它们混在一起,结果用错排序键。我把它拆成两个子问题:

子问题一:最多不重叠区间数。这个上面讲过了,按右端点升序,能选就选。

子问题二:用最少的点覆盖所有区间。比如数轴上有一堆区间,你要在数轴上放最少的点,使得每个区间至少包含一个点。这个的贪心策略是:按右端点升序,每次选当前区间的右端点作为放置点,然后跳过所有包含这个点的区间。

为什么又是右端点?因为右端点是区间最"靠右"的位置,放在这里能覆盖到尽可能多后续区间的可能性——后续区间的左端点只要小于等于这个点,就被覆盖了。这个逻辑和区间调度是相通的:右端点代表了一个"最紧迫的截止时间",优先满足最紧迫的需求,是贪心在这类问题上的通用直觉。

子问题三:合并所有重叠区间。这个反而要按左端点升序。因为你要找的是连续重叠的区间段,按左端点排序后,只要当前区间的左端点小于等于已合并区间的右端点,就说明重叠,可以合并;否则就是一个新的独立区间。这里按左端点排是因为我们要顺序扫描、动态维护一个"当前合并区间的右边界",左端点排序保证了重叠关系是连续的。

你看,同样是区间问题,排序键的选择完全取决于你要优化什么。排序键就是贪心策略的载体,选错了键,整个策略就崩了。

2.2 分数背包:贪心为什么在这里成立,而0-1背包不行

分数背包问题是:有一堆物品,每个物品有重量和价值,你可以拿走物品的一部分(比如拿半块金条),背包有容量上限,求最大价值。贪心策略很直观:按单位重量价值(价值/重量)从高到低排序,能拿多少拿多少,拿不下就切一块。

这个策略为什么成立?因为物品可以分割。你总是优先拿"性价比"最高的,直到背包装满。由于可以无限细分,最后一件物品即使只能拿一部分,也不会浪费容量。数学上可以证明,这个贪心解就是最优解。

但0-1背包就不行了,物品要么全拿要么不拿。假设背包容量10,物品A重量10价值100(单位价值10),物品B重量6价值60(单位价值10),物品C重量5价值55(单位价值11)。按单位价值排,先拿C(价值55,剩容量5),再拿B拿不下(重6),最后拿A也拿不下,总价值55。但最优解是拿A,价值100。贪心在这里彻底失败。

这个对比告诉我们一个深刻的道理:贪心成立与否,往往取决于问题是否具有"连续性"或"可分割性"。一旦决策变成离散的、非此即彼的,贪心就容易翻车。这也是为什么很多资源分配问题,如果资源可以连续分配,贪心往往有效;如果资源是整数个、不可分割的,就要警惕。

2.3 霍夫曼编码:贪心如何构建出最优前缀码

霍夫曼编码是贪心算法最漂亮的应用之一。问题是:给一堆字符和它们的出现频率,设计一套二进制编码,使得编码后的总长度最短,且任何编码都不是另一个编码的前缀(前缀码,保证解码无歧义)。

贪心策略是:每次从当前所有节点中选出频率最小的两个,合并成一个新节点,新节点频率为两者之和,放回集合;重复直到只剩一个节点。这个合并过程构建出一棵二叉树,左分支标0,右分支标1,从根到叶的路径就是每个字符的编码。

为什么这样能得到最优前缀码?直觉是这样的:频率低的字符应该用更长的编码,频率高的字符应该用更短的编码。而霍夫曼的合并过程,恰恰是让频率低的节点沉到树的深处(路径长),频率高的节点留在浅处(路径短)。每次合并最小的两个,保证了"最不常用的两个"被放到最底层,它们的编码最长,对总长度的贡献最小。

我手写一个简化版实现,帮你理解这个过程:

import heapq def huffman(freq): # freq: dict char -> frequency heap = [[weight, [char, ""]] for char, weight in freq.items()] heapq.heapify(heap) while len(heap) > 1: lo = heapq.heappop(heap) hi = heapq.heappop(heap) for pair in lo[1:]: pair[1] = '0' + pair[1] for pair in hi[1:]: pair[1] = '1' + pair[1] heapq.heappush(heap, [lo[0] + hi[0]] + lo[1:] + hi[1:]) return sorted(heap[0][1:], key=lambda p: (len(p[-1]), p)) freq = {'a': 45, 'b': 13, 'c': 12, 'd': 16, 'e': 9, 'f': 5} codes = huffman(freq) for char, code in codes: print(f"{char}: {code}")

这段代码用最小堆维护频率集合,每次弹出两个最小的合并。时间复杂度 O(n log n),n 是字符种类数。实测下来,对于几万种字符的语料,构建编码树也就毫秒级。

这里有个实操心得:霍夫曼树不是唯一的。当两个节点频率相同时,谁左谁右、谁先合并,可能产生不同的树结构,但它们的加权路径长度(也就是编码总长度)是相同的。所以如果你在对比不同实现的输出,发现编码不一样但总长度一样,不要慌,这是正常的。

3. 贪心失效的四种典型场景与反例构造方法

前面一直在讲贪心怎么用,但真正拉开水平差距的,是知道它什么时候不能用。我总结了四种最常见的失效场景,每一种都配一个反例,你可以在面试或做题时快速对照。

3.1 局部最优的"短视"陷阱:硬币找零问题

硬币找零是贪心最经典的翻车现场。假设你有面值1、5、11的硬币,要找零15元,求最少硬币数。贪心策略是每次选不超过剩余金额的最大面值:先选11,剩4,再选4个1,总共5枚。但最优解是选3个5,只要3枚。贪心在这里选了"当前最大"的11,反而把自己逼进了死角。

这个反例的关键在于:贪心选择11之后,剩余金额4只能用1元硬币填,而如果放弃11选5,后续可以用更少的硬币填满。也就是说,当前看起来"最大"的选择,破坏了后续选择的灵活性。

什么时候硬币问题贪心成立?当硬币面值系统是"规范"的,比如1、5、10、25、50这种,每个面值都是前一个的倍数或满足特定整除关系。人民币和美元的面值系统恰好满足这个性质,所以日常找零贪心没问题。但一旦面值系统变得奇怪,贪心就可能失效。

注意:在实际工程中遇到找零类问题,如果面值系统不确定,优先考虑动态规划,不要赌贪心。动态规划虽然时间复杂度高一点(O(amount * n)),但保证正确。

3.2 决策顺序影响结果的场景:任务调度带截止时间和惩罚

假设有一批任务,每个任务需要1个单位时间完成,有截止时间和超时惩罚。如果所有任务的惩罚相同,贪心按截止时间排序就能最小化超时任务数。但如果惩罚不同,比如任务A惩罚100截止时间2,任务B惩罚1截止时间1,任务C惩罚1截止时间2,贪心按截止时间排可能先做B和C,导致A超时罚100;而最优解是先做A,虽然B或C会超时,但只罚1。

这个场景的教训是:当目标函数涉及不同权重的惩罚时,单纯的截止时间排序不再充分,需要引入更复杂的策略(比如带权重的调度,通常用优先队列动态调整)。贪心的"一步定终身"在这里需要更精细的设计。

3.3 需要全局视野的图问题:最短路径中的负权边

单源最短路径的Dijkstra算法本质上是一种贪心:每次从优先队列里取出当前距离最小的节点,标记为已确定,然后松弛它的邻居。这个贪心在所有边权非负时是正确的。但一旦存在负权边,Dijkstra就会失效。

反例:节点A到B距离1,A到C距离2,C到B距离-2。Dijkstra先确定B(距离1),然后处理C(距离2),发现C到B是-2,可以更新B的距离为0,但B已经被标记为"已确定",不再更新。最终B的距离停留在1,而实际最短是0。

这个反例说明:贪心的"不再回头"特性,在存在负权边这种"后面可能推翻前面"的情况下会出错。负权边意味着你后面发现的路径可能比前面确定的更短,而贪心不允许修改已确定的决策。这时候需要用Bellman-Ford这类允许反复松弛的算法。

3.4 构造反例的通用套路

上面这些反例不是背下来的,而是有一套构造方法。我总结了一个三步套路:

  1. 找"诱惑点":贪心会在哪一步被一个看起来很好的选择吸引?通常是当前最大、最小、最早、最晚的那个。
  2. 让诱惑点产生"后遗症":这个选择会消耗掉什么资源、堵死什么后续可能性?比如消耗了容量、占用了时间窗口、切断了连通性。
  3. 构造一个"绕路更优"的替代方案:放弃诱惑点,选一个当前看起来稍差但后续更灵活的选择,最终总收益更高。

用这个套路,你可以自己给任何贪心策略构造反例。我平时做题时,如果怀疑贪心有问题,就会花两分钟按这个套路试一下,往往能快速验证。

失效场景贪心策略反例核心正确解法
硬币找零每次选最大面值面值11导致剩余4只能用1填动态规划
带权调度按截止时间排序高惩罚任务被低惩罚任务挤掉优先队列动态调整
负权最短路Dijkstra贪心确定负权边推翻已确定距离Bellman-Ford
0-1背包按单位价值排序高单位价值物品占满容量动态规划

4. 工程实战:用贪心优化日志合并与内存分配

贪心不只是刷题用的,在实际工程里它经常以"启发式策略"的形式出现。我拿两个真实场景来讲,一个是日志合并,一个是内存分配,都是我在项目里踩过坑的。

4.1 日志合并:按时间戳贪心归并的边界处理

假设你有一个分布式系统,每个节点产生一份按时间戳有序的日志文件,现在要把它们合并成一份全局有序的日志。最直接的做法是多路归并:维护一个最小堆,堆里放每个文件当前未读取的第一条日志,每次弹出时间戳最小的那条,输出后从对应文件读下一条压入堆。

这个策略本质上就是贪心:每一步都选当前所有文件里时间戳最小的那条。它为什么正确?因为每个文件内部已经有序,全局最小的日志一定在某个文件的开头,选它不会错过任何更小的。这个贪心选择性质是成立的。

但实操中有几个坑:

  • 时间戳精度不一致:不同节点的时钟可能有毫秒级偏差,甚至有的用秒有的用毫秒。如果直接比较,会出现"后发生的日志被排到前面"。我的做法是统一转换成毫秒时间戳,并且对于同一毫秒内的日志,用一个稳定的次级排序键(比如节点ID+序列号)保证顺序确定。
  • 文件句柄耗尽:如果文件数量上万,同时打开所有文件会耗尽句柄。这时候要分批处理,或者用内存映射的方式按需读取。
  • 空文件与读取失败:堆里压入之前要检查文件是否已读完,读完后不要再压入,否则会死循环。
import heapq def merge_logs(file_paths): heap = [] files = [open(p, 'r') for p in file_paths] for idx, f in enumerate(files): line = f.readline() if line: # (timestamp, file_idx, line) ts = int(line.split()[0]) heapq.heappush(heap, (ts, idx, line)) while heap: ts, idx, line = heapq.heappop(heap) yield line next_line = files[idx].readline() if next_line: next_ts = int(next_line.split()[0]) heapq.heappush(heap, (next_ts, idx, next_line)) for f in files: f.close()

这个实现的时间复杂度是 O(N log K),N 是总日志条数,K 是文件数。实测下来,合并100个文件、总共1000万条日志,大概十几秒,内存占用也很低,因为堆里最多只有K条。

4.2 内存分配:首次适应与最佳适应的取舍

操作系统里的内存分配器经常用贪心策略。最简单的两种:

  • 首次适应(First Fit):从头扫描空闲块链表,找到第一个足够大的块就分配。
  • 最佳适应(Best Fit):扫描所有空闲块,找最小的那个足够大的块分配。

这两个都是贪心,但效果不同。首次适应速度快,但容易在低地址产生大量小碎片;最佳适应看起来更"聪明",但会产生大量难以利用的微小碎片(因为总是把大块切得只剩一点点)。

我在一个嵌入式项目里实测过:对于频繁分配释放小块内存的场景,首次适应的碎片率反而比最佳适应低,因为最佳适应总是优先消耗那些"刚好够大"的块,导致大块被保留但小块被切碎。这个结论和很多教科书说的"最佳适应最优"是相反的,但实际数据就是这样。

后来我们改用了分离适配:把空闲块按大小分类,每类维护一个链表,分配时先从最匹配的大小类找,找不到再往更大的类找。这其实是贪心思想的一种工程化改进——不是全局找最优,而是在一个受控的范围内找足够好的。

提示:工程中的贪心往往不是追求数学最优,而是追求足够好且足够快。在内存分配这种高频操作里,O(n) 的全局扫描是不可接受的,所以必须用空间换时间,用分类结构把贪心选择限制在局部。

4.3 从工程视角看贪心的"够用就好"哲学

这两个案例让我对贪心有了新的认识:它在工程中的价值,往往不在于证明它是最优的,而在于它足够简单、足够快,而且在实际数据分布下表现良好。日志合并的贪心是严格最优的,内存分配的贪心是启发式的,但它们都解决了实际问题。

如果你在工程中遇到一个优化问题,先问自己三个问题:

  1. 问题的规模有多大?如果数据量小,动态规划或暴力搜索可能更简单可靠。
  2. 对最优性的要求有多高?如果差几个百分点可以接受,贪心往往是性价比最高的选择。
  3. 数据分布有没有规律?如果数据本身有偏斜(比如大部分日志时间戳集中),贪心的实际表现可能远好于理论最坏情况。

5. 贪心与动态规划的抉择:一张决策表与三个判断信号

很多人卡在"这题到底用贪心还是动态规划"上。我总结了一张决策表,配合三个判断信号,基本能覆盖大部分场景。

5.1 决策表:从问题特征反推算法选择

问题特征贪心动态规划
决策是否可回头不可回头,一步定保留所有子问题解
是否可分割可分割时常用离散选择时常用
子问题是否重叠通常不重叠大量重叠
是否需要全局比较局部最优即全局需要比较多种组合
时间复杂度通常 O(n log n)通常 O(n^2) 或更高
实现难度简单较复杂
正确性证明需要贪心选择性质需要最优子结构+无后效性

这张表不是绝对的,但能帮你快速定位。比如你发现子问题大量重叠,那基本就是动态规划的信号;如果每一步选择后问题规模单调缩小且不回头,贪心的可能性就很大。

5.2 三个判断信号:快速识别该用哪个

信号一:能不能构造出"绕路更优"的反例?如果你花几分钟就能构造出一个反例,说明贪心不成立,直接上动态规划。如果怎么构造都构造不出来,贪心可能成立,但还需要进一步验证。

信号二:问题是否要求"恰好"满足某个约束?比如"恰好装满背包"、"恰好用完所有资源",这种精确约束往往需要动态规划来枚举所有可能的组合。贪心更适合"不超过"、"最多"这类宽松约束。

信号三:决策之间是否有依赖关系?如果后面的决策依赖于前面决策的具体结果(比如前面选了什么会影响后面能选什么),那贪心容易失效,因为贪心不记录历史。动态规划通过状态转移天然处理这种依赖。

5.3 一个混合案例:带截止时间的任务调度

我拿一个混合案例来演示怎么在实际中做选择。问题是:有n个任务,每个任务有截止时间和收益,每个任务需要1单位时间,求最大收益。

如果所有任务收益相同,贪心按截止时间排序,用优先队列维护当前已选任务,遇到超时就丢弃收益最小的(其实收益相同就随便丢一个),这是最优的。

如果收益不同,贪心策略变成:按截止时间排序,用最小堆维护已选任务的收益,当发现当前任务会导致超时时,比较当前任务收益和堆顶收益,如果当前更大就替换。这个策略其实是贪心+堆的结合,本质上是在每一步做局部最优的替换决策。

但这个策略也不是万能的。如果任务需要的时间不同(有的需要2单位时间),问题就变成了带权区间调度,贪心就搞不定了,需要动态规划或者更复杂的算法。

import heapq def max_profit_schedule(tasks): # tasks: list of (deadline, profit), each takes 1 unit time tasks.sort(key=lambda x: x[0]) # sort by deadline heap = [] # min-heap of profits for deadline, profit in tasks: heapq.heappush(heap, profit) if len(heap) > deadline: heapq.heappop(heap) # remove smallest profit return sum(heap)

这段代码的核心逻辑是:始终维护一个"当前已选任务集合",当集合大小超过当前截止时间时,说明有任务必须被放弃,放弃收益最小的那个。这个贪心策略在任务等长时是最优的,因为放弃收益最小的任务,对总收益的损失最小。

实测下来,这个算法在几千个任务的规模下跑得飞快,而且代码简洁,比动态规划版本好维护得多。但如果任务时长不等,这个策略就会出错,必须换算法。

6. 贪心算法的代码落地细节与性能调优经验

最后这部分,我聊一些代码层面的实操细节。这些是文档里不会写、但实际写代码时一定会遇到的问题。

6.1 排序键的选择与稳定性

贪心算法十有八九要先排序,而排序键的选择直接决定正确性。Python的sort是稳定排序,这意味着如果两个元素排序键相同,它们会保持原来的相对顺序。这个特性有时候能帮你,有时候会坑你。

比如区间调度问题,如果两个区间结束时间相同,稳定排序会保持它们在输入中的顺序。这通常没问题,因为结束时间相同的区间,选哪个对后续影响一样。但如果你需要特定的次级排序(比如结束时间相同选开始时间更晚的),就要显式指定多级排序键:

intervals.sort(key=lambda x: (x[1], -x[0])) # 结束时间升序,开始时间降序

这个细节在面试中经常被忽略,但实际写代码时,多级排序键能让你的贪心策略更鲁棒。

6.2 优先队列的懒删除技巧

贪心算法经常用优先队列(堆)来维护候选集合。但堆不支持删除任意元素,只支持弹出堆顶。如果你需要"删除某个特定元素",常用的技巧是懒删除:不真正删除,而是用一个额外的集合记录已删除的元素,每次弹出堆顶时检查它是否在删除集合里,如果在就丢弃并继续弹。

这个技巧在带截止时间的调度问题里特别有用。比如你需要维护"当前已选任务",当要替换时,不是真的从堆里删除,而是标记删除,等它浮到堆顶时再丢弃。

import heapq def lazy_delete_example(): heap = [] deleted = set() # 添加元素 heapq.heappush(heap, (5, 'task_a')) heapq.heappush(heap, (3, 'task_b')) # 标记删除 task_b deleted.add('task_b') # 弹出时检查 while heap: val, name = heapq.heappop(heap) if name in deleted: continue # 懒删除,跳过 print(f"有效元素: {name}, 值: {val}")

懒删除的代价是堆里可能积累大量已删除元素,导致内存占用偏高。如果删除操作非常频繁,可以考虑定期重建堆,把已删除元素真正清理掉。

6.3 贪心算法的测试策略:如何验证正确性

贪心算法最难的不是写,而是验证。因为你很难穷举所有情况来证明它正确。我的测试策略分三层:

第一层:小规模暴力对拍。写一个暴力搜索版本(枚举所有可能的选择组合),对于小规模输入(比如n<=10),随机生成大量测试数据,对比贪心结果和暴力结果。如果发现不一致,就找到了反例。

第二层:边界条件测试。空输入、单个元素、所有元素相同、极端值(最大最小)、已排序和逆序输入,这些都要覆盖。贪心算法在边界上特别容易出问题,比如空输入时堆操作崩溃、单个元素时循环不执行等。

第三层:性能压测。构造大规模数据(比如百万级),测试运行时间和内存占用。贪心算法虽然理论复杂度低,但常数因子可能很大(比如排序的常数),实际性能需要验证。

import random def brute_force_intervals(intervals): # 暴力枚举所有子集,找最大不重叠区间数 n = len(intervals) best = 0 for mask in range(1 << n): selected = [intervals[i] for i in range(n) if mask & (1 << i)] selected.sort() ok = True for i in range(1, len(selected)): if selected[i][0] < selected[i-1][1]: ok = False break if ok: best = max(best, len(selected)) return best def test_greedy(): for _ in range(1000): n = random.randint(1, 8) intervals = [] for _ in range(n): s = random.randint(0, 20) e = random.randint(s+1, 25) intervals.append((s, e)) g = max_meetings(intervals) b = brute_force_intervals(intervals) if g != b: print(f"反例找到: {intervals}, 贪心={g}, 暴力={b}") return print("1000组随机测试全部通过") test_greedy()

这个对拍脚本我强烈建议你在写完任何贪心算法后都跑一遍。1000组随机测试通常几秒钟就能跑完,但能帮你发现绝大多数逻辑错误。

6.4 从面试到生产:贪心代码的可读性优化

面试时写贪心,追求的是快速正确;生产环境写贪心,追求的是可读可维护。我见过太多生产代码里的贪心逻辑写得像天书,后人根本不敢改。几个优化建议:

  • 把排序键提取成命名函数,而不是内联lambda。比如def sort_key(interval): return interval[1],这样别人一看就知道按什么排序。
  • 把贪心选择的判断逻辑单独抽成一个函数,比如def should_select(current, candidate): ...,把"为什么这样选"的注释写在这里。
  • 在关键决策点加日志,记录贪心选择了什么、放弃了什么。生产环境出问题时,这些日志是排查的关键。
  • 写清楚贪心策略的假设,比如"假设所有任务等长"、"假设边权非负"。一旦假设被打破,代码就需要重新审视。

我在一个项目里维护过一段贪心调度代码,最初写得很紧凑,后来需求变化,任务时长变得不等,那段代码直接失效。如果当初写清楚了"假设任务等长"这个前提,后人改需求时就会意识到需要重新设计算法,而不是在原有代码上打补丁。

贪心算法说到底是一种思维方式:在信息不完全、时间有限的情况下,做出当前看起来最好的选择,并承担这个选择带来的后果。它不追求完美,但追求在约束下尽可能好。这种思维方式,其实比算法本身更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询