3009. Maximum Number of Intersections on the Chart

HardBinary Indexed TreeGeometryArrayMath
Leetcode Link

Problem Description

The problem presents a line chart with n points, each point corresponding to a given y-coordinate from an integer array y. Each point on the chart has the coordinates (k, y[k]) where k is a 1-indexed position denoting the x-coordinate and y[k] is the y-coordinate of the k-th point. All points are connected by line segments to form a polyline, and it is mentioned that there are no horizontal line segments, meaning that the y-coordinates of every two consecutive points are different.

The task is to calculate the maximum number of intersection points that a horizontal line, which extends infinitely in both directions, can have with the given chart. The output should be the maximum number of times any such horizontal line crosses the line chart.

Intuition

When looking for the maximum number of intersections, we should consider that the number of intersections for a continuous polyline will increase or decrease as we move the horizontal line up or down.

The insight into solving this problem is to find all potential y-values where intersection count changes, which happen at or between the y-values of the given points. Instead of using a brute force approach and checking all possible y-values, we can optimize by only considering the y-values where changes occur.

Each segment between two points on the chart can intersect with a horizontal line at most once. If two consecutive points have y-coordinates such that y[i] < y[i + 1], the segment between them will intersect with any horizontal line with y-coordinate between y[i] and y[i + 1] - and similarly, if y[i] > y[i + 1], between y[i + 1] and y[i].

To keep track of the changes, we can use a TreeMap to simulate the process. The keys of the TreeMap will be significant y-values multiplied by 2 (to handle the situation where the start and end y-values could be half values, since points can intersect at mid-points between integer y-values). The corresponding values will be the change in the intersection count happening at that y-coordinate.

We iterate through the segments and update the TreeMap where the start of the segment increments the intersection count and the end of the segment decrements it. While updating the TreeMap, the merge function is used to add or subtract counts for specific y-values. After processing all segments, iterate through the TreeMap to find the maximum number of intersections by summing changes to the intersection count and keeping track of the maximum value encountered.

The solution code implements this method and finds the maximum intersection count efficiently.

Learn more about Math patterns.

Solution Approach

The solution approach leverages the TreeMap data structure, which is a Red-Black tree-based NavigableMap implementation. It is used here because of its ability to maintain sorted keys, which allows for quick insertions and deletions while also providing an efficient means of iteratively processing in a sorted order.

The main algorithm proceeds as follows:

  1. Initialize a TreeMap called line and two integer variables: ans to track the maximum number of intersections and intersectionCount to keep a cumulative count of intersections as we iterate through the changes.
  2. Iterate over each segment in the line chart, looking at the pairs of points (i.e., (y[i-1], y[i]) for i ranging from 1 to n-1), since the points are 1-indexed.
  3. For each segment, calculate the starting and ending coordinates by doubling the y-values (2 * y[i-1] and 2 * y[i]). Doubling is done to handle cases where the intersection occurs at exact halves between integers.
  4. Conditionally adjust the ending coordinate by adding or subtracting 1 to prevent overlapping counts on segment borders (except for the last point).
  5. Update the TreeMap using the merge method, which either adds a new key with the value or updates the existing key with the provided remapping function (Integer::sum in this case). Increment the intersection count at the start of the segment and decrement it after the end of the segment.
  6. After populating the TreeMap with all the potential y-values and their corresponding changes, iterate through the values of the TreeMap.
  7. As we traverse the TreeMap, add each value (change in intersection count) to intersectionCount. If intersectionCount exceeds ans at any point, update ans with the value of intersectionCount, thus keeping track of the maximum intersection count at any y-coordinate.
  8. After completing the TreeMap traversal, return ans as the maximum number of intersections.

The use of TreeMap is crucial because it allows us to abstract away the process of sorting and efficiently computing the cumulative changes while iterating over the y-values. By simulating this process, we avoid calculating the intersection count for every possible y-value and instead only focus on those that actually contribute to an increase or decrease in the intersection count, thus optimizing the solution.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).

Example Walkthrough

Let's assume we have a line chart with 5 points (n=5) and the corresponding y-coordinates are as follows: [2, 3, 1, 4, 2]. These points translate to the coordinates: (1, 2), (2, 3), (3, 1), (4, 4), (5, 2) when considering their positions in our 1-indexed chart.

