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:
- Each section contains at least one rectangle
- 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.
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
reaches0
, we've completely exited a group of connected rectangles, so we incrementlines
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 EvaluatorExample 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
andx_coordinates
lists. Each rectangle contributes 2 points to each list, resulting inO(n)
time. - Sorting: Both
y_coordinates
andx_coordinates
lists contain2n
elements each. Sorting these lists takesO(2n log(2n)) = O(n log n)
time for each list. - Counting line intersections: The
countLineIntersections
method iterates through2n
coordinates once, takingO(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 stores2n
tuples (start and end points for each rectangle's y-coordinates):O(n)
space. - The
x_coordinates
list stores2n
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) useO(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.
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!