Facebook Pixel

3394. Check if Grid can be Cut into Sections

Problem Description

You have an n x n grid where the origin is at the bottom-left corner. You're given a list of non-overlapping rectangles on this grid, where each rectangle is defined by its bottom-left corner (start_x, start_y) and top-right corner (end_x, end_y).

Your task is to determine if you can make exactly two cuts on the grid (either both horizontal or both vertical) that will divide the grid into three sections such that:

  1. Each section contains at least one rectangle
  2. Each rectangle belongs to exactly one section (no rectangle is split by the cuts)

The solution uses an interval merging approach. For each rectangle, it tracks the start and end coordinates along both x and y axes. By sorting these coordinates and using a sweep line technique with an overlap counter, it determines how many distinct "segments" or groups of rectangles exist along each axis.

The key insight is that if rectangles can be separated into at least 3 groups along either the x-axis (vertical cuts) or y-axis (horizontal cuts), then two cuts can successfully divide them into three sections. The countLineIntersections function counts these groups by tracking when the overlap becomes zero - each time this happens, it indicates a gap between rectangle groups where a cut could be placed.

The algorithm returns true if either the horizontal direction (y-coordinates) or vertical direction (x-coordinates) has at least 3 distinct groups, meaning two cuts in that direction would create the required three sections.

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

Intuition

The key realization is that we need to find two parallel cuts (either both horizontal or both vertical) that separate all rectangles into three groups without cutting through any rectangle.

Think about what happens when we project all rectangles onto a single axis. If we project onto the x-axis, we get intervals [x1, x2] for each rectangle. Similarly, projecting onto the y-axis gives us intervals [y1, y2]. The question becomes: can we find two points along one of these axes that separate the intervals into three non-overlapping groups?

If rectangles don't overlap in their projections on an axis, there are natural "gaps" between groups of rectangles. These gaps are the only places where we can make cuts. For example, if we have rectangles at x-coordinates [1,3], [2,4], [6,8], and [10,12], the first two rectangles overlap (forming one group), while the third and fourth rectangles are separate. This gives us three groups with two gaps where cuts can be placed.

The clever part is recognizing that we can detect these groups using a sweep line algorithm with an overlap counter. As we sweep across the axis:

  • When we encounter a rectangle start, we increment the overlap counter
  • When we encounter a rectangle end, we decrement the overlap counter
  • When the counter reaches zero, it means we've completely passed through a group of overlapping rectangles

Each time the counter returns to zero represents the end of one connected component of rectangles. If we have at least 3 such components along either axis, we can place our two cuts in the gaps between them.

This is why the solution tracks start/end markers (coordinate, 1) for starts and (coordinate, 0) for ends, sorts them, and counts how many times the overlap drops to zero. Finding 3+ groups on either axis means we can make valid cuts in that direction.

Learn more about Sorting patterns.

Solution Approach

The solution implements a sweep line algorithm with interval merging to detect separable groups of rectangles along each axis.

Step 1: Extract and Mark Coordinates

For each rectangle [x1, y1, x2, y2], we extract:

  • Y-coordinates: (y1, 1) for start and (y2, 0) for end
  • X-coordinates: (x1, 1) for start and (x2, 0) for end

The marker 1 indicates the beginning of an interval, and 0 indicates the end. This allows us to track when we enter and leave rectangle boundaries.

Step 2: Sort Coordinates

Both coordinate lists are sorted using sort(key=lambda x: (x[0], x[1])). This ensures:

  • Primary sorting by coordinate value
  • When coordinates are equal, ends (0) come before starts (1)

The second criterion is crucial: when one rectangle ends exactly where another begins (touching but not overlapping), we process the end first, allowing the overlap counter to reach zero and correctly identify them as separate groups.

Step 3: Count Line Intersections (Groups)

The countLineIntersections method processes the sorted coordinates:

lines = 0
overlap = 0
for value, marker in coordinates:
    if marker == 0:  # End of rectangle
        overlap -= 1
    else:  # Start of rectangle (marker == 1)
        overlap += 1
  
    if overlap == 0:
        lines += 1

The overlap counter tracks how many rectangles we're currently "inside":

  • When we hit a start marker, we increment overlap
  • When we hit an end marker, we decrement overlap
  • When overlap reaches 0, we've completely exited a group of connected rectangles, so we increment lines

Step 4: Check Both Directions

The solution returns true if either direction has at least 3 groups:

return self.countLineIntersections(y_coordinates) or self.countLineIntersections(x_coordinates)

If we find 3+ groups along the y-axis, we can make two horizontal cuts between them. If we find 3+ groups along the x-axis, we can make two vertical cuts between them. Either condition satisfies the problem requirements.