Following the solution approach, here's a step-by-step illustration of how to find the maximum number of intersections a horizontal line would have with the polyline formed by these points:

  1. Initialize TreeMap line and integer variables ans = 0 and intersectionCount = 0.
  2. We have 4 segments in the line chart: between points (1, 2), (2, 3); (2, 3), (3, 1); (3, 1), (4, 4); and (4, 4), (5, 2). Iterate over these segments to determine start and end coordinates and update the TreeMap accordingly.
  3. For the first segment from (1, 2) to (2, 3), we double the y-values and get (4, 6) as our starting and ending coordinates. Update the TreeMap:
    • line.merge(4, 1, Integer::sum) – Starting y-value; increments intersection count.
    • line.merge(6, -1, Integer::sum) – Ending y-value; decrements intersection count.
  4. For the second segment from (2, 3) to (3, 1), doubled y-values are (6, 2). Since we decrement after the end, adjust the endpoint (1) to not overlap, making it (6, 1):
    • line.merge(6, 1, Integer::sum) – Overlaps with the endpoint of the first segment.
    • line.merge(1, -1, Integer::sum) – Adjusted starting y-value; decrements intersection count.
  5. Repeat the above step for the remaining segments.
  6. The TreeMap now stores all the points where the intersection count potentially changes with keys representing y-values (doubled and adjusted) and values representing the change in the intersection count at those points.
  7. Iterate through the TreeMap, while adding each value to intersectionCount and updating ans if intersectionCount exceeds the current ans:
    • intersectionCount += line.get(key);
    • ans = Math.max(ans, intersectionCount).
  8. After the TreeMap is fully traversed, ans will hold the maximum number of intersections, which is the answer we seek to find.

Using this method, let's calculate the number of intersections based on the values we have populated in our TreeMap:

Starting with intersectionCount = 0 and considering TreeMap entries as (y-value, change):

  • At y-value 1: intersectionCount += -1 => intersectionCount = -1.
  • At y-value 4: intersectionCount += 1 => intersectionCount = 0.
  • At y-value 6: intersectionCount += 1 - 1 => intersectionCount = 0.
  • At y-value 8: intersectionCount += 1 => intersectionCount = 1 (update ans to 1).
  • At y-value 14: intersectionCount += -1 => intersectionCount = 0.

Therefore, the maximum number of intersections a horizontal line can have with this polyline is ans = 1.

Solution Implementation

