
1. 先看题目一个被低估的“送分题”LeetCode 1266题目名很直白访问所有点的最小时间。给定平面上 n 个点按数组给出的顺序依次访问从第一个点出发每秒钟可以沿水平、竖直或者对角线方向移动一格问最少需要多少秒走完全程。先别急着觉得“这不就是模拟吗”这个题最大的坑在于移动规则。很多第一次刷到的朋友会下意识套用“欧几里得距离”或者“曼哈顿距离”的思路结果发现样例都对不上或者自己推导出来的公式和答案差了一点。原因很简单题目允许斜向移动而且是“每秒钟可以沿水平、竖直或对角线方向移动一格”。这一个条件直接改变了距离的定义。这道题在 LeetCode 上被标记为“简单”但我个人觉得它更适合作为“贪心思维的入门热身题”。它不涉及复杂的数据结构不需要动态规划也没有抽象的排序策略核心就是一个朴素的观察从点 A 走到点 B斜着走永远比绕直角走更省时间所以每一步的最优决策都是独立的局部最优就是全局最优。这不正是贪心算法最原始、最干净的样子吗这篇文章适合三类人看一是刚刷完数组、字符串、链表想开始接触贪心算法但被“区间调度”“跳跃游戏”吓得不敢动的新手二是面试前想快速过一遍“简单题”里藏着的数学原理的老手三是平时写代码习惯了 for 循环嵌套想看看“一行版”到底是怎么压缩出来的朋友。先说结论相邻两点之间的最短时间等于两点横坐标之差的绝对值和纵坐标之差的绝对值中的较大者也就是max(|x1 - x2|, |y1 - y2|)。把所有相邻点的这个值加起来就是答案。这个结论看起来简单到像一个脑筋急转弯但它背后藏着“为什么斜着走能同步缩短两个方向的距离”这个核心问题。下面我一步步把这个结论从直觉到数学到代码全部掰开讲清楚。2. 移动规则决定的不是“路程”而是“距离公式”2.1 三种距离欧式、曼哈顿、切比雪夫要真正理解这道题必须先搞清楚一个底层概念平面上两点之间的距离不是唯一的。我们初中几何学的两点的距离是欧几里得距离也就是直线距离公式是sqrt(dx² dy²)。但在实际场景里距离的定义取决于你“怎么走”。举个很直观的例子你在一个方格城市里街道都是横平竖直的从 A 到 B 不能穿楼只能沿着街道走。这时候你走的距离就不是直线距离而是横向要走的格子数加上纵向要走的格子数|dx| |dy|这个叫曼哈顿距离。名字来源于纽约曼哈顿的街区布局。但在 LeetCode 1266 里移动规则变成了“可以斜着走”而且是同时向水平和对角线方向移动每移动一格都算 1 秒。这其实就是第三种距离——“切比雪夫距离”公式是max(|dx|, |dy|)。为什么叫切比雪夫距离因为在国际象棋里国王King每次只能走一格但可以向周围 8 个方向任意移动。国王从棋盘一格走到另一格需要的最少步数恰好就是max(|dx|, |dy|)。这道题里的移动方式和国际象棋国王一模一样。2.2 为什么斜着走能“同时”缩短两个方向这是整道题的核心直觉也是最容易讲不清的地方。假设从(0, 0)出发要去(5, 3)。如果你只能水平向右、竖直向上走曼哈顿规则那你需要先走 5 步到(5, 0)再走 3 步到(5, 3)一共 8 步。但如果你允许斜着走斜着走一步的同时既向右了一格又向上一格。你先斜着走 3 步到达(3, 3)——注意此刻横向需求还剩 2纵向需求已经清零。接下来再水平向右走 2 步到达(5, 3)。总步数 3 2 5。这 5 步恰好就是max(5, 3) 5。你发现规律没有斜着走是用来“消掉”横纵距离中较小的那一部分。两个方向的距离差多少斜着走能覆盖多少最后剩下的那个差值就是你必须直着走的距离。所以总步数永远是“两个距离中的较大者”——因为较小的那部分被斜着走“顺路”解决了。这个直觉一旦建立整个题目就豁然开朗了。2.3 一个反直觉的验证不是加和是取大为了让你彻底信服再看两组极端数据。第一组(0, 0)到(100, 100)。如果按曼哈顿距离算要 200 秒按切比雪夫距离算只要 100 秒。实际情况也是 100 秒——你一直斜着走就完了每一步都同时横纵各进一格100 步之后刚好到达。两个方向的需求永远相等所以一条对角线走到黑。第二组(0, 0)到(100, 0)。横向距离 100纵向距离 0取大结果是 100 秒。这个很好理解纵向没有需求你只需要水平走 100 格。但注意就算你中途偶尔斜着走再走回来也不会更优因为斜着走会产生“多余的纵向位移”最后还要花额外的步数修正。所以两点间可达的最小时间就是两个方向绝对差的最大值。它既不是dx dy也不是sqrt(dx² dy²)而是max(|dx|, |dy|)。一句话总结在八个方向的移动模型下时间被“短板”限制但决定速度的是“长板”。3. 为什么能“贪”相邻点独立最优3.1 题目顺序是固定的但它仍然是一个贪心问题有一个常见的疑问题目给的点的顺序是固定的我必须从points[0]到points[1]再从points[1]到points[2]……既然顺序都定死了还有什么贪心不贪心这是一个好问题。很多人理解的贪心是“从一堆选项里挑最优”而这道题看起来没有任何选择。但实际上即使路线必须按顺序走每一步的移动方式仍然存在选择空间。比如从(0, 0)到(5, 3)你可以先斜走 3 步再横走 2 步也可以先横走 1 步再斜走 3 步再横走 1 步甚至可以先竖着多走几步绕个路。这些不同的移动策略耗时完全不同。贪心在这里表现为每一段路程都选择耗时最少的移动方式不回头、不绕路、不“为后续省力”而牺牲当前步。而为什么这种局部最优能成立因为段与段之间完全独立——你在points[i]到points[i1]怎么移动不会影响你到达points[i1]时的位置和状态。到达点是确定的所以上一段的结束状态就是下一段的起始状态不存在“先多走一段路能让我接下来更快”的可能性。这和经典的贪心问题——比如“用最少的硬币凑出金额”——有本质区别。硬币问题里你如果先用大面额硬币可能影响后续能否凑整。但 1266 里不存在这种耦合。每段路程只有起点和终点这两个约束中间怎么走完全自由所以每段取最优总和必然最优。3.2 如果题目允许“自由选择访问顺序”这里做个延伸思考这也是面试官很喜欢追问的变体如果题目改成“可以以任意顺序访问所有点求最短时间”那还能不能贪心答案是不能了。因为这时候问题变成“给定一组点求一条经过所有点的最短路径”本质上是一个旅行商问题TSP的变体。在二维平面上虽然有些启发式算法但不存在多项式时间的精确解复杂度随点数指数增长。这个变体和原题的差距就像“按顺序走完一条街”和“在市区里规划一条最短路线把所有快递送完”的差距。前者是数学题后者是运筹学难题。3.3 贪心的边界局部最优与全局最优的一致性验证我们还可以用一个更严谨的方式来验证“每段最优带来全局最优”的正确性。设总时间T等于所有相邻点距离之和T sum(d(points[i], points[i1]))其中d是我们定义的切比雪夫距离。因为每段d都是该段的最小值任何其他移动策略都会让该段时间 d。总时间是各段时间之和每一项都至少是d所以任何策略的总时间至少是sum(d)。而我们构造一个策略刚好达到这个下界——每段都按切比雪夫距离的最短方式走——所以这个下界就是最小值。这就是贪心算法里最标准也最令人舒服的证明结构先证明一个下界再构造达到下界的方案。一旦两步都成立你就知道答案不可能更小了。这个证明过程我觉得比题目本身更重要因为很多贪心题的正确性证明套路都是这样。4. 代码实现从暴力模拟到 Python 一行版4.1 基础实现双层循环求和先写出最容易理解的版本也方便验证公式的正确性class Solution: def minTimeToVisitAllPoints(self, points: List[List[int]]) - int: ans 0 for i in range(len(points) - 1): x1, y1 points[i] x2, y2 points[i 1] ans max(abs(x1 - x2), abs(y1 - y2)) return ans这段代码的逻辑非常简单遍历相邻点对计算横纵坐标差的绝对值取较大值累加。时间复杂度O(n)空间复杂度O(1)。你可能觉得这段代码太简单没有任何技巧。但恰恰是这种朴素实现最能体现对题目的理解程度。面试的时候先写出这种清晰版本再提优化或一行版观感远好于一上来就写一行表达式。4.2 一行版的拆解过程一行版的核心是 Python 的zip和生成器表达式配合sumclass Solution: def minTimeToVisitAllPoints(self, points: List[List[int]]) - int: return sum(max(abs(p1[0] - p2[0]), abs(p1[1] - p2[1])) for p1, p2 in zip(points, points[1:]))一行版做了三件事zip(points, points[1:])把点列表和它去掉第一个元素后的列表打包得到(points[0], points[1])、(points[1], points[2])……这一下子就完成了“相邻点对”的配对不需要像基础版那样用索引i和i 1去访问。生成器表达式for p1, p2 in zip(...)逐个点对计算max(abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))等价于基础版循环体里的表达式。sum(...)把所有点对的结果累加。这三个机制配合起来就把显式的ans 循环压缩成了函数式的一行。本质上做的事情和基础版完全一样只是语法层面的压缩。4.3 再进一步用zip解包的写法如果你想展示对解包语法的熟练度还可以这样写class Solution: def minTimeToVisitAllPoints(self, points: List[List[int]]) - int: return sum(max(abs(a - c), abs(b - d)) for (a, b), (c, d) in zip(points, points[1:]))这里(a, b)和(c, d)直接解包了前后两个点的横纵坐标表达式里就不需要再写p1[0]这种索引访问了。从可读性角度这种写法比p1[0] - p2[0]更清晰也更能体现 Python 解包语法的优雅。不过我得提醒一句一行版在 LeetCode 上能跑在实际工程项目里不建议这么写。zip(points, points[1:])会额外创建一个points[1:]的副本虽然只持有引用但仍然是 O(n) 的额外空间而且一行表达式对调试不友好。但作为刷题展示或者作为理解 Python 函数式编程的练习它确实很漂亮。4.4 三种代码的对比小结版本可读性空间复杂度适用场景基础版显式循环最高O(1)面试讲解、工程代码、教学一行版zip sum中等O(n)points[1:]副本刷题炫技、理解函数式写法解包一行版中等偏上O(n)想要兼顾优雅与可读性时如果你想兼顾效率和可读性又不想要points[1:]的副本可以用索引表达式return sum(max(abs(points[i][0] - points[i1][0]), abs(points[i][1] - points[i1][1])) for i in range(len(points) - 1))这段既不创建切片副本又保持了一行表达式的紧凑感是工程中更稳妥的折中方案。5. 边界情况、雷区与性能陷阱5.1 只有一个点或空数组按题目约束points的长度至少为 1。如果长度是 1循环体一次都不执行返回 0。这个逻辑基础版和一行版都天然成立不需要额外写if判断。但如果你在代码审查时看到有人加了if len(points) 1: return 0也不要觉得多余——它不影响正确性只是防御式编程的习惯。在面试里主动提一句“这个边界条件被循环结构天然覆盖”能体现你对边界的敏感度。5.2 坐标可为负题目没有限制坐标必须为正。比如(-7, -3)到(5, 2)计算abs(-7 - 5) 12abs(-3 - 2) 5结果是 12。绝对值的存在让负数坐标不影响正确性。这里唯一的风险是初学者可能会下意识用max(x1, x2) - min(x1, x2)来代替abs(x1 - x2)两种写法都正确但abs更清晰、更不容易出错。5.3 大坐标与溢出Python 的整数是任意精度的所以即使坐标到10^9级别也不会溢出。但如果你是 C 或 Java 选手int可能不够用取决于题目给的约束范围需要用long long。这属于语言层面的差异Python 选手不需要担心。5.4 常见错误误用曼哈顿距离这个错误出现的频率高到我单独拿出来说。很多人第一眼看到“水平、竖直、对角线都可以走”会觉得“那肯定比曼哈顿快”但写代码时还是手滑写成了abs(dx) abs(dy)。这会导致答案偏大。比如两点为(1, 1)到(4, 5)曼哈顿距离是3 4 7切比雪夫距离是max(3, 4) 4。一个点对就差 3 秒点多了差异就非常明显。怎么避免这个错误每次写完之后用最简单的用例自测points [[0, 0], [1, 1]]答案应该是 1斜着一步就到而不是 2。如果写成dx dy得到 2立刻就能发现。5.5 复杂度与 LeetCode 性能的“错觉”这个题的O(n)复杂度在 LeetCode 上跑起来大约是几十毫秒级别。就算点数到10^5也只要求一次遍历完全没有性能压力。但如果你用了points[1:]的一行版虽然是 O(n) 的时间和 O(n) 的空间实测也不会超时。这就是 LeetCode “简单题”的典型特征——即使你写出了次优版本测试数据量也远构不成威胁。但我要强调不要把“没超时”当成“算法最优”的理由。在面试场景中面试官看到一行版通常会追问空间复杂度这时候如果你能准确说出它比基础版多用了 O(n) 的切片空间并说明在什么场景下这个额外空间不可接受这反而是一个加分项。5.6 关于“贪心证明”的追问我遇到过不止一次面试官在候选人写完代码后追问“你凭什么说每段取最短时间总时间就一定最短万一后面某一段因为前面的走法而变长呢”正确回答的姿势是因为后一段的起点是固定的所以前一段的走法不会改变后一段的起点和终点也就不会影响后一段的最小时间。每一段是独立的决策没有耦合局部最优自然叠加成全局最优。如果面试官再深挖你可以补充如果我们把每个点对的切比雪夫距离定义为一个函数d(p, q)这个函数满足三角不等式吗答案是不一定满足固定版本下的“绕路反而更短”不存在因为d已经是两点之间的最短路径长度。三角不等式在这里体现为d(a, c) d(a, b) d(b, c)——但那是另一个问题的分析工具在这个按顺序访问的设定下用不上。6. 从 1266 看开去一道简单题背后的三个通用能力6.1 识别“距离定义”的能力很多算法题的成绩差距不在于你会不会写代码而在于你能不能识别出题人在用哪种“距离模型”。LeetCode 1266 考的是切比雪夫距离换一个背景它可以变成“机器人网格寻路最短步数”曼哈顿距离再换一个背景可以是“二维坐标下救援船的最短时间”切比雪夫距离。我自己的一个习惯是拿到一题第一件事不是打开编辑器而是先问自己三个问题这个移动规则对应哪种距离公式点和点之间有没有耦合顺序是固定的还是自由选择的这三个问题回答完解法往往已经浮出水面了。6.2 数学推导的最小闭环定义 → 归纳 → 边界这一题里我们实际上做了三步完整的数学推导定义两点间的移动模型等价于切比雪夫距离max(|dx|, |dy|)归纳通过“斜着走能同步缩短两个方向”的直觉验证了取大而不是取和的合理性边界空数组、单点、负数坐标、大数都在公式和代码中被自然覆盖。这个“最小闭环”值得刻在脑子里。很多刷题的人看到一个题直接开始写代码结果代码逻辑在常见用例上没问题一到边界就崩。原因就是没有先做数学层面的“边界扫描”。6.3 “暴力枚举 推导公式 数学构造”的思维链路题目相关的热搜词里出现了“暴力枚举推导公式数学构造”这其实概括了一个更通用的解题套路非常适合用来复盘 1266暴力枚举如果我不知道切比雪夫距离公式我可以直接 BFS 模拟从一点到另一点的最短步数点对少时也能跑出答案。但 BFS 对每个点对都要做一遍复杂度是O(n * max_coord)点数一多就废了推导公式通过观察移动规则推导出max(|dx|, |dy|)直接 O(1) 计算每个点对数学构造上面的推导本质上是一个构造性证明——我们不仅知道答案是max还知道怎么走能走到先斜后直这保证了公式的现实可执行性。这三步对应了三种思维层次暴力求解保证正确性公式推导保证高效性构造证明保证可复现性。我觉得这一题的真正的价值不在于“你这么简单还分析这么久”而在于它把这三步压缩到了最小规模非常适合用来练习思维链路。6.4 后续扩展如果题目变成“曼哈顿规则”或“欧式规则”最后留一个思考题给你自己验证如果把移动规则改成只能上下左右走曼哈顿距离答案就变成每段|dx| |dy|的总和。如果再改成可以沿任意方向走任意距离但每单位距离耗时 1答案就变成每段欧几里得距离sqrt(dx² dy²)的总和而且这种模式下“斜向直接走直线”就是唯一最优解没有任何贪心选择的余地。三种移动模型三种距离公式同一道题三种答案。理解了这个1266 就算真正吃透了。