Facebook Pixel

2280. Minimum Lines to Represent a Line Chart

MediumGeometryArrayMathNumber TheorySorting
Leetcode Link

Problem Description

You are given a 2D integer array stockPrices where each element stockPrices[i] = [day_i, price_i] represents that on day day_i, the stock price is price_i.

When you plot these points on an XY plane (with days on the X-axis and prices on the Y-axis) and connect adjacent points in order, you create a line chart. The key question is: what is the minimum number of straight line segments needed to represent this entire chart?

For example, if you have stock prices for multiple days and when you connect them, some consecutive points might be collinear (lie on the same straight line). In such cases, they can be represented by a single line segment instead of multiple segments.

The solution works by:

  1. First sorting the stock prices by day (to ensure proper chronological order)
  2. Then checking each consecutive pair of points to determine if the slope changes
  3. Using cross multiplication dy * dx1 != dx * dy1 to compare slopes without division (avoiding floating-point precision issues)
  4. Counting how many times the slope changes, which gives us the minimum number of lines needed

The clever part of the implementation is using the cross-product comparison: instead of comparing dy/dx with dy1/dx1 (which could cause division by zero or precision issues), it compares dy * dx1 with dx * dy1, which is mathematically equivalent but safer.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that we need to count how many times the direction (slope) changes as we move through the points in chronological order.

Think about drawing a line chart by hand: you start at the first point and draw a straight line to the second point. If the third point lies on the same line extended from the first two points, you can continue with the same line. But if the third point is off that line, you need to start a new line segment with a different slope.

This naturally leads us to check whether consecutive points share the same slope. If three points A, B, and C are collinear, then the slope from A to B equals the slope from B to C. Whenever this equality breaks, we know we need a new line segment.

The mathematical challenge is comparing slopes accurately. The naive approach of calculating (y2-y1)/(x2-x1) has two problems:

  1. Division by zero when points have the same x-coordinate
  2. Floating-point precision errors when comparing decimal numbers

The elegant solution is to use cross multiplication. Instead of checking if (y2-y1)/(x2-x1) == (y3-y2)/(x3-x2), we can cross-multiply to get (y2-y1) * (x3-x2) == (y3-y2) * (x2-x1). This avoids division entirely and keeps everything in integer arithmetic, ensuring perfect precision.

By tracking the previous direction vector (dx, dy) and comparing it with each new direction vector (dx1, dy1), we can count exactly how many times we need to change direction, which equals the minimum number of line segments needed.

Learn more about Math and Sorting patterns.

Solution Approach

The implementation follows a systematic approach to count the minimum number of line segments:

  1. Sort the stock prices by day: We start with stockPrices.sort() to ensure the points are in chronological order. This is crucial because we need to connect points in the order of days.

  2. Initialize tracking variables:

    • dx, dy = 0, 1 represents the initial direction vector. We use (0, 1) as a dummy initial slope that's guaranteed to be different from any actual slope between points (since actual slopes will have non-zero dx).
    • ans = 0 counts the number of line segments needed.
  3. Iterate through consecutive pairs: Using pairwise(stockPrices), we examine each pair of consecutive points (x, y) and (x1, y1).

  4. Calculate the new direction vector: For each pair, we compute:

    • dx1 = x1 - x (change in days)
    • dy1 = y1 - y (change in price)

    This gives us the direction vector (dx1, dy1) for the current segment.

  5. Check for slope change: The critical comparison dy * dx1 != dx * dy1 determines if the slope has changed:

    • This is equivalent to checking if dy/dx != dy1/dx1 but avoids division
    • If the cross products are not equal, the slopes are different, meaning we need a new line segment
    • When this condition is true, we increment ans += 1
  6. Update the previous direction: After checking, we update dx, dy = dx1, dy1 to remember the current direction for the next iteration.

  7. Return the result: The final ans gives us the minimum number of line segments needed.