Time Complexity: O(n log n) where n is the number of rectangles, due to sorting. Space Complexity: O(n) for storing the coordinate lists.

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 4 rectangles on a 10Γ—10 grid:

  • Rectangle A: [1, 1, 3, 4] (bottom-left at (1,1), top-right at (3,4))
  • Rectangle B: [2, 2, 4, 5] (overlaps with A)
  • Rectangle C: [6, 1, 8, 3] (separated from A and B)
  • Rectangle D: [7, 6, 9, 8] (separated from all others)

Step 1: Extract Coordinates

For Y-coordinates (horizontal cuts):

  • Rectangle A: (1, 1) start, (4, 0) end
  • Rectangle B: (2, 1) start, (5, 0) end
  • Rectangle C: (1, 1) start, (3, 0) end
  • Rectangle D: (6, 1) start, (8, 0) end

Y-coordinates list: [(1,1), (4,0), (2,1), (5,0), (1,1), (3,0), (6,1), (8,0)]

For X-coordinates (vertical cuts):

  • Rectangle A: (1, 1) start, (3, 0) end
  • Rectangle B: (2, 1) start, (4, 0) end
  • Rectangle C: (6, 1) start, (8, 0) end
  • Rectangle D: (7, 1) start, (9, 0) end

X-coordinates list: [(1,1), (3,0), (2,1), (4,0), (6,1), (8,0), (7,1), (9,0)]

Step 2: Sort Coordinates

Y-coordinates sorted: [(1,1), (1,1), (2,1), (3,0), (4,0), (5,0), (6,1), (8,0)]

X-coordinates sorted: [(1,1), (2,1), (3,0), (4,0), (6,1), (7,1), (8,0), (9,0)]

Step 3: Count Groups for Y-coordinates

Processing sorted Y-coordinates:

  • (1,1): overlap = 1, lines = 0
  • (1,1): overlap = 2, lines = 0 (A and C both start at y=1)
  • (2,1): overlap = 3, lines = 0 (B starts)
  • (3,0): overlap = 2, lines = 0 (C ends)
  • (4,0): overlap = 1, lines = 0 (A ends)
  • (5,0): overlap = 0, lines = 1 βœ“ (B ends - first group complete!)
  • (6,1): overlap = 1, lines = 1 (D starts)
  • (8,0): overlap = 0, lines = 2 βœ“ (D ends - second group complete!)

Result: 2 groups along Y-axis (not enough for 3 sections)

Step 4: Count Groups for X-coordinates

Processing sorted X-coordinates:

  • (1,1): overlap = 1, lines = 0 (A starts)
  • (2,1): overlap = 2, lines = 0 (B starts, overlaps with A)
  • (3,0): overlap = 1, lines = 0 (A ends)
  • (4,0): overlap = 0, lines = 1 βœ“ (B ends - first group complete!)
  • (6,1): overlap = 1, lines = 1 (C starts)
  • (7,1): overlap = 2, lines = 1 (D starts, overlaps with C)
  • (8,0): overlap = 1, lines = 1 (C ends)
  • (9,0): overlap = 0, lines = 2 βœ“ (D ends - second group complete!)

Result: 2 groups along X-axis (not enough for 3 sections)

Final Result: Since neither axis has 3+ groups, return false. We cannot make two cuts to create three sections.

Alternative Example for Success: If Rectangle D was at [1, 6, 3, 8] instead, the Y-coordinates would yield 3 groups:

  • Group 1: Rectangles A, B, C (y: 1-5)
  • Group 2: Rectangle D (y: 6-8)
  • Wait, that's still only 2 groups...

Let me correct with a better example. If we had rectangles at:

  • A: [1, 1, 2, 2]
  • B: [1, 4, 2, 5]
  • C: [1, 7, 2, 8]

The Y-coordinate analysis would show 3 distinct groups with gaps at y=3 and y=6, allowing two horizontal cuts to create three sections. The function would return true.

Solution Implementation

