1943. Describe the Painting
Problem Description
You have a long painting represented as a number line where multiple colored segments overlap. Each segment is defined by [start, end, color]
representing a half-closed interval [start, end)
painted with a unique color.
When segments overlap, their colors mix together. Mixed colors are represented as sets - for example, if colors 2, 4, and 6 overlap at a position, the mixed color is {2,4,6}
. For output simplicity, you need to return the sum of colors in each set rather than the set itself (so {2,4,6}
becomes 12
).
Your task is to describe the final painting using the minimum number of non-overlapping segments, where each segment shows the mixed color sum for that interval.
Key points:
- Input:
segments[i] = [start_i, end_i, color_i]
- Output:
painting[j] = [left_j, right_j, mix_j]
wheremix_j
is the sum of all colors in that interval - Half-closed intervals
[a, b)
include pointa
but exclude pointb
- Return only painted portions (exclude unpainted parts)
- Segments can be returned in any order
Example:
Given segments = [[1,4,5],[1,7,7]]
:
- Interval
[1,4)
has both colors 5 and 7, so the mixed sum is12
- Interval
[4,7)
has only color 7 - Output:
[[1,4,12],[4,7,7]]
The solution uses a difference array technique with a sweep line approach. It marks color changes at segment boundaries (adding color at start, subtracting at end), sorts these events, computes running sums to get the color mix at each interval, and constructs the final non-overlapping segments from consecutive events where the color sum is non-zero.
Intuition
Think about what happens when we paint overlapping segments on a number line. At any point, we need to track which colors are "active" and sum them up. The key insight is that colors only change at segment boundaries - when a segment starts or ends.
Imagine walking along the number line from left to right. As we move, the only positions where the color mix changes are:
- When a new segment starts (a color gets added to the mix)
- When a segment ends (a color gets removed from the mix)
This suggests we don't need to check every single point on the line. We only need to examine "event points" where segments begin or end.
Consider the example [[1,4,5],[1,7,7]]
:
- At position 1: both colors 5 and 7 start (total: +5, +7)
- At position 4: color 5 ends (total: -5)
- At position 7: color 7 ends (total: -7)
If we track these changes using a difference array concept, we can mark +color
when a segment starts and -color
when it ends. Then by maintaining a running sum as we sweep through these events, we automatically get the correct color mix for each interval.
The approach becomes:
- Mark all color changes at boundary points in a map/dictionary
- Sort these boundary points
- Calculate the running sum between consecutive boundaries
- Each pair of consecutive boundaries with a non-zero sum forms a segment in our answer
This efficiently handles all overlaps without explicitly checking every possible position, reducing the problem to processing only the critical points where changes occur.
Learn more about Prefix Sum and Sorting patterns.
Solution Approach
The solution implements a sweep line algorithm with a difference array technique:
Step 1: Build the difference array
d = defaultdict(int)
for l, r, c in segments:
d[l] += c
d[r] -= c
We use a dictionary d
to track color changes at boundary points. For each segment [l, r, c]
:
- Add color
c
at the start positionl
(color becomes active) - Subtract color
c
at the end positionr
(color becomes inactive)
This leverages the half-closed interval property - color is active at l
but not at r
.
Step 2: Sort the boundary points
s = sorted([[k, v] for k, v in d.items()])
Convert the dictionary to a sorted list of [position, color_change]
pairs. Sorting ensures we process events from left to right along the number line.
Step 3: Calculate running sums
n = len(s)
for i in range(1, n):
s[i][1] += s[i - 1][1]
Transform each color change into the actual color sum at that position using prefix sums. After this step, s[i][1]
contains the total color sum for the interval starting at position s[i][0]
.
Step 4: Build the result segments
return [[s[i][0], s[i + 1][0], s[i][1]] for i in range(n - 1) if s[i][1]]
Create segments from consecutive boundary points:
- Left boundary:
s[i][0]
- Right boundary:
s[i + 1][0]
- Color sum:
s[i][1]
The condition if s[i][1]
filters out intervals with zero color sum (unpainted regions).
Time Complexity: O(n log n)
where n
is the number of segments, dominated by sorting.
Space Complexity: O(n)
for storing the boundary points and their color changes.
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 trace through the algorithm with segments = [[1,4,5],[3,6,7],[5,8,3]]
:
Step 1: Build the difference array
For each segment, we mark color changes:
[1,4,5]
: Add 5 at position 1, subtract 5 at position 4[3,6,7]
: Add 7 at position 3, subtract 7 at position 6[5,8,3]
: Add 3 at position 5, subtract 3 at position 8
Our difference array d
:
d = {1: 5, 4: -5, 3: 7, 6: -7, 5: 3, 8: -3}
Step 2: Sort the boundary points
Convert to sorted list:
s = [[1, 5], [3, 7], [4, -5], [5, 3], [6, -7], [8, -3]]
Step 3: Calculate running sums
Transform each position to show the actual color sum starting from that position:
- Position 1: sum = 5
- Position 3: sum = 5 + 7 = 12
- Position 4: sum = 12 + (-5) = 7
- Position 5: sum = 7 + 3 = 10
- Position 6: sum = 10 + (-7) = 3
- Position 8: sum = 3 + (-3) = 0
After running sums:
s = [[1, 5], [3, 12], [4, 7], [5, 10], [6, 3], [8, 0]]
Step 4: Build result segments
Create segments from consecutive pairs where color sum > 0:
[1, 3)
with color sum 5[3, 4)
with color sum 12[4, 5)
with color sum 7[5, 6)
with color sum 10[6, 8)
with color sum 3- Skip
[8, ?)
since color sum is 0
Final result: [[1,3,5], [3,4,12], [4,5,7], [5,6,10], [6,8,3]]
This gives us the minimum number of non-overlapping segments describing the painted regions with their mixed color sums.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def splitPainting(self, segments: List[List[int]]) -> List[List[int]]:
6 # Create a difference array to track color changes at segment boundaries
7 color_changes = defaultdict(int)
8
9 # For each segment, add color at start and subtract at end
10 for start, end, color in segments:
11 color_changes[start] += color
12 color_changes[end] -= color
13
14 # Sort positions and their color changes by position
15 sorted_positions = sorted([[position, change] for position, change in color_changes.items()])
16
17 # Calculate prefix sum to get actual color mix at each position
18 num_positions = len(sorted_positions)
19 for i in range(1, num_positions):
20 sorted_positions[i][1] += sorted_positions[i - 1][1]
21
22 # Build result segments, excluding segments with 0 color mix
23 result = []
24 for i in range(num_positions - 1):
25 if sorted_positions[i][1]: # Only include segments with non-zero color
26 result.append([
27 sorted_positions[i][0], # start position
28 sorted_positions[i + 1][0], # end position
29 sorted_positions[i][1] # color mix value
30 ])
31
32 return result
33
1class Solution {
2 public List<List<Long>> splitPainting(int[][] segments) {
3 // TreeMap to store position changes in color values (sorted by position)
4 // Key: position, Value: color change at that position
5 TreeMap<Integer, Long> colorChanges = new TreeMap<>();
6
7 // Process each segment and record color changes at boundaries
8 for (int[] segment : segments) {
9 int start = segment[0];
10 int end = segment[1];
11 int color = segment[2];
12
13 // Add color at start position
14 colorChanges.put(start, colorChanges.getOrDefault(start, 0L) + color);
15 // Remove color at end position
16 colorChanges.put(end, colorChanges.getOrDefault(end, 0L) - color);
17 }
18
19 // Result list to store non-overlapping painted intervals
20 List<List<Long>> result = new ArrayList<>();
21
22 // Variables to track current interval and color sum
23 long intervalStart = 0;
24 long intervalEnd = 0;
25 long currentColorSum = 0;
26
27 // Process each position where color changes occur
28 for (Map.Entry<Integer, Long> entry : colorChanges.entrySet()) {
29 // Skip the first entry to initialize intervalStart
30 if (Objects.equals(entry.getKey(), colorChanges.firstKey())) {
31 intervalStart = entry.getKey();
32 } else {
33 // Set the end of current interval
34 intervalEnd = entry.getKey();
35
36 // Add interval to result if it has positive color
37 if (currentColorSum > 0) {
38 result.add(Arrays.asList(intervalStart, intervalEnd, currentColorSum));
39 }
40
41 // Move to next interval
42 intervalStart = intervalEnd;
43 }
44
45 // Update current color sum with the change at this position
46 currentColorSum += entry.getValue();
47 }
48
49 return result;
50 }
51}
52
1class Solution {
2public:
3 vector<vector<long long>> splitPainting(vector<vector<int>>& segments) {
4 // Use a map to store color changes at each position
5 // Key: position, Value: color change (positive for start, negative for end)
6 map<int, long long> colorChanges;
7
8 // Process each segment and record color changes
9 for (auto& segment : segments) {
10 int startPos = segment[0];
11 int endPos = segment[1];
12 int color = segment[2];
13
14 // Add color at start position
15 colorChanges[startPos] += color;
16 // Remove color at end position
17 colorChanges[endPos] -= color;
18 }
19
20 // Result vector to store the painted segments
21 vector<vector<long long>> result;
22
23 // Variables to track current interval and color sum
24 long long intervalStart = 0;
25 long long intervalEnd = 0;
26 long long currentColorSum = 0;
27
28 // Process each position with color change
29 for (auto& [position, colorChange] : colorChanges) {
30 // Handle the first position
31 if (position == colorChanges.begin()->first) {
32 intervalStart = position;
33 } else {
34 // Set the end of current interval
35 intervalEnd = position;
36
37 // Add interval to result if it has non-zero color
38 if (currentColorSum > 0) {
39 result.push_back({intervalStart, intervalEnd, currentColorSum});
40 }
41
42 // Start new interval from current position
43 intervalStart = intervalEnd;
44 }
45
46 // Update the current color sum
47 currentColorSum += colorChange;
48 }
49
50 return result;
51 }
52};
53
1function splitPainting(segments: number[][]): number[][] {
2 // Use a map to store color changes at each position
3 // Key: position, Value: color change (positive for start, negative for end)
4 const colorChanges = new Map<number, number>();
5
6 // Process each segment and record color changes
7 for (const segment of segments) {
8 const startPos = segment[0];
9 const endPos = segment[1];
10 const color = segment[2];
11
12 // Add color at start position
13 colorChanges.set(startPos, (colorChanges.get(startPos) || 0) + color);
14 // Remove color at end position
15 colorChanges.set(endPos, (colorChanges.get(endPos) || 0) - color);
16 }
17
18 // Sort positions to process them in order
19 const sortedPositions = Array.from(colorChanges.keys()).sort((a, b) => a - b);
20
21 // Result array to store the painted segments
22 const result: number[][] = [];
23
24 // Variables to track current interval and color sum
25 let intervalStart = 0;
26 let currentColorSum = 0;
27 let isFirstPosition = true;
28
29 // Process each position with color change
30 for (const position of sortedPositions) {
31 // Handle the first position
32 if (isFirstPosition) {
33 intervalStart = position;
34 isFirstPosition = false;
35 } else {
36 // Set the end of current interval
37 const intervalEnd = position;
38
39 // Add interval to result if it has non-zero color
40 if (currentColorSum > 0) {
41 result.push([intervalStart, intervalEnd, currentColorSum]);
42 }
43
44 // Start new interval from current position
45 intervalStart = intervalEnd;
46 }
47
48 // Update the current color sum
49 currentColorSum += colorChanges.get(position) || 0;
50 }
51
52 return result;
53}
54
Time and Space Complexity
Time Complexity: O(n log n)
where n
is the number of segments.
- Creating the dictionary
d
and iterating through all segments takesO(n)
time, where each segment contributes at most 2 entries (start and end points). - Converting the dictionary to a list of key-value pairs takes
O(k)
time wherek
is the number of unique points, andk ≤ 2n
. - Sorting the list
s
takesO(k log k)
time, which isO(n log n)
in the worst case sincek ≤ 2n
. - The prefix sum calculation loop takes
O(k)
time, which isO(n)
. - Building the final result list takes
O(k)
time, which isO(n)
. - Overall:
O(n) + O(n log n) + O(n) + O(n) = O(n log n)
.
Space Complexity: O(n)
- The dictionary
d
stores at most2n
entries (start and end points for each segment), requiringO(n)
space. - The sorted list
s
contains at most2n
elements, requiringO(n)
space. - The result list contains at most
2n - 1
intervals in the worst case, requiringO(n)
space. - Overall space complexity is
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Integer Overflow with Large Color Values
Problem: When multiple segments with large color values overlap, their sum might exceed the maximum integer limit. For instance, if you have many segments each with color value near 10^9
overlapping at the same position, the sum could overflow.
Solution:
- In Python, integers have arbitrary precision so this isn't an issue
- In languages like Java or C++, use
long
orlong long
data types:
// C++ example map<long long, long long> color_changes; vector<vector<long long>> result;
Pitfall 2: Forgetting to Handle Empty Segments (Zero Color Sum)
Problem: After calculating prefix sums, some intervals might have a color sum of 0 (where all colors have ended). Including these would violate the requirement to "exclude unpainted parts."
Incorrect approach:
# Wrong: includes segments with 0 color
return [[sorted_positions[i][0], sorted_positions[i + 1][0], sorted_positions[i][1]]
for i in range(num_positions - 1)]
Solution: Always check if the color sum is non-zero before adding to result:
# Correct: filters out zero-color segments if sorted_positions[i][1]: # Only non-zero color sums result.append([...])
Pitfall 3: Misunderstanding Half-Closed Intervals
Problem: Treating intervals as closed [start, end]
instead of half-closed [start, end)
leads to incorrect color mixing at boundary points.
Example mistake:
# Wrong: treating end as inclusive for start, end, color in segments: color_changes[start] += color color_changes[end + 1] -= color # Incorrect!
Solution: Remember that the end point is exclusive:
# Correct: end is already exclusive in [start, end) for start, end, color in segments: color_changes[start] += color color_changes[end] -= color # Color stops at end, not end+1
Pitfall 4: Not Handling Adjacent Segments with Same Color Sum
Problem: The algorithm naturally merges adjacent segments with the same color sum, which is correct. However, some might incorrectly try to keep them separate or add extra logic to merge them.
Solution: The algorithm already handles this correctly - consecutive boundary points naturally create minimal segments. No additional merging logic is needed.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!