The algorithm runs in O(n log n) time due to sorting, where n is the number of stock prices. The space complexity is O(1) as we only use a constant amount of extra variables.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with stock prices: [[1,3], [2,6], [3,9], [5,5], [7,1]]

Step 1: Sort by day Already sorted: [[1,3], [2,6], [3,9], [5,5], [7,1]]

Step 2: Initialize

  • dx = 0, dy = 1 (dummy initial direction)
  • ans = 0 (no line segments yet)

Step 3: Process pairs

Pair 1: [1,3] → [2,6]

  • Calculate: dx1 = 2-1 = 1, dy1 = 6-3 = 3
  • Check slope change: dy * dx1 != dx * dy11 * 1 != 0 * 31 != 0 (TRUE)
  • Slopes are different, so ans = 1 (need first line segment)
  • Update: dx = 1, dy = 3

Pair 2: [2,6] → [3,9]

  • Calculate: dx1 = 3-2 = 1, dy1 = 9-6 = 3
  • Check slope change: 3 * 1 != 1 * 33 != 3 (FALSE)
  • Same slope! Points are collinear, no new segment needed
  • Update: dx = 1, dy = 3

Pair 3: [3,9] → [5,5]

  • Calculate: dx1 = 5-3 = 2, dy1 = 5-9 = -4
  • Check slope change: 3 * 2 != 1 * (-4)6 != -4 (TRUE)
  • Different slope, so ans = 2 (need second line segment)
  • Update: dx = 2, dy = -4

Pair 4: [5,5] → [7,1]

  • Calculate: dx1 = 7-5 = 2, dy1 = 1-5 = -4
  • Check slope change: (-4) * 2 != 2 * (-4)-8 != -8 (FALSE)
  • Same slope! Points are collinear, no new segment needed
  • Update: dx = 2, dy = -4

Result: ans = 2

The chart needs exactly 2 line segments:

  • First segment: connects [1,3] → [2,6] → [3,9] (slope = 3/1)
  • Second segment: connects [3,9] → [5,5] → [7,1] (slope = -4/2 = -2)

Solution Implementation

