Before attempting this problem, you should be comfortable with:
This problem is essentially finding the longest common subsequence (LCS) between two arrays. We can draw a line between nums1[i] and nums2[j] only if they have the same value. Lines cannot cross, which means if we connect nums1[i] to nums2[j], we can only connect elements after index i in nums1 to elements after index j in nums2.
At each position, we have two choices: if the current elements match, we draw a line and move both pointers forward. If they don't match, we try skipping an element from either array and take the maximum result.
dfs(i, j) that returns the maximum number of uncrossed lines starting from index i in nums1 and index j in nums2.i or j reaches the end of its array, return 0.nums1[i] == nums2[j], we can draw a line. Return 1 + dfs(i + 1, j + 1).dfs(i, j + 1) and dfs(i + 1, j).dfs(0, 0).class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
def dfs(i, j):
if i == len(nums1) or j == len(nums2):
return 0
if nums1[i] == nums2[j]:
return 1 + dfs(i + 1, j + 1)
return max(dfs(i, j + 1), dfs(i + 1, j))
return dfs(0, 0)Where and are the sizes of the arrays and respectively.
The recursive solution recomputes many subproblems. For example, dfs(2, 3) might be called multiple times through different paths. We can use memoization to store results of subproblems and avoid redundant calculations.
dp initialized with -1 to indicate uncomputed states.dfs(i, j) similar to the brute force approach.dp[i][j] is already computed. If so, return the cached value.dp[i][j].dfs(0, 0).class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
dp = {}
def dfs(i, j):
if i == len(nums1) or j == len(nums2):
return 0
if (i, j) in dp:
return dp[(i, j)]
if nums1[i] == nums2[j]:
dp[(i, j)] = 1 + dfs(i + 1, j + 1)
else:
dp[(i, j)] = max(dfs(i, j + 1), dfs(i + 1, j))
return dp[(i, j)]
return dfs(0, 0)Where and are the sizes of the arrays and respectively.
Instead of recursion with memoization, we can build the solution iteratively. We define dp[i][j] as the maximum number of uncrossed lines using nums1[0..i-1] and nums2[0..j-1]. By filling this table row by row, we avoid recursion overhead.
dp of size (n+1) x (m+1) initialized with zeros.i in nums1 and j in nums2.nums1[i] == nums2[j], set dp[i+1][j+1] = 1 + dp[i][j] (draw a line).dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]) (skip one element).dp[n][m] where n and m are the sizes of the arrays.class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
n, m = len(nums1), len(nums2)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n):
for j in range(m):
if nums1[i] == nums2[j]:
dp[i + 1][j + 1] = 1 + dp[i][j]
else:
dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j])
return dp[n][m]Where and are the sizes of the arrays and respectively.
Notice that each row of the dp table only depends on the previous row. We can reduce space complexity by keeping only two rows: the prev row and the current dp row being computed.
prev of size m+1 with zeros.nums1, create a new array dp for the current row.nums2:dp[j+1] = 1 + prev[j].dp[j+1] = max(dp[j], prev[j+1]).prev = dp.prev[m].class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
prev = [0] * (len(nums2) + 1)
for i in range(len(nums1)):
dp = [0] * (len(nums2) + 1)
for j in range(len(nums2)):
if nums1[i] == nums2[j]:
dp[j + 1] = 1 + prev[j]
else:
dp[j + 1] = max(dp[j], prev[j + 1])
prev = dp
return prev[len(nums2)]Where and are the sizes of the arrays and respectively.
We can further optimize by using a single array and a variable to track the diagonal value from the previous iteration. This requires careful handling to avoid overwriting values we still need.
nums2 is longer than nums1, swap them so we iterate over the longer array in the outer loop. This minimizes space usage.dp of size m+1 with zeros.nums1:prev to store the diagonal value before it gets overwritten.j in nums2:dp[j+1] as temp before updating.dp[j+1] = 1 + prev.dp[j+1] = max(dp[j+1], dp[j]).prev = temp for the next iteration.dp[m].class Solution:
def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
n, m = len(nums1), len(nums2)
if m > n:
n, m = m, n
nums1, nums2 = nums2, nums1
dp = [0] * (m + 1)
for i in range(n):
prev = 0
for j in range(m):
temp = dp[j + 1]
if nums1[i] == nums2[j]:
dp[j + 1] = 1 + prev
else:
dp[j + 1] = max(dp[j + 1], dp[j])
prev = temp
return dp[m]Where and are the sizes of the arrays and respectively.
Many struggle to identify that this problem is equivalent to finding the Longest Common Subsequence (LCS). The "uncrossed lines" constraint naturally maps to the subsequence property where relative order must be preserved. Failing to make this connection leads to overcomplicated solutions.
When implementing the bottom-up DP solution, a common mistake is using incorrect indices. The DP table is typically sized (n+1) x (m+1) to handle base cases, but accessing nums1[i] vs dp[i+1] can be confusing. Ensure consistency between array indices and DP table indices.
When using space-optimized DP with a single array, forgetting to track the previous diagonal value before overwriting leads to incorrect results. The prev variable must capture dp[j] before it gets updated, as this value is needed for the next iteration's diagonal lookup.