1035. Uncrossed Lines
Problem Description
You have two integer arrays nums1
and nums2
written on two separate horizontal lines in their given order.
Your task is to draw straight lines connecting numbers from nums1
to nums2
following these rules:
- You can only connect
nums1[i]
tonums2[j]
if they have the same value (nums1[i] == nums2[j]
) - The connecting lines cannot cross each other
- Each number can only be part of one connection (lines cannot share endpoints)
The goal is to find the maximum number of such non-crossing lines you can draw.
For example, if nums1 = [1, 4, 2]
and nums2 = [1, 2, 4]
, you could connect:
nums1[0]
(value 1) tonums2[0]
(value 1)nums1[2]
(value 2) tonums2[1]
(value 2)
This gives you 2 lines maximum. You cannot also connect nums1[1]
(value 4) to nums2[2]
(value 4) because that line would cross the line connecting the 2s.
The problem is essentially finding the longest common subsequence between the two arrays while maintaining the relative order, which ensures the lines don't cross.
Intuition
The key insight is recognizing that lines won't cross if we maintain the relative order of connections. When we connect nums1[i]
to nums2[j]
and nums1[k]
to nums2[l]
, the lines won't cross if and only if:
- When
i < k
, we must havej < l
- When
i > k
, we must havej > l
This means we're essentially looking for matching elements that preserve their relative positions in both arrays - which is exactly the Longest Common Subsequence (LCS) problem in disguise.
Think about it this way: as we scan through both arrays from left to right, whenever we find matching values, we can either:
- Draw a line between them (if they equal)
- Skip the current element in
nums1
and look for matches later innums2
- Skip the current element in
nums2
and look for matches later innums1
This naturally leads to a dynamic programming approach where we build up the solution by considering prefixes of both arrays. For each position (i, j)
, we ask: "What's the maximum number of non-crossing lines we can draw using the first i
elements of nums1
and the first j
elements of nums2
?"
The beauty is that when nums1[i-1] == nums2[j-1]
, we can safely add this connection to whatever optimal solution we had for the smaller subproblem (i-1, j-1)
, knowing it won't cross any previous lines. When they don't match, we take the better option between excluding the current element from either array.
This builds up our solution incrementally, ensuring we always maintain the maximum possible connections without any crossings.
Learn more about Dynamic Programming patterns.
Solution Approach
We implement the solution using a 2D dynamic programming table f[i][j]
where each cell represents the maximum number of non-crossing lines we can draw between the first i
elements of nums1
and the first j
elements of nums2
.
Initialization:
- Create a 2D table
f
with dimensions(m+1) × (n+1)
wherem = len(nums1)
andn = len(nums2)
- Initialize all values to 0, since with 0 elements from either array, we can draw 0 lines
State Transition:
For each position (i, j)
where 1 ≤ i ≤ m
and 1 ≤ j ≤ n
:
-
When elements match (
nums1[i-1] == nums2[j-1]
):- We can draw a line connecting these matching elements
- This adds 1 to the optimal solution of the subproblem without these elements
- Formula:
f[i][j] = f[i-1][j-1] + 1
-
When elements don't match (
nums1[i-1] != nums2[j-1]
):- We can't connect these elements
- We take the maximum of two options:
- Exclude element from
nums1
: use solution fromf[i-1][j]
- Exclude element from
nums2
: use solution fromf[i][j-1]
- Exclude element from
- Formula:
f[i][j] = max(f[i-1][j], f[i][j-1])
Building the Solution: The algorithm iterates through all pairs of indices using nested loops:
- Outer loop:
i
from 1 tom
(considering each element innums1
) - Inner loop:
j
from 1 ton
(considering each element innums2
)
Note that we use 1-based indexing for the DP table while the arrays use 0-based indexing, hence we access nums1[i-1]
and nums2[j-1]
when filling f[i][j]
.
Final Answer:
The value at f[m][n]
gives us the maximum number of non-crossing lines between all elements of both arrays.
The time complexity is O(m × n)
for filling the DP table, and space complexity is also O(m × n)
for storing the table.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with nums1 = [2, 5, 1, 2, 5]
and nums2 = [10, 5, 2, 1, 5, 2]
.
We'll build a DP table f[i][j]
where f[i][j]
represents the maximum number of non-crossing lines we can draw using the first i
elements of nums1
and first j
elements of nums2
.
Initial Setup:
- Create a table with dimensions 6×7 (including row/column for empty arrays)
- Initialize all values to 0
Step-by-step filling:
Starting state (all zeros):
"" 10 5 2 1 5 2 "" 0 0 0 0 0 0 0 2 0 ? ? ? ? ? ? 5 0 ? ? ? ? ? ? 1 0 ? ? ? ? ? ? 2 0 ? ? ? ? ? ? 5 0 ? ? ? ? ? ?
Row 1 (nums1[0] = 2):
- Compare 2 with each element in nums2
- 2 ≠ 10:
f[1][1] = max(f[0][1], f[1][0]) = 0
- 2 ≠ 5:
f[1][2] = max(f[0][2], f[1][1]) = 0
- 2 = 2:
f[1][3] = f[0][2] + 1 = 1
✓ (Found match!) - 2 ≠ 1:
f[1][4] = max(f[0][4], f[1][3]) = 1
- 2 ≠ 5:
f[1][5] = max(f[0][5], f[1][4]) = 1
- 2 = 2:
f[1][6] = f[0][5] + 1 = 1
Row 2 (nums1[1] = 5):
- 5 ≠ 10:
f[2][1] = max(f[1][1], f[2][0]) = 0
- 5 = 5:
f[2][2] = f[1][1] + 1 = 1
✓ - 5 ≠ 2:
f[2][3] = max(f[1][3], f[2][2]) = 1
- 5 ≠ 1:
f[2][4] = max(f[1][4], f[2][3]) = 1
- 5 = 5:
f[2][5] = f[1][4] + 1 = 2
✓ - 5 ≠ 2:
f[2][6] = max(f[1][6], f[2][5]) = 2
Row 3 (nums1[2] = 1):
- 1 ≠ 10:
f[3][1] = 0
- 1 ≠ 5:
f[3][2] = 1
- 1 ≠ 2:
f[3][3] = 1
- 1 = 1:
f[3][4] = f[2][3] + 1 = 2
✓ - 1 ≠ 5:
f[3][5] = max(f[2][5], f[3][4]) = 2
- 1 ≠ 2:
f[3][6] = 2
Row 4 (nums1[3] = 2):
- Following similar logic, when we reach position [4][3] where both values are 2:
f[4][3] = f[3][2] + 1 = 2
- When we reach [4][6] where both are 2:
f[4][6] = f[3][5] + 1 = 3
Row 5 (nums1[4] = 5):
- The second 5 in nums1 can match with either 5 in nums2
- Best result comes from matching with the second 5:
f[5][5] = f[4][4] + 1 = 3
- Final cell:
f[5][6] = max(f[4][6], f[5][5]) = 3
Final DP Table:
"" 10 5 2 1 5 2 "" 0 0 0 0 0 0 0 2 0 0 0 1 1 1 1 5 0 0 1 1 1 2 2 1 0 0 1 1 2 2 2 2 0 0 1 2 2 2 3 5 0 0 1 2 2 3 3
Answer: f[5][6] = 3
The three non-crossing lines connect:
- nums1[1] (5) → nums2[1] (5)
- nums1[2] (1) → nums2[3] (1)
- nums1[3] (2) → nums2[5] (2)
These connections maintain order (indices increase in both arrays), ensuring no lines cross.
Solution Implementation
1class Solution:
2 def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
3 """
4 Find the maximum number of uncrossed lines between two arrays.
5 This is equivalent to finding the Longest Common Subsequence (LCS).
6
7 Args:
8 nums1: First array of integers
9 nums2: Second array of integers
10
11 Returns:
12 Maximum number of connecting lines that don't cross
13 """
14 m, n = len(nums1), len(nums2)
15
16 # Create a 2D DP table with dimensions (m+1) x (n+1)
17 # dp[i][j] represents the maximum uncrossed lines using
18 # first i elements from nums1 and first j elements from nums2
19 dp = [[0] * (n + 1) for _ in range(m + 1)]
20
21 # Fill the DP table
22 for i, num1 in enumerate(nums1, 1):
23 for j, num2 in enumerate(nums2, 1):
24 if num1 == num2:
25 # If elements match, we can draw a line
26 # Add 1 to the result from previous indices
27 dp[i][j] = dp[i - 1][j - 1] + 1
28 else:
29 # If elements don't match, take the maximum of:
30 # - Excluding current element from nums1
31 # - Excluding current element from nums2
32 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
33
34 # Return the maximum uncrossed lines using all elements
35 return dp[m][n]
36
1class Solution {
2 /**
3 * Find the maximum number of uncrossed lines between two arrays.
4 * This is essentially finding the Longest Common Subsequence (LCS).
5 *
6 * @param nums1 First integer array
7 * @param nums2 Second integer array
8 * @return Maximum number of connecting lines that don't cross
9 */
10 public int maxUncrossedLines(int[] nums1, int[] nums2) {
11 int length1 = nums1.length;
12 int length2 = nums2.length;
13
14 // dp[i][j] represents the maximum uncrossed lines using
15 // first i elements from nums1 and first j elements from nums2
16 int[][] dp = new int[length1 + 1][length2 + 1];
17
18 // Build the DP table bottom-up
19 for (int i = 1; i <= length1; i++) {
20 for (int j = 1; j <= length2; j++) {
21 // Current elements in the original arrays (0-indexed)
22 if (nums1[i - 1] == nums2[j - 1]) {
23 // If elements match, we can draw a line between them
24 // Add 1 to the result without these elements
25 dp[i][j] = dp[i - 1][j - 1] + 1;
26 } else {
27 // If elements don't match, take the maximum of:
28 // 1. Excluding current element from nums1
29 // 2. Excluding current element from nums2
30 dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
31 }
32 }
33 }
34
35 // Return the maximum uncrossed lines using all elements
36 return dp[length1][length2];
37 }
38}
39
1class Solution {
2public:
3 int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
4 int m = nums1.size();
5 int n = nums2.size();
6
7 // dp[i][j] represents the maximum number of uncrossed lines
8 // between nums1[0...i-1] and nums2[0...j-1]
9 vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
10
11 // Fill the DP table
12 for (int i = 1; i <= m; ++i) {
13 for (int j = 1; j <= n; ++j) {
14 // If current elements match, we can draw a line
15 if (nums1[i - 1] == nums2[j - 1]) {
16 // Add 1 to the result without these matching elements
17 dp[i][j] = dp[i - 1][j - 1] + 1;
18 } else {
19 // If they don't match, take the maximum from either:
20 // - Excluding current element from nums1
21 // - Excluding current element from nums2
22 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
23 }
24 }
25 }
26
27 // Return the maximum uncrossed lines for the entire arrays
28 return dp[m][n];
29 }
30};
31
1/**
2 * Finds the maximum number of uncrossed lines between two arrays.
3 * This is equivalent to finding the Longest Common Subsequence (LCS).
4 *
5 * @param nums1 - First array of numbers
6 * @param nums2 - Second array of numbers
7 * @returns Maximum number of lines that can be drawn without crossing
8 */
9function maxUncrossedLines(nums1: number[], nums2: number[]): number {
10 const nums1Length: number = nums1.length;
11 const nums2Length: number = nums2.length;
12
13 // Create a 2D DP table where dp[i][j] represents the maximum uncrossed lines
14 // between the first i elements of nums1 and first j elements of nums2
15 const dpTable: number[][] = Array.from(
16 { length: nums1Length + 1 },
17 () => Array(nums2Length + 1).fill(0)
18 );
19
20 // Fill the DP table using bottom-up approach
21 for (let i = 1; i <= nums1Length; i++) {
22 for (let j = 1; j <= nums2Length; j++) {
23 // If current elements match, we can draw a line
24 if (nums1[i - 1] === nums2[j - 1]) {
25 // Add 1 to the result from previous elements (diagonal)
26 dpTable[i][j] = dpTable[i - 1][j - 1] + 1;
27 } else {
28 // If elements don't match, take the maximum from either
29 // excluding current element from nums1 or nums2
30 dpTable[i][j] = Math.max(
31 dpTable[i - 1][j], // Exclude current element from nums1
32 dpTable[i][j - 1] // Exclude current element from nums2
33 );
34 }
35 }
36 }
37
38 // Return the maximum uncrossed lines for all elements
39 return dpTable[nums1Length][nums2Length];
40}
41
Time and Space Complexity
The time complexity is O(m × n)
, where m
is the length of nums1
and n
is the length of nums2
. This is because the algorithm uses two nested loops: the outer loop iterates through all elements of nums1
(m iterations), and the inner loop iterates through all elements of nums2
(n iterations). Each cell in the DP table f[i][j]
is computed exactly once with constant time operations.
The space complexity is O(m × n)
. The algorithm creates a 2D DP table f
with dimensions (m + 1) × (n + 1)
to store the intermediate results. This table requires O((m + 1) × (n + 1)) = O(m × n)
space to store all the computed values for the dynamic programming solution.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Index Confusion Between DP Table and Arrays
The Pitfall: One of the most common mistakes is mixing up the indexing between the DP table (1-based) and the original arrays (0-based). Developers often write:
if nums1[i] == nums2[j]:
instead ofif nums1[i-1] == nums2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
wheni
andj
are array indices (0-based)
This leads to index out of bounds errors or incorrect results.
Solution:
Always remember that dp[i][j]
represents the solution for the first i
elements of nums1
and first j
elements of nums2
. When accessing array elements inside the loop where i
ranges from 1 to m, use nums1[i-1]
and nums2[j-1]
.
2. Incorrect DP Table Initialization
The Pitfall: Creating the DP table with wrong dimensions like:
dp = [[0] * n for _ in range(m)]
(missing the +1)dp = [[0] * (m + 1) for _ in range(n + 1)]
(swapped dimensions)
Solution:
Always create the table as dp = [[0] * (n + 1) for _ in range(m + 1)]
where the outer list has m+1
rows and each row has n+1
columns. The extra row and column handle the base case of 0 elements.
3. Space Optimization Attempt Gone Wrong
The Pitfall:
Trying to optimize space by using a 1D array but not properly handling the dependency on dp[i-1][j-1]
:
# Incorrect optimization
dp = [0] * (n + 1)
for i in range(1, m + 1):
for j in range(1, n + 1):
if nums1[i-1] == nums2[j-1]:
dp[j] = dp[j-1] + 1 # Wrong! This uses the current row's j-1
Solution: If optimizing for space, you need to carefully preserve the previous diagonal value:
dp = [0] * (n + 1)
for i in range(1, m + 1):
prev = 0 # Store dp[i-1][j-1]
for j in range(1, n + 1):
temp = dp[j] # Save current value before updating
if nums1[i-1] == nums2[j-1]:
dp[j] = prev + 1
else:
dp[j] = max(dp[j], dp[j-1])
prev = temp # Update prev for next iteration
4. Misunderstanding the Problem as Maximum Matching
The Pitfall: Thinking the problem asks for the maximum number of connections regardless of crossing, leading to solutions that count all matching pairs:
# Wrong approach count = 0 for num in nums1: if num in nums2: count += 1
Solution: Remember that lines cannot cross. The problem is essentially finding the Longest Common Subsequence (LCS), where the order matters. The DP approach ensures we only count non-crossing connections by maintaining the relative order of elements.
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!