1class Solution:
2    def minimumLines(self, stockPrices: List[List[int]]) -> int:
3        # Sort stock prices by day (x-coordinate)
4        stockPrices.sort()
5      
6        # Initialize previous slope components (vertical line as reference)
7        prev_dx, prev_dy = 0, 1
8      
9        # Count the number of line segments needed
10        line_count = 0
11      
12        # Iterate through consecutive pairs of points
13        for i in range(len(stockPrices) - 1):
14            # Get current and next points
15            x_curr, y_curr = stockPrices[i]
16            x_next, y_next = stockPrices[i + 1]
17          
18            # Calculate slope components for current segment
19            curr_dx = x_next - x_curr
20            curr_dy = y_next - y_curr
21          
22            # Check if slope has changed using cross multiplication
23            # Avoids floating point division: dy1/dx1 != dy2/dx2 => dy1*dx2 != dx1*dy2
24            if prev_dy * curr_dx != prev_dx * curr_dy:
25                line_count += 1
26          
27            # Update previous slope for next iteration
28            prev_dx, prev_dy = curr_dx, curr_dy
29      
30        return line_count
31
1class Solution {
2    public int minimumLines(int[][] stockPrices) {
3        // Sort stock prices by day (first element of each pair)
4        Arrays.sort(stockPrices, (a, b) -> a[0] - b[0]);
5      
6        // Initialize slope components for comparison
7        // Using dx=0, dy=1 as initial values ensures first comparison always creates a new line
8        int previousDeltaX = 0;
9        int previousDeltaY = 1;
10        int lineCount = 0;
11      
12        // Iterate through consecutive stock price points
13        for (int i = 1; i < stockPrices.length; i++) {
14            // Get coordinates of previous point
15            int prevDay = stockPrices[i - 1][0];
16            int prevPrice = stockPrices[i - 1][1];
17          
18            // Get coordinates of current point
19            int currentDay = stockPrices[i][0];
20            int currentPrice = stockPrices[i][1];
21          
22            // Calculate slope components (rise and run) for current segment
23            int currentDeltaX = currentDay - prevDay;
24            int currentDeltaY = currentPrice - prevPrice;
25          
26            // Check if slope changed by cross-multiplication to avoid division
27            // If previousDeltaY/previousDeltaX != currentDeltaY/currentDeltaX
28            // Then previousDeltaY * currentDeltaX != previousDeltaX * currentDeltaY
29            if (previousDeltaY * currentDeltaX != previousDeltaX * currentDeltaY) {
30                lineCount++;
31            }
32          
33            // Update slope components for next iteration
34            previousDeltaX = currentDeltaX;
35            previousDeltaY = currentDeltaY;
36        }
37      
38        return lineCount;
39    }
40}
41
1class Solution {
2public:
3    int minimumLines(vector<vector<int>>& stockPrices) {
4        // Sort stock prices by day (x-coordinate)
5        sort(stockPrices.begin(), stockPrices.end());
6      
7        // Initialize previous slope components (direction vector)
8        // Starting with dx=0, dy=1 ensures first comparison always creates a new line
9        int prevDeltaX = 0;
10        int prevDeltaY = 1;
11      
12        // Counter for minimum number of lines needed
13        int lineCount = 0;
14      
15        // Iterate through consecutive points to check if slope changes
16        for (int i = 1; i < stockPrices.size(); ++i) {
17            // Get coordinates of previous point
18            int prevX = stockPrices[i - 1][0];
19            int prevY = stockPrices[i - 1][1];
20          
21            // Get coordinates of current point
22            int currX = stockPrices[i][0];
23            int currY = stockPrices[i][1];
24          
25            // Calculate slope components (direction vector) between consecutive points
26            int currDeltaX = currX - prevX;
27            int currDeltaY = currY - prevY;
28          
29            // Check if slope changed using cross multiplication to avoid floating point
30            // If prevDeltaY/prevDeltaX != currDeltaY/currDeltaX, then slopes are different
31            // Cross multiply: prevDeltaY * currDeltaX != prevDeltaX * currDeltaY
32            if ((long long)prevDeltaY * currDeltaX != (long long)prevDeltaX * currDeltaY) {
33                ++lineCount;
34            }
35          
36            // Update previous slope for next iteration
37            prevDeltaX = currDeltaX;
38            prevDeltaY = currDeltaY;
39        }
40      
41        return lineCount;
42    }
43};
44
1/**
2 * Calculates the minimum number of straight lines needed to connect all stock price points
3 * @param stockPrices - 2D array where each element is [day, price]
4 * @returns The minimum number of lines needed
5 */
6function minimumLines(stockPrices: number[][]): number {
7    const numberOfPoints = stockPrices.length;
8  
9    // Sort stock prices by day (x-coordinate) in ascending order
10    stockPrices.sort((a, b) => a[0] - b[0]);
11  
12    let lineCount = 0;
13    // Store previous line's direction vector [dx, dy] using BigInt to avoid precision issues
14    let previousDirection: [bigint, bigint] = [BigInt(0), BigInt(0)];
15  
16    // Iterate through consecutive points to check if they form different lines
17    for (let i = 1; i < numberOfPoints; i++) {
18        // Get coordinates of current and previous points
19        const [x1, y1] = stockPrices[i - 1];
20        const [x2, y2] = stockPrices[i];
21      
22        // Calculate direction vector for current line segment
23        const deltaX = BigInt(x2 - x1);
24        const deltaY = BigInt(y2 - y1);
25      
26        // Check if current direction differs from previous direction
27        // Using cross multiplication to compare slopes: dx1/dy1 != dx2/dy2 => dx1*dy2 != dx2*dy1
28        if (i === 1 || deltaX * previousDirection[1] !== deltaY * previousDirection[0]) {
29            lineCount++;
30        }
31      
32        // Update previous direction for next iteration
33        previousDirection = [deltaX, deltaY];
34    }
35  
36    return lineCount;
37}
38

