2021. Brightest Position on Street 🔒
Problem Description
You have a straight street represented as a number line with street lamps placed at various positions. Each street lamp is described by two values: its position on the number line and its range (how far its light reaches).
Given a 2D array lights
where each element lights[i] = [position_i, range_i]
:
position_i
is where the street lamp is locatedrange_i
is how far the light extends in both directions- The lamp illuminates the area from
[position_i - range_i, position_i + range_i]
(inclusive)
The brightness at any position on the street is defined as the number of street lamps that illuminate that position. Multiple lamps can overlap and illuminate the same area, making it brighter.
Your task is to find the position on the street with the maximum brightness. If multiple positions have the same maximum brightness, return the smallest position.
For example, if a lamp is at position 5
with range 3
, it illuminates positions [2, 3, 4, 5, 6, 7, 8]
. If another lamp at position 7
with range 2
illuminates [5, 6, 7, 8, 9]
, then positions 5, 6, 7, 8
would have brightness 2
(illuminated by both lamps), while other positions would have brightness 1
or 0
.
Intuition
The naive approach would be to check every possible position on the street and count how many lamps illuminate it. However, this could be inefficient if the range of positions is large.
Instead, we can observe that the brightness only changes at specific points - when we enter or exit the range of a lamp. Between these critical points, the brightness remains constant.
Think of it like this: when we walk along the street from left to right, the brightness increases by 1 when we enter a lamp's illuminated area (at position position_i - range_i
), and decreases by 1 when we exit it (just after position position_i + range_i
).
This observation leads us to use a difference array technique. Rather than tracking the brightness at every single position, we only track the changes in brightness:
- Mark
+1
at the start of each lamp's range - Mark
-1
at the position right after the end of each lamp's range
By accumulating these changes as we scan from left to right, we can reconstruct the actual brightness at any position. Since we only need to process positions where changes occur, we use a hash table to store these change points efficiently.
After collecting all the change points, we sort them by position and traverse from left to right, maintaining a running sum of brightness. Whenever we encounter a new maximum brightness, we record that position. Since we're traversing in ascending order, we automatically get the smallest position when there are ties.
Learn more about Prefix Sum and Sorting patterns.
Solution Approach
The solution implements the difference array technique using a hash table and sorting:
Step 1: Build the Difference Array
We use a defaultdict(int)
to store the brightness changes at specific positions. For each lamp at [position_i, range_i]
:
- Calculate the left boundary:
l = position_i - range_i
- Calculate the right boundary:
r = position_i + range_i
- Mark
+1
at positionl
(entering the illuminated area) - Mark
-1
at positionr + 1
(exiting the illuminated area)
d = defaultdict(int)
for i, j in lights:
l, r = i - j, i + j
d[l] += 1
d[r + 1] -= 1
Step 2: Process Positions in Sorted Order
We sort the positions where changes occur and traverse them from left to right:
- Initialize variables:
ans = 0
(answer position),s = 0
(current brightness),mx = 0
(maximum brightness) - For each position
k
in sorted order:- Add the change value to the running sum:
s += d[k]
- If current brightness
s
exceeds the maximummx
:- Update maximum:
mx = s
- Record this position as the answer:
ans = k
- Update maximum:
- Add the change value to the running sum:
ans = s = mx = 0
for k in sorted(d):
s += d[k]
if mx < s:
mx = s
ans = k
Why This Works:
- The hash table ensures we only process positions where brightness changes, making the solution efficient even for large coordinate ranges
- Sorting the positions ensures we check them in ascending order, automatically giving us the smallest position when there are ties
- The running sum
s
accurately tracks the brightness at each change point because we've recorded all entries and exits from lamp ranges
Time Complexity: O(n log n)
where n
is the number of lamps, due to sorting the change points.
Space Complexity: O(n)
for storing the difference array in the hash table.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with lights = [[5, 3], [7, 2]]
.
Step 1: Understanding the lamp coverage
- Lamp 1 at position 5 with range 3 illuminates
[2, 3, 4, 5, 6, 7, 8]
- Lamp 2 at position 7 with range 2 illuminates
[5, 6, 7, 8, 9]
Step 2: Build the difference array
For lamp [5, 3]
:
- Left boundary:
5 - 3 = 2
- Right boundary:
5 + 3 = 8
- Mark
+1
at position 2 (start of illumination) - Mark
-1
at position 9 (8 + 1, end of illumination)
For lamp [7, 2]
:
- Left boundary:
7 - 2 = 5
- Right boundary:
7 + 2 = 9
- Mark
+1
at position 5 (start of illumination) - Mark
-1
at position 10 (9 + 1, end of illumination)
Our difference array d
becomes:
d = {2: 1, 9: -1, 5: 1, 10: -1}
Step 3: Process positions in sorted order
Sort the positions: [2, 5, 9, 10]
Now traverse and accumulate:
Position | Change | Running Sum | Max Brightness | Answer Position |
---|---|---|---|---|
Initial | - | 0 | 0 | 0 |
2 | +1 | 1 | 1 | 2 |
5 | +1 | 2 | 2 | 5 |
9 | -1 | 1 | 2 (no change) | 5 |
10 | -1 | 0 | 2 (no change) | 5 |
Result: The maximum brightness is 2, occurring at position 5.
Verification:
- Positions 2, 3, 4: brightness = 1 (only lamp 1)
- Positions 5, 6, 7, 8: brightness = 2 (both lamps)
- Position 9: brightness = 1 (only lamp 2)
- Other positions: brightness = 0
The answer is position 5, which is the smallest position with maximum brightness of 2.
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def brightestPosition(self, lights: List[List[int]]) -> int:
6 # Dictionary to track brightness changes at each position
7 brightness_changes = defaultdict(int)
8
9 # For each light, calculate its illumination range
10 for position, range_value in lights:
11 # Calculate left and right boundaries of illumination
12 left_boundary = position - range_value
13 right_boundary = position + range_value
14
15 # Mark the start of illumination (increment brightness)
16 brightness_changes[left_boundary] += 1
17 # Mark the end of illumination (decrement brightness after the range)
18 brightness_changes[right_boundary + 1] -= 1
19
20 # Initialize variables to track the brightest position
21 brightest_position = 0
22 current_brightness = 0
23 max_brightness = 0
24
25 # Process positions in sorted order to calculate cumulative brightness
26 for position in sorted(brightness_changes):
27 # Update current brightness by applying the change at this position
28 current_brightness += brightness_changes[position]
29
30 # Check if we found a new maximum brightness
31 if current_brightness > max_brightness:
32 max_brightness = current_brightness
33 brightest_position = position
34
35 return brightest_position
36
1class Solution {
2 public int brightestPosition(int[][] lights) {
3 // TreeMap to store position changes (sorted by position)
4 // Key: position, Value: change in brightness
5 TreeMap<Integer, Integer> positionChanges = new TreeMap<>();
6
7 // Process each light source
8 for (int[] light : lights) {
9 // Calculate the left and right boundaries of light coverage
10 // light[0] is position, light[1] is range
11 int leftBoundary = light[0] - light[1];
12 int rightBoundary = light[0] + light[1];
13
14 // Increment brightness at left boundary (light starts)
15 positionChanges.merge(leftBoundary, 1, Integer::sum);
16 // Decrement brightness after right boundary (light ends)
17 positionChanges.merge(rightBoundary + 1, -1, Integer::sum);
18 }
19
20 // Variables to track the result
21 int brightestPosition = 0; // Position with maximum brightness
22 int currentBrightness = 0; // Current accumulated brightness
23 int maxBrightness = 0; // Maximum brightness found so far
24
25 // Sweep through positions from left to right
26 for (Map.Entry<Integer, Integer> entry : positionChanges.entrySet()) {
27 int brightnessChange = entry.getValue();
28 currentBrightness += brightnessChange;
29
30 // Update the brightest position if current brightness exceeds maximum
31 if (currentBrightness > maxBrightness) {
32 maxBrightness = currentBrightness;
33 brightestPosition = entry.getKey();
34 }
35 }
36
37 return brightestPosition;
38 }
39}
40
1class Solution {
2public:
3 int brightestPosition(vector<vector<int>>& lights) {
4 // Use a map to track brightness changes at each position
5 // Key: position, Value: change in brightness at that position
6 map<int, int> brightnessChanges;
7
8 // Process each light source
9 for (auto& light : lights) {
10 int position = light[0];
11 int range = light[1];
12
13 // Calculate the leftmost and rightmost positions affected by this light
14 int leftBoundary = position - range;
15 int rightBoundary = position + range;
16
17 // Mark the start of illumination (brightness increases)
18 ++brightnessChanges[leftBoundary];
19
20 // Mark the end of illumination (brightness decreases after this point)
21 --brightnessChanges[rightBoundary + 1];
22 }
23
24 // Find the position with maximum brightness using sweep line algorithm
25 int brightestPos = 0;
26 int currentBrightness = 0;
27 int maxBrightness = 0;
28
29 // Traverse positions in sorted order and accumulate brightness
30 for (auto& [position, brightnessChange] : brightnessChanges) {
31 currentBrightness += brightnessChange;
32
33 // Update the brightest position if current brightness exceeds maximum
34 if (maxBrightness < currentBrightness) {
35 maxBrightness = currentBrightness;
36 brightestPos = position;
37 }
38 }
39
40 return brightestPos;
41 }
42};
43
1/**
2 * @param {number[][]} lights - Array of lights where each light is [position, range]
3 * @return {number} - The position with maximum brightness
4 */
5function brightestPosition(lights: number[][]): number {
6 // Map to store brightness changes at each position
7 // Key: position, Value: brightness change at that position
8 const brightnessChanges = new Map<number, number>();
9
10 // Process each light to mark where its brightness starts and ends
11 for (const [position, range] of lights) {
12 // Calculate left boundary (where light starts affecting)
13 const leftBoundary = position - range;
14 // Calculate right boundary (where light stops affecting)
15 const rightBoundary = position + range;
16
17 // Increment brightness at start position
18 brightnessChanges.set(leftBoundary, (brightnessChanges.get(leftBoundary) ?? 0) + 1);
19 // Decrement brightness after end position
20 brightnessChanges.set(rightBoundary + 1, (brightnessChanges.get(rightBoundary + 1) ?? 0) - 1);
21 }
22
23 // Extract all positions and sort them
24 const positions: number[] = [];
25 for (const position of brightnessChanges.keys()) {
26 positions.push(position);
27 }
28 positions.sort((a, b) => a - b);
29
30 // Find the position with maximum brightness using sweep line algorithm
31 let brightestPos = 0;
32 let currentBrightness = 0;
33 let maxBrightness = 0;
34
35 for (const position of positions) {
36 // Update current brightness based on the change at this position
37 currentBrightness += brightnessChanges.get(position)!;
38
39 // Update maximum brightness and its position if current is greater
40 if (maxBrightness < currentBrightness) {
41 maxBrightness = currentBrightness;
42 brightestPos = position;
43 }
44 }
45
46 return brightestPos;
47}
48
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm iterates through the lights
array once to create interval boundaries, which takes O(n)
time. Each light creates two boundary points (start and end), resulting in at most 2n
entries in the dictionary d
. The dominant operation is sorting these boundary points using sorted(d)
, which has a time complexity of O(2n × log(2n))
= O(n × log n)
. The final iteration through the sorted keys takes O(2n)
= O(n)
time. Therefore, the overall time complexity is O(n) + O(n × log n) + O(n)
= O(n × log n)
.
Space Complexity: O(n)
The algorithm uses a dictionary d
to store the boundary points. In the worst case, each light contributes two unique boundary points (left and right + 1), resulting in at most 2n
entries in the dictionary, which requires O(2n)
= O(n)
space. The sorted operation creates a list of keys which also takes O(n)
space. Additional variables (ans
, s
, mx
) use constant space O(1)
. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Coordinates
When calculating boundaries (position - range
or position + range
), the result might exceed typical integer bounds if positions and ranges are very large.
Example Problem:
- Light at position
10^9
with range10^9
would create a right boundary of2 * 10^9
Solution:
- In Python, integers have arbitrary precision, so this isn't an issue
- In languages like Java/C++, use
long
data type or check for overflow conditions
2. Forgetting the +1 Offset for Range End
A critical mistake is marking the range end at position + range
instead of position + range + 1
.
Incorrect Code:
d[r] -= 1 # Wrong! This would exclude the last position
Correct Code:
d[r + 1] -= 1 # Correct: decrement happens AFTER the range ends
Why it matters: The range is inclusive of both endpoints. If we decrement at position r
, we'd incorrectly reduce brightness at the last illuminated position.
3. Not Handling Empty Input
If the lights
array is empty, the code would return 0
as the brightest position, which might not be the intended behavior.
Solution:
def brightestPosition(self, lights: List[List[int]]) -> int:
if not lights:
return 0 # or raise an exception based on requirements
# rest of the code...
4. Misunderstanding the Tie-Breaking Rule
When multiple positions have the same maximum brightness, returning the wrong position is a common error.
Incorrect Approach:
if current_brightness >= max_brightness: # Wrong! This returns the LAST position with max brightness max_brightness = current_brightness brightest_position = position
Correct Approach:
if current_brightness > max_brightness: # Correct: Only update on strictly greater brightness max_brightness = current_brightness brightest_position = position
5. Processing Positions in Wrong Order
Using an unordered iteration over the dictionary keys would give incorrect results.
Incorrect:
for position in brightness_changes: # Wrong! Dictionary iteration order isn't guaranteed to be sorted current_brightness += brightness_changes[position]
Correct:
for position in sorted(brightness_changes): # Correct: Must process in ascending order
current_brightness += brightness_changes[position]
6. Confusion Between Position and Index
The problem deals with positions on a number line, not array indices. Positions can be negative or non-consecutive.
Example: Lights at positions [-5, 3]
and [100, 2]
are valid inputs. Don't assume positions start at 0 or are consecutive integers.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
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!