1class Solution:
2    def max_intersection_count(self, y):
3        n = len(y)  # Number of elements in the list y
4        max_intersections = 0  # Holds the maximum count of intersections
5        current_intersection_count = 0  # Tracks the current count of intersections
6        sweep_line = {}  # Dictionary to simulate the sweep line algorithm
7
8        # Iterate over elements of the array to populate the dictionary
9        for i in range(1, n):
10            # Calculate the starting point of the line segment
11            start = 2 * y[i - 1]
12            # Calculate the ending point of the line segment
13            # Adjust for the last point or if the current y is greater than the previous y
14            end = 2 * y[i] + (0 if i == n - 1 else (-1 if y[i] > y[i - 1] else 1))
15            # Merge the start points into dictionary, incrementing the count for intersections
16            sweep_line[start, end] = sweep_line.get(start, end, 0) + 1
17            # Merge the end points into dictionary, decrementing the count for intersections
18            sweep_line[end + 1] = sweep_line.get(end + 1, 0) - 1
19
20        # Sort the keys of the dictionary to simulate the behavior of TreeMap
21        sorted_keys = sorted(sweep_line)
22
23        # Iterate through the sorted keys
24        for key in sorted_keys:
25            # Update the current intersection count
26            current_intersection_count += sweep_line[key]
27            # Update the maximum intersection count found so far
28            max_intersections = max(max_intersections, current_intersection_count)
29
30        return max_intersections  # Return the maximum number of intersections found
31
1class Solution {
2    public int maxIntersectionCount(int[] y) {
3        final int n = y.length; // Number of elements in the array y
4        int maxIntersections = 0; // Holds the maximum intersection count
5        int currentIntersectionCount = 0; // Tracks the current count of intersections
6        TreeMap<Integer, Integer> sweepLine = new TreeMap<>(); // TreeMap to simulate the sweep line algorithm
7
8        // Iterate over the elements of the array to populate the TreeMap
9        for (int i = 1; i < n; ++i) {
10            // Calculate the starting point of the line segment
11            final int start = 2 * y[i - 1];
12            // Calculate the ending point of the line segment
13            // Adjust for the last point or if the current y is more than the previous y
14            final int end = 2 * y[i] + (i == n - 1 ? 0 : (y[i] > y[i - 1] ? -1 : 1));
15            // Merge the start points into TreeMap, incrementing the count for intersections
16            sweepLine.merge(Math.min(start, end), 1, Integer::sum);
17            // Merge the end points into TreeMap, decrementing the count for intersections
18            sweepLine.merge(Math.max(start, end) + 1, -1, Integer::sum);
19        }
20
21        // Iterate through the TreeMap
22        for (final int count : sweepLine.values()) {
23            // Update the current intersection count
24            currentIntersectionCount += count;
25            // Update the maximum intersection count found so far
26            maxIntersections = Math.max(maxIntersections, currentIntersectionCount);
27        }
28
29        return maxIntersections; // Return the max number of intersections found
30    }
31}
32
1#include <map>
2
3class Solution {
4public:
5    int maxIntersectionCount(std::vector<int>& y) {
6        const int n = y.size(); // Number of elements in the vector y
7        int maxIntersections = 0; // Holds the maximum intersection count
8        int currentIntersectionCount = 0; // Tracks the current count of intersections
9        std::map<int, int> sweepLine; // Map to simulate the sweep line algorithm
10
11        // Iterate over the elements of the vector to populate the map
12        for (int i = 1; i < n; ++i) {
13            // Calculate the starting point of the line segment
14            const int start = 2 * y[i - 1];
15            // Calculate the ending point of the line segment
16            // Adjust for the last point or if the current y is more than the previous y
17            const int end = 2 * y[i] + (i == n - 1 ? 0 : (y[i] > y[i - 1] ? -1 : 1));
18          
19            // Add or update the starting point in the map
20            int& startCount = sweepLine[std::min(start, end)];
21            startCount++;
22          
23            // Add or update the ending point in the map
24            int& endCount = sweepLine[std::max(start, end) + 1];
25            endCount--;
26        }
27
28        // Iterate through the map
29        for (auto it = sweepLine.begin(); it != sweepLine.end(); ++it) {
30            // Update the current intersection count
31            currentIntersectionCount += it->second;
32            // Update the maximum intersection count found so far
33            maxIntersections = std::max(maxIntersections, currentIntersectionCount);
34        }
35
36        // Return the max number of intersections found
37        return maxIntersections;
38    }
39};
40
1// Defining a type alias for clarity
2type TreeMap = Map<number, number>;
3
4function maxIntersectionCount(y: number[]): number {
5    const n: number = y.length; // Number of elements in the array y
6    let maxIntersections: number = 0; // Holds the maximum intersection count
7    let currentIntersectionCount: number = 0; // Tracks the current count of intersections
8    let sweepLine: TreeMap = new Map<number, number>(); // TreeMap to simulate the sweep line algorithm
9
10    // Iterate over the elements of the array to populate the TreeMap
11    for (let i: number = 1; i < n; ++i) {
12        // Calculate the starting point of the line segment
13        const start: number = 2 * y[i - 1];
14        // Calculate the ending point of the line segment
15        // Adjust for the last point or if the current y is more than the previous y
16        const end: number = 2 * y[i] + (i === n - 1 ? 0 : (y[i] > y[i - 1] ? -1 : 1));
17      
18        // Merge the start points into TreeMap, incrementing the count for intersections
19        sweepLine.set(Math.min(start, end), (sweepLine.get(Math.min(start, end)) || 0) + 1);
20        // Merge the end points into TreeMap, decrementing the count for intersections
21        sweepLine.set(Math.max(start, end) + 1, (sweepLine.get(Math.max(start, end) + 1) || 0) - 1);
22    }
23
24    // Iterate through the TreeMap
25    sweepLine.forEach((count: number) => {
26        // Update the current intersection count
27        currentIntersectionCount += count;
28        // Update the maximum intersection count found so far
29        maxIntersections = Math.max(maxIntersections, currentIntersectionCount);
30    });
31
32    return maxIntersections; // Return the max number of intersections found
33}
34

Time and Space Complexity

The time complexity of the code is determined by several loops and operations on the TreeMap line.

  • The first for loop runs (n - 1) times, where n is the length of the array y.
  • Inside the loop, there are two merge operations on the TreeMap. TreeMap operations typically take O(log m) time where m is the number of keys in the map because TreeMap is implemented with a Red-Black tree, which maintains a sorted order of its keys. As there are 2 merge operations for each i in the loop, they contribute O(2*(n-1)*log m) to the time complexity.
  • The second for loop iterates through the values of the TreeMap line. In the worst case, the TreeMap can have 2*(n-1) keys if all start and end points are different. Iterating through the values therefore takes O(m) time, where m can be as large as 2*(n-1) in the worst case scenario.

Putting it all together, the total time complexity is O((n-1)*log m) + O(m). Since m is at most 2*(n-1), we can consider the complexity to be O(n log n) in the worst case.

The space complexity of the code is primarily dictated by the size of the TreeMap line.

  • In the worst case scenario, line may contain 2*(n-1) key-value pairs if each segment has a unique starting and ending point.
  • Since the TreeMap stores all the starting and ending points, its size could be O(2*(n-1)), which simplifies to O(n).

Therefore, the space complexity of the code is O(n).

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


Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings


Got a question? Ask the Monster Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.

Tired of the LeetCode Grind?

Our structured approach teaches you the patterns behind problems, so you can confidently solve any challenge. Get started now to land your dream tech job.

Get Started

🪄