1class Solution:
2    def countLineIntersections(self, coordinates: List[tuple[int, int]]) -> bool:
3        """
4        Count the number of non-overlapping segments formed by the given coordinates.
5        Returns True if there are at least 3 segments (allowing for 2 cuts).
6      
7        Args:
8            coordinates: List of (position, marker) tuples where marker is 1 for start, 0 for end
9          
10        Returns:
11            bool: True if at least 3 non-overlapping segments exist
12        """
13        segment_count = 0
14        active_rectangles = 0
15      
16        for position, marker in coordinates:
17            # marker == 0 indicates end of a rectangle
18            if marker == 0:
19                active_rectangles -= 1
20            # marker == 1 indicates start of a rectangle
21            else:
22                active_rectangles += 1
23          
24            # When no rectangles are active, we've completed a segment
25            if active_rectangles == 0:
26                segment_count += 1
27      
28        # Need at least 3 segments to make 2 cuts
29        return segment_count >= 3
30  
31    def checkValidCuts(self, n: int, rectangles: List[List[int]]) -> bool:
32        """
33        Check if it's possible to make 2 cuts (horizontal or vertical) that divide all rectangles.
34      
35        Args:
36            n: Grid size (not used in current implementation)
37            rectangles: List of rectangles, each as [x1, y1, x2, y2]
38          
39        Returns:
40            bool: True if 2 valid cuts can be made horizontally or vertically
41        """
42        # Store start and end coordinates for horizontal and vertical projections
43        horizontal_events = []  # For horizontal cuts (using y-coordinates)
44        vertical_events = []    # For vertical cuts (using x-coordinates)
45      
46        # Process each rectangle
47        for rectangle in rectangles:
48            x1, y1, x2, y2 = rectangle
49          
50            # Add horizontal projection events (y-coordinates)
51            horizontal_events.append((y1, 1))  # Rectangle starts at y1
52            horizontal_events.append((y2, 0))  # Rectangle ends at y2
53          
54            # Add vertical projection events (x-coordinates)
55            vertical_events.append((x1, 1))  # Rectangle starts at x1
56            vertical_events.append((x2, 0))  # Rectangle ends at x2
57      
58        # Sort events by coordinate value
59        # When coordinates are equal, process ends (0) before starts (1)
60        horizontal_events.sort(key=lambda event: (event[0], event[1]))
61        vertical_events.sort(key=lambda event: (event[0], event[1]))
62      
63        # Check if either horizontal or vertical cuts are valid
64        return (self.countLineIntersections(horizontal_events) or 
65                self.countLineIntersections(vertical_events))
66
1class Solution {
2    /**
3     * Helper class to represent a coordinate with its type (start or end)
4     */
5    static class Pair {
6        int value;  // The coordinate value
7        int type;   // 1 for start, 0 for end
8      
9        Pair(int value, int type) {
10            this.value = value;
11            this.type = type;
12        }
13    }
14  
15    /**
16     * Counts the number of non-overlapping line segments formed by the coordinates
17     * @param coordinates List of coordinate pairs with their types
18     * @return true if there are at least 3 non-overlapping segments, false otherwise
19     */
20    private boolean countLineIntersections(List<Pair> coordinates) {
21        int segmentCount = 0;  // Number of complete non-overlapping segments
22        int activeRectangles = 0;  // Number of currently overlapping rectangles
23      
24        // Process each coordinate event
25        for (Pair coordinate : coordinates) {
26            if (coordinate.type == 0) {
27                // End of a rectangle
28                activeRectangles--;
29            } else {
30                // Start of a rectangle
31                activeRectangles++;
32            }
33          
34            // When no rectangles are active, we've completed a segment
35            if (activeRectangles == 0) {
36                segmentCount++;
37            }
38        }
39      
40        // Check if we have at least 3 segments (allowing for 2 cuts)
41        return segmentCount >= 3;
42    }
43  
44    /**
45     * Checks if valid cuts can be made to separate rectangles
46     * @param n Number of rectangles (not used in current implementation)
47     * @param rectangles Array of rectangles, each as [x1, y1, x2, y2]
48     * @return true if valid cuts exist, false otherwise
49     */
50    public boolean checkValidCuts(int n, int[][] rectangles) {
51        // Lists to store y-coordinates and x-coordinates separately
52        List<Pair> yCoordinates = new ArrayList<>();
53        List<Pair> xCoordinates = new ArrayList<>();
54      
55        // Extract coordinates from each rectangle
56        for (int[] rectangle : rectangles) {
57            // Rectangle format: [x1, y1, x2, y2]
58            // Add y-coordinates (for horizontal cuts)
59            yCoordinates.add(new Pair(rectangle[1], 1));  // y1 (bottom), start
60            yCoordinates.add(new Pair(rectangle[3], 0));  // y2 (top), end
61          
62            // Add x-coordinates (for vertical cuts)
63            xCoordinates.add(new Pair(rectangle[0], 1));  // x1 (left), start
64            xCoordinates.add(new Pair(rectangle[2], 0));  // x2 (right), end
65        }
66      
67        // Define comparator: sort by value first, then by type (ends before starts)
68        Comparator<Pair> coordinateComparator = (a, b) -> {
69            if (a.value != b.value) {
70                return Integer.compare(a.value, b.value);
71            }
72            // When values are equal, process ends (0) before starts (1)
73            return Integer.compare(a.type, b.type);
74        };
75      
76        // Sort both coordinate lists
77        Collections.sort(yCoordinates, coordinateComparator);
78        Collections.sort(xCoordinates, coordinateComparator);
79      
80        // Check if either horizontal or vertical cuts are valid
81        return countLineIntersections(yCoordinates) || countLineIntersections(xCoordinates);
82    }
83}
84
1class Solution {
2public:
3    bool checkValidCuts(int n, vector<vector<int>>& rectangles) {
4        // Create event points for vertical and horizontal sweeps
5        vector<pair<int, int>> verticalEvents, horizontalEvents;
6      
7        // For each rectangle, create start and end events
8        // Event type: 1 for rectangle start, 0 for rectangle end
9        for (const auto& rectangle : rectangles) {
10            // Vertical sweep events (using y-coordinates)
11            verticalEvents.push_back({rectangle[1], 1});  // bottom edge (start)
12            verticalEvents.push_back({rectangle[3], 0});  // top edge (end)
13          
14            // Horizontal sweep events (using x-coordinates)
15            horizontalEvents.push_back({rectangle[0], 1}); // left edge (start)
16            horizontalEvents.push_back({rectangle[2], 0}); // right edge (end)
17        }
18      
19        // Sort events by coordinate position
20        sort(verticalEvents.begin(), verticalEvents.end());
21        sort(horizontalEvents.begin(), horizontalEvents.end());
22      
23        // Check if we can make at least 2 cuts in either direction
24        // (which creates at least 3 segments)
25        return countValidCuts(verticalEvents) || countValidCuts(horizontalEvents);
26    }
27  
28private:
29    bool countValidCuts(vector<pair<int, int>>& events) {
30        int activeRectangles = 0;  // Number of overlapping rectangles at current position
31        int validCuts = 0;         // Number of valid cut positions found
32      
33        // Sweep through all events
34        for (const auto& event : events) {
35            if (event.second == 0) {
36                // Rectangle end event
37                activeRectangles--;
38            } else {
39                // Rectangle start event
40                activeRectangles++;
41            }
42          
43            // When no rectangles are active, we found a valid cut position
44            if (activeRectangles == 0) {
45                validCuts++;
46            }
47        }
48      
49        // Need at least 3 segments (2 cuts) to divide into 3 parts
50        return validCuts >= 3;
51    }
52};
53
1/**
2 * Checks if it's possible to make exactly 2 cuts (resulting in 3 sections) 
3 * that don't intersect with any rectangles
4 * @param n - The size of the grid
5 * @param rectangles - Array of rectangles, each defined as [x1, y1, x2, y2]
6 * @returns true if valid cuts can be made, false otherwise
7 */
8function checkValidCuts(n: number, rectangles: number[][]): boolean {
9    /**
10     * Helper function to check if cuts can be made in one direction
11     * @param sortedRectangles - Rectangles sorted by start coordinate
12     * @param extractCoordinates - Function to extract relevant coordinates from rectangle
13     * @returns true if 2 cuts can be made without intersecting rectangles
14     */
15    const checkDirection = (
16        sortedRectangles: number[][], 
17        extractCoordinates: (rectangle: number[]) => number[]
18    ): boolean => {
19        // Need to make 2 cuts (resulting in 3 sections)
20        let cutsRemaining = 3;
21        // Track the rightmost/bottommost point of current group of overlapping rectangles
22        let maxEndCoordinate = 0;
23
24        for (const rectangle of sortedRectangles) {
25            const [startCoordinate, endCoordinate] = extractCoordinates(rectangle);
26
27            if (startCoordinate < maxEndCoordinate) {
28                // Current rectangle overlaps with previous group, extend the group
29                maxEndCoordinate = Math.max(maxEndCoordinate, endCoordinate);
30            } else {
31                // No overlap, can place a cut here before this rectangle
32                maxEndCoordinate = endCoordinate;
33                cutsRemaining--;
34              
35                // Successfully placed all required cuts
36                if (cutsRemaining === 0) {
37                    return true;
38                }
39            }
40        }
41
42        return false;
43    };
44
45    // Comparator to sort rectangles by x1 coordinate
46    const sortByXCoordinate = (rectA: number[], rectB: number[]): number => {
47        return rectA[0] - rectB[0];
48    };
49
50    // Comparator to sort rectangles by y1 coordinate
51    const sortByYCoordinate = (rectA: number[], rectB: number[]): number => {
52        return rectA[1] - rectB[1];
53    };
54
55    // Extract x coordinates [x1, x2] from rectangle [x1, y1, x2, y2]
56    const extractXCoordinates = (rectangle: number[]): number[] => {
57        return [rectangle[0], rectangle[2]];
58    };
59
60    // Extract y coordinates [y1, y2] from rectangle [x1, y1, x2, y2]
61    const extractYCoordinates = (rectangle: number[]): number[] => {
62        return [rectangle[1], rectangle[3]];
63    };
64
65    // Check if valid cuts can be made vertically (along x-axis) or horizontally (along y-axis)
66    return checkDirection(rectangles.toSorted(sortByXCoordinate), extractXCoordinates) || 
67           checkDirection(rectangles.toSorted(sortByYCoordinate), extractYCoordinates);
68}
69