Time and Space Complexity

Time Complexity: O(n log n) where n is the number of stock prices.

  • The sorting operation stockPrices.sort() takes O(n log n) time.
  • The pairwise iteration through the sorted array takes O(n) time, visiting each consecutive pair once.
  • Inside the loop, all operations (arithmetic calculations and comparisons) take O(1) time.
  • Therefore, the overall time complexity is dominated by the sorting step: O(n log n).

Space Complexity: O(1) if we don't count the space used by sorting, or O(n) if we do.

  • The algorithm uses a constant amount of extra variables: dx, dy, dx1, dy1, ans, x, y, x1, y1.
  • The pairwise function creates an iterator that doesn't store additional copies of the data.
  • However, Python's sort() method uses Timsort algorithm which requires O(n) auxiliary space in the worst case.
  • If we consider only the additional space used by the algorithm itself (excluding sorting), the space complexity is O(1).
  • If we include the sorting space requirement, the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Handling Edge Cases with Insufficient Points

A critical pitfall is not checking if there are enough points to form any line segments. If the input has 0 or 1 points, no line segments can be formed, and the function should return 0.

Problem Example:

stockPrices = [[1, 10]]  # Only one point
# Or
stockPrices = []  # Empty array

Solution: Add an early return check at the beginning of the function:

def minimumLines(self, stockPrices: List[List[int]]) -> int:
    # Handle edge cases
    if len(stockPrices) <= 1:
        return 0
  
    stockPrices.sort()
    # ... rest of the code

2. Using Floating-Point Division for Slope Comparison

A tempting but dangerous approach is to directly calculate and compare slopes using division:

# WRONG APPROACH - Don't do this!
slope1 = dy1 / dx1
slope2 = dy2 / dx2
if slope1 != slope2:
    line_count += 1

Problems with this approach:

  • Division by zero: When points have the same x-coordinate (vertical line)
  • Floating-point precision errors: Slopes that should be equal might differ due to rounding
  • Integer overflow concerns: Very large numbers might lose precision

Solution: Always use cross-multiplication to compare slopes:

# Correct approach
if prev_dy * curr_dx != prev_dx * curr_dy:
    line_count += 1

3. Forgetting to Sort the Input

The problem states points should be connected in chronological order (by day). If you forget to sort, the algorithm will connect points in the wrong order, producing incorrect results.

Problem Example:

stockPrices = [[3, 30], [1, 10], [2, 20]]
# Without sorting, we'd connect (3,30) -> (1,10) -> (2,20)
# Instead of the correct (1,10) -> (2,20) -> (3,30)

Solution: Always sort the input by day (x-coordinate) first:

stockPrices.sort()  # or stockPrices.sort(key=lambda x: x[0])

4. Incorrect Initial Slope Values

Using an initial slope that could match an actual slope between points can cause the first segment to be missed.

Problem Example:

# WRONG: Using (1, 0) as initial slope
prev_dx, prev_dy = 1, 0  # Horizontal line
# If the first actual segment is also horizontal, it won't be counted

Solution: Use an impossible initial slope like (0, 1) which represents a vertical line from x=0 to x=0, which can never match any actual segment between different days:

prev_dx, prev_dy = 0, 1  # Guaranteed to be different from any real slope

5. Using pairwise Without Proper Import

The pairwise function from itertools is only available in Python 3.10+. Using it without checking version compatibility or proper import will cause runtime errors.

Solution: Either ensure Python 3.10+ compatibility or use index-based iteration:

# Option 1: With pairwise (Python 3.10+)
from itertools import pairwise
for (x, y), (x1, y1) in pairwise(stockPrices):
    # process pairs

# Option 2: Index-based (works with all Python versions)
for i in range(len(stockPrices) - 1):
    x, y = stockPrices[i]
    x1, y1 = stockPrices[i + 1]
    # process pairs
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More