最长数对链 图解题解
这道题到底在问什么
- 输入
- pairs=[[1,2],[2,3],[3,4]]
- 输出
- 2 ([1,2] 接 [3,4],2 不能接 [2,3] 因为 2 不小于 2)
- 输入
- pairs=[[1,2],[7,8],[4,5]]
- 输出
- 3 ([1,2] 接 [4,5] 接 [7,8])
最优解:为什么这么做
一句话答案:LeetCode 646 最长数对链:把数对按右端点从小到大排序,再一遍扫描,左端点严格大于当前链尾 end 就接上、否则跳过。链尾右端点尽量小才给后面留空间,贪心即最优,时间 O(n log n)、空间 O(1)。
最长数对链这道题到底在接什么
给一个数对数组 pairs,每个数对是 [left, right] 且 left 小于 right。规则是:只要前一个数对的 right 严格小于后一个的 left,前一个就能接到后一个前面。要挑出尽量多的数对接成一条链,返回这条链最长能有多长。数对可以跳着挑、不必按原顺序,比如 pairs=[[1,2],[7,8],[4,5]],[1,2] 接 [4,5] 再接 [7,8],链长 3。
为什么把所有接法都列一遍会爆
最直接的想法是把所有能接成的链都摆出来,挑最长的。可每个数对选或不选、还要考虑接的先后,组合随数对个数指数级膨胀,数对一多就列不完。不去枚举整条链,改成从左到右扫一遍,边扫边用贪心(每一步只做当下最划算的选择、定了不反悔)决定每个数对接不接。
为什么按右端点排序而不是左端点
先把所有数对按右端点从小到大排好,再从左往右扫。为什么盯右端点?一条链还能接多长,卡在链尾的右端点上——链尾右端点越小,后面数对的左端点越容易越过它,留给后面的空间就越大。所以每次都优先接右端点小的数对,让链尾停得尽量靠前。若改按左端点排,可能先接了一个左端点小、右端点却很大的数对,把后面一大片空间堵死,反而漏掉更优的接法。
扫描时凭一个 end 门槛怎么决定接不接
排好序后用一个变量 end 记住当前链尾的右端点,初值设成一个极小的数,链长 ans 从 0 起。挨个看每个数对:它的左端点只要严格大于 end,就说明能接到链尾后面,把 ans 加一、并把 end 换成这个数对的右端点,门槛随之抬高;左端点没超过 end 就跳过它。这里必须是严格大于,题目要求前一个 right 严格小于后一个 left,两端相等算重叠、接不上。跳过的数对也不亏:它的右端点不比当前 end 小,留着它只会让链尾更靠后、更挤,丢掉不影响后面。
拿题面的三个数对亲手接一遍
拿题面的 pairs=[[1,2],[7,8],[4,5]] 走一遍。先按右端点排序,三个右端点分别是 2、8、5,排完是 [1,2]、[4,5]、[7,8]。end 起手是极小值、ans=0。第一个 [1,2],左端点 1 远大于 end,接上,ans 变 1,end 更新成 2。第二个 [4,5],左端点 4 严格大于 2,接上,ans 变 2,end 更新成 5。第三个 [7,8],左端点 7 严格大于 5,接上,ans 变 3,end 更新成 8。三个都接进来了,答案 3,和题面输出对上。
复杂度是多少,end 初值和相等两个坑
开销集中在排序,O(n log n)(大 O 记号描述规模变大时操作数怎么涨),n 是数对个数;排完只扫一遍是 O(n),加起来仍由排序主导。只用了 end、ans 两个变量,空间 O(1)。三个边界最容易踩坑:一是 end 初值要设成足够小的负数,数对可能带负数,设成 0 会让第一个数对接不进来;二是判断写成左端点大于等于 end 就错了,相等的两端接上会重叠,必须严格大于;三是只有一个数对时直接返回 1,它自己就算一条链。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这套「按右端点排序,左端点能越过 end 就接、否则跳」,下面每一帧都在套它。
- 4先看原始 8 个数对,右端点是乱的:[1,2]、[7,8]、[4,5]、[2,3]、[5,6]、[3,4]、[6,7]、[1,9]。贪心的前提是把它们按右端点排好。
- 5按右端点从小到大排好:[1,2]、[2,3]、[3,4]、[4,5]、[5,6]、[6,7]、[7,8]、[1,9]。这条轴上每个数字就是对应数对的右端点,左端点写在讲解里。现在从左往右贪心扫。
- 6开局:还没接任何数对,把 end 设成极小(这样第一个数对一定能接进来),链长 ans=0。从最左边那个数对开始看。
- 7轮到数对 [1,2](紫色,右端点 2)。它能不能接到链尾,只看它的左端点 1 是否严格大于当前 end = 负无穷。绿色是已接入的,灰色是已跳过的。
- 81 严格大于 end,能接!把 [1,2] 接进链(变绿),链长涨到 1,并把 end 更新成它的右端点 2(下一个数对要越过的就是这个新门槛)。
- 9轮到数对 [2,3](紫色,右端点 3)。它能不能接到链尾,只看它的左端点 2 是否严格大于当前 end = 2。绿色是已接入的,灰色是已跳过的。
- 102 没有严格大于 end = 2,接上去会和链尾重叠(红色),跳过 [2,3]。注意:它的右端点不比当前 end 小,留着它只会更挤,丢掉不亏。end 保持 2。
- 11轮到数对 [3,4](紫色,右端点 4)。它能不能接到链尾,只看它的左端点 3 是否严格大于当前 end = 2。绿色是已接入的,灰色是已跳过的。
- 123 严格大于 end,能接!把 [3,4] 接进链(变绿),链长涨到 2,并把 end 更新成它的右端点 4(下一个数对要越过的就是这个新门槛)。
- 13轮到数对 [4,5](紫色,右端点 5)。它能不能接到链尾,只看它的左端点 4 是否严格大于当前 end = 4。绿色是已接入的,灰色是已跳过的。
- 144 没有严格大于 end = 4,接上去会和链尾重叠(红色),跳过 [4,5]。注意:它的右端点不比当前 end 小,留着它只会更挤,丢掉不亏。end 保持 4。
- 15轮到数对 [5,6](紫色,右端点 6)。它能不能接到链尾,只看它的左端点 5 是否严格大于当前 end = 4。绿色是已接入的,灰色是已跳过的。
- 165 严格大于 end,能接!把 [5,6] 接进链(变绿),链长涨到 3,并把 end 更新成它的右端点 6(下一个数对要越过的就是这个新门槛)。
- 17轮到数对 [6,7](紫色,右端点 7)。它能不能接到链尾,只看它的左端点 6 是否严格大于当前 end = 6。绿色是已接入的,灰色是已跳过的。
- 186 没有严格大于 end = 6,接上去会和链尾重叠(红色),跳过 [6,7]。注意:它的右端点不比当前 end 小,留着它只会更挤,丢掉不亏。end 保持 6。
- 19轮到数对 [7,8](紫色,右端点 8)。它能不能接到链尾,只看它的左端点 7 是否严格大于当前 end = 6。绿色是已接入的,灰色是已跳过的。
- 207 严格大于 end,能接!把 [7,8] 接进链(变绿),链长涨到 4,并把 end 更新成它的右端点 8(下一个数对要越过的就是这个新门槛)。
- 21轮到数对 [1,9](紫色,右端点 9)。它能不能接到链尾,只看它的左端点 1 是否严格大于当前 end = 8。绿色是已接入的,灰色是已跳过的。
- 221 没有严格大于 end = 8,接上去会和链尾重叠(红色),跳过 [1,9]。注意:它的右端点不比当前 end 小,留着它只会更挤,丢掉不亏。end 保持 8。
- 23扫完全部 8 个数对,绿色这 4 个就是接成的最长链:[1,2] → [3,4] → [5,6] → [7,8]。灰掉的都是接不进来的。答案 4。
⚠️ 容易写错的地方
✗ 错:按左端点排序
✓ 对:按右端点排序
贪心要让链尾右端点尽量小,才给后面留最大空间;按左端点排会漏最优解
✗ 错:把「可接」写成 a ≥ end
✓ 对:a > end(严格)
题目要求前一个 right 严格小于后一个 left,相等不能接
✗ 错:end 初值设为 0
✓ 对:设成极小值
数对可能含负数,初值不够小会让第一个数对接不进来
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def findLongestChain(self, pairs: List[List[int]]) -> int:
pairs.sort(key=lambda x: x[1])
ans = 0
end = -10**18
for a, b in pairs:
if a > end:
ans += 1
end = b
return ansC++
#include <algorithm>
#include <climits>
#include <vector>
using namespace std;
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(), pairs.end(), [](auto& a, auto& b){ return a[1] < b[1]; });
int ans = 0, end = INT_MIN;
for (auto &p : pairs) if (p[0] > end) { ans++; end = p[1]; }
return ans;
}
};Java
import java.util.*;
class Solution {
public int findLongestChain(int[][] pairs) {
Arrays.sort(pairs, Comparator.comparingInt(a -> a[1]));
int ans = 0, end = Integer.MIN_VALUE;
for (int[] p : pairs) if (p[0] > end) { ans++; end = p[1]; }
return ans;
}
}复杂度
时间
O(n log n)
排序占主导,之后扫描只 O(n)
空间
O(1)
只用 ans、end 两个变量(排序原地)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长数对链 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题也能用动态规划,和贪心比该选哪个?+
能。动态规划的思路是按左端点排序后求最长上升子序列(LIS,最长的逐个变大的子序列)——把每个数对前面能接上的前驱都看一遍、取最长,转移是 O(n²),配上二分能压到 O(n log n)。贪心这边按右端点排、一遍扫,同样 O(n log n),但代码更短、常数更小,是这题的首选。面试里两种都能讲清楚更稳。
它和无重叠区间 LeetCode 435 是一类题吗?+
是同一套按右端点排序的贪心。LeetCode 435 求最少删几个区间让剩下的互不重叠,等价于最多保留多少个互不重叠的区间,也是按右端点排、能不重叠就留下。本题的『前一个 right 严格小于后一个 left 才能接』对应那题的『区间不重叠』,把不等号的口径对齐,两题就能互相转化。
为什么按右端点排序一定得到最长链,不会漏掉更优的接法?+
关键在链尾右端点:每一步都接右端点最小、又能接上的数对,链尾就停得尽量靠前,给后面留的空间最大,不会因这一步把后面堵死。反过来想,假设存在一条更长的最优链,把它也按右端点排好,逐位和贪心选的对比,贪心每一位的链尾右端点都不会比最优链的更大,所以贪心往后能接的数对不会更少,长度至少追平最优链。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长数对链 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。