不相交的线 图解题解
这道题到底在问什么
- 输入
- nums1=[1,4,2], nums2=[1,2,4]
- 输出
- 2 (连 1-1 与 4-4,再连 2-2 就会和 4-4 交叉)
- 输入
- nums1=[1,2,4], nums2=[1,2,4]
- 输出
- 3 (三个数顺序完全一致,全连上)
最优解:为什么这么做
一句话答案:LeetCode 1035 不相交的线其实就是求两数组的最长公共子序列 LCS:连线不交叉等价于按原顺序对齐,填 dp[i][j] 表,相等取左上加一、不等取上左较大,时间和空间都 O(m×n)。
1035 到底要连出几条不相交的线
nums1 写上排、nums2 写下排,各按给定顺序。相等才准连一条线,且线不能交叉、每个数最多连一条,问最多连几条。题面 nums1=[1,4,2]、nums2=[1,2,4],连 1-1、4-4 得 2 条,再补 2-2 会跟 4-4 撞,答案是 2。
连线不能交叉,凭什么就是最长公共子序列
把「不交叉」翻译一下:1-1 连在 4-4 前面,就要求 1 在两排里都排在 4 前面——不交叉,等于对应的数在两排先后顺序一致。这正是从两数组各挑一串、保持原顺序且逐个相等,就是最长公共子序列 LCS(子序列=跳着挑、不必挨着,但保持原顺序)。连线数就等于 LCS 长度。
为什么不能把所有连法都枚举一遍
把所有连线方案摆出来数最大的,随两数组长度指数级膨胀试不完,且大量方案前半截重叠、被反复重算。把「nums1 前 i 个、nums2 前 j 个能连几条」各算一次存起复用,就是动态规划(DP,把『前 i 个、前 j 个能连几条』算一次存下、后面直接取)。
dp[i][j] 定成什么,相等和不等各怎么转移
定义 dp[i][j] 为「只看 nums1 前 i 个、nums2 前 j 个能连出的最大线数」,坐标 (i,j) 是 dp 表第 i 行第 j 列、从 0 数起;第 0 行第 0 列代表「有一排为空」、连不出线全填 0,这圈 0 是转移(转移=怎么从旁边算好的格子推出这一格)的地基。
填内部每格看 nums1 第 i 个和 nums2 第 j 个(代码写成 nums1[i-1]、nums2[j-1],下标从 0 起)相不相等。相等:能新连一条线,接「两排都退一格」的 dp[i-1][j-1] 加一,dp[i][j]=dp[i-1][j-1]+1。不等:连不了线,取上左较大,dp[i][j]=max(dp[i-1][j], dp[i][j-1])。
拿题面两个数组把整张表填出来
开一张 4×4 表,第 0 行第 0 列全 0,行头 nums1=1、4、2,列头 nums2=1、2、4。第一行对 1:遇 1 相等,左上 0 加一为 1;遇 2、4 不等取较大均为 1。第二行对 4:遇 1、2 不等均为 1;遇 4 相等,左上 1 加一为 2。第三行对 2:遇 1 不等为 1;遇 2 相等,左上 1 加一为 2;遇 4 不等取较大为 2。填到 dp[3][3]=2,即最多线数,与题面对上。
踩左上角加一的有三格——1-1、4-4、2-2,但 4-4 和 2-2 会交叉、只能二选一,最多仍是 2 条。
少留第 0 行第 0 列那圈 0,整张表为什么会算崩
两层循环填满 m×n 格,每格 O(1),时间 O(m×n)(大 O 记号,操作数随规模怎么涨);一张二维表,空间 O(m×n)。每格只用左上、上、左三个邻格,可压成一维滚动数组,空间降到 O(min(m,n))。
忘留第 0 行第 0 列那圈 0 最常见,相等时 dp[i-1][j-1] 会越到表外、整张表一路错。另一个坑是当成找连续子串——子序列可跳着挑、按子串会漏解。最后:两数组毫无公共值时整张表仍是 0,答案就是 0。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这套「相等取左上加一,不相等取上、左较大」,下面每填一格都在套它。
- 4先搭表。行头是 nums1=[1,4,2],列头是 nums2=[1,2,4],最上一行和最左一列代表「一边为空」,连不出线,全部填 0(蓝色)。接下来从左上往右下一格格填内部。
- 5填 [1,1] 这格:nums1 的 1 和 nums2 的 1 正好相等,可以新连一条线。看它的左上角那格(黄色),在它的基础上加一条。
- 6左上角是 0,加上新连的这一条 = 1。这格落子 1(绿色)。
- 7填 [1,2] 这格:nums1 的 1 和 nums2 的 2 不相等,连不了新线,只能从上面一格和左边一格(两个黄色)里挑大的继承过来。
- 8上面是 0,左边是 1,取较大 = 1。这格落子 1(绿色)。
- 9填 [1,3] 这格:nums1 的 1 和 nums2 的 4 不相等,连不了新线,只能从上面一格和左边一格(两个黄色)里挑大的继承过来。
- 10上面是 0,左边是 1,取较大 = 1。这格落子 1(绿色)。
- 11填 [2,1] 这格:nums1 的 4 和 nums2 的 1 不相等,连不了新线,只能从上面一格和左边一格(两个黄色)里挑大的继承过来。
- 12上面是 1,左边是 0,取较大 = 1。这格落子 1(绿色)。
- 13填 [2,2] 这格:nums1 的 4 和 nums2 的 2 不相等,连不了新线,只能从上面一格和左边一格(两个黄色)里挑大的继承过来。
- 14上面是 1,左边是 1,取较大 = 1。这格落子 1(绿色)。
- 15填 [2,3] 这格:nums1 的 4 和 nums2 的 4 正好相等,可以新连一条线。看它的左上角那格(黄色),在它的基础上加一条。
- 16左上角是 1,加上新连的这一条 = 2。这格落子 2(绿色)。
- 17填 [3,1] 这格:nums1 的 2 和 nums2 的 1 不相等,连不了新线,只能从上面一格和左边一格(两个黄色)里挑大的继承过来。
- 18上面是 1,左边是 0,取较大 = 1。这格落子 1(绿色)。
- 19填 [3,2] 这格:nums1 的 2 和 nums2 的 2 正好相等,可以新连一条线。看它的左上角那格(黄色),在它的基础上加一条。
- 20左上角是 1,加上新连的这一条 = 2。这格落子 2(绿色)。
- 21填 [3,3] 这格:nums1 的 2 和 nums2 的 4 不相等,连不了新线,只能从上面一格和左边一格(两个黄色)里挑大的继承过来。
- 22上面是 2,左边是 2,取较大 = 2。这格落子 2(绿色)。
- 23表填满了。右下角 dp[3][3] = 2,就是最多能连出的线数。
- 24回看哪几步是「相等加一」:整张表其实有三格——[1,1] 连 1-1、[2,3] 连 4-4、[3,2] 连 2-2,都是踩着左上角加一长出来的。但 [2,3](4-4) 和 [3,2](2-2) 这两条会交叉,只能二选一;这条最优连线选了 [1,1] 和 [2,3] 两格(橙色),第三格 [3,2](蓝色)连上就会和 4-4 撞,所以最多仍是 2 条。
⚠️ 容易写错的地方
✗ 错:把它当成「最长公共子串」
✓ 对:是子序列,可以跳着选
连线只要求保持顺序,不要求挨着,所以是子序列不是子串
✗ 错:相等时去比上、左
✓ 对:相等时直接取左上角加一
相等取左上加一一定不劣,比上、左更准,写成 max 反而会少算
✗ 错:忘了留第 0 行第 0 列
✓ 对:表开成 (m+1)×(n+1),多出一圈 0
没有这圈 0,dp[1][1] 取左上角时会越界
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
m, n = len(nums1), len(nums2)
f = [[0] * (n + 1) for _ in range(m + 1)]
for i, x in enumerate(nums1, 1):
for j, y in enumerate(nums2, 1):
if x == y:
f[i][j] = f[i - 1][j - 1] + 1
else:
f[i][j] = max(f[i - 1][j], f[i][j - 1])
return f[m][n]C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
int f[m + 1][n + 1];
memset(f, 0, sizeof(f));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (nums1[i - 1] == nums2[j - 1]) {
f[i][j] = f[i - 1][j - 1] + 1;
} else {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
}
return f[m][n];
}
};Java
import java.util.*;
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[][] f = new int[m + 1][n + 1];
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (nums1[i - 1] == nums2[j - 1]) {
f[i][j] = f[i - 1][j - 1] + 1;
} else {
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
}
}
}
return f[m][n];
}
}复杂度
时间
O(m×n)
两层循环填满整张 dp 表,每格 O(1)
空间
O(m×n)
一张二维表;可滚动数组压成 O(min(m,n))
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 不相交的线 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和 LeetCode 1143 最长公共子序列是什么关系?+
同一道题的两张皮。1143 直接给两个字符串求 LCS 长度,本题把「相等能连一条不交叉的线」包在外面,而连线不交叉恰好等价于「两排数按原顺序对齐相等」,也就是 LCS。两题的 dp 定义、相等取左上加一、不等取上左较大的转移一模一样,会了 1143,本题只是把字符换成数字、把「公共子序列」讲成「连线」。
为什么相等时直接取左上角加一,不用再在上、左里比一次?+
相等意味着 nums1 第 i 个和 nums2 第 j 个可以配成新的一条线,这条线用掉了这两个数,剩下的最优连法就落在「两排都退一格」的 dp[i-1][j-1] 上,加一即可。有人担心取上或左会不会更大,其实不会:dp[i-1][j-1] 加一已经把这条新线算进去了,而上、左两格各少考虑了其中一个数,不可能比它加一还大,所以相等时无需再比,直接接左上加一。
空间能不能从二维压到一维?+
能。dp[i][j] 只用到上一行的 dp[i-1][j]、dp[i-1][j-1] 和本行左边的 dp[i][j-1],用一维数组就地覆盖就行。要小心的是左上角 dp[i-1][j-1] 会在更新中被本行的新值盖掉,得先拿一个临时变量存住它再更新。这样空间从 O(m×n) 降到 O(min(m,n))(把短的那一维当数组长度),时间仍是 O(m×n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 不相交的线 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。