Time and Space Complexity

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

  • Creating coordinate lists: We iterate through all rectangles once to create y_coordinates and x_coordinates lists. Each rectangle contributes 2 points to each list, resulting in O(n) time.
  • Sorting: Both y_coordinates and x_coordinates lists contain 2n elements each. Sorting these lists takes O(2n log(2n)) = O(n log n) time for each list.
  • Counting line intersections: The countLineIntersections method iterates through 2n coordinates once, taking O(n) time. This method is called twice (once for each dimension).
  • Overall: O(n) + O(n log n) + O(n log n) + O(n) + O(n) = O(n log n)

Space Complexity: O(n) where n is the number of rectangles.

  • The y_coordinates list stores 2n tuples (start and end points for each rectangle's y-coordinates): O(n) space.
  • The x_coordinates list stores 2n tuples (start and end points for each rectangle's x-coordinates): O(n) space.
  • The sorting operations may use additional O(n) space depending on the implementation (Python's Timsort).
  • Other variables (lines, overlap, loop variables) use O(1) space.
  • Overall: O(n) + O(n) + O(n) = O(n)

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

Common Pitfalls

1. Incorrect Sorting Order for Equal Coordinates

The Pitfall: A critical mistake is sorting the events incorrectly when coordinates are equal. If you sort so that starts (1) come before ends (0) at the same coordinate, the algorithm will fail to detect separate groups when rectangles are touching but not overlapping.

Example of the bug:

# WRONG: This will fail for touching rectangles
coordinates.sort(key=lambda x: x[0])  # Only sorts by position
# or
coordinates.sort(key=lambda x: (x[0], -x[1]))  # Starts before ends

Consider rectangles [0,0,2,2] and [2,0,4,2] that touch at x=2:

  • With incorrect sorting: [(0,1), (2,1), (2,0), (4,0)]
  • The overlap counter goes: 0β†’1β†’2β†’1β†’0 (never reaches 0 in the middle)
  • Result: Counted as 1 group instead of 2

The Solution:

# CORRECT: Ends must be processed before starts at the same position
coordinates.sort(key=lambda x: (x[0], x[1]))

This ensures:

  • Correct sorting: [(0,1), (2,0), (2,1), (4,0)]
  • The overlap counter goes: 0β†’1β†’0β†’1β†’0 (reaches 0 at x=2)
  • Result: Correctly counted as 2 groups

2. Counting Segments vs. Counting Gaps

The Pitfall: Confusing whether to count the number of segments (groups of rectangles) or the number of gaps between segments. The algorithm counts segments when overlap == 0, but developers might mistakenly try to count gaps.

Example of the bug:

# WRONG: Trying to count gaps instead of segments
def countLineIntersections(self, coordinates):
    gaps = 0
    overlap = 0
    was_zero = True
  
    for position, marker in coordinates:
        if marker == 0:
            overlap -= 1
        else:
            if was_zero:  # Thinking this is a new gap
                gaps += 1
            overlap += 1
            was_zero = False
      
        if overlap == 0:
            was_zero = True
  
    return gaps >= 2  # Wrong logic

The Solution: Count segments (groups) directly when overlap reaches zero. Remember: 3 segments means 2 possible cut positions between them.

3. Off-by-One Error in Segment Counting

The Pitfall: Incorrectly checking for >= 2 segments instead of >= 3, forgetting that two cuts create three sections.

Example of the bug:

# WRONG: Checking for 2 segments instead of 3
return segment_count >= 2  # This would only allow for 1 cut!

The Solution:

# CORRECT: Need at least 3 segments for 2 cuts
return segment_count >= 3

Remember the relationship: n cuts create n+1 sections, so 2 cuts require 3 segments.

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

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

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

Load More