3951. Minimum Energy to Maintain Brightness
Problem Description
You are given an integer n, representing n light bulbs arranged in a line and indexed from 0 to n - 1.
You are also given an integer brightness and a 2D integer array intervals, where intervals[i] = [startᵢ, endᵢ] represents an inclusive time interval during which the lighting requirement must be satisfied.
At each time unit, every bulb can independently be either on or off. A bulb that is on illuminates its own position and its adjacent positions, if they exist. This means a single bulb can light up at most 3 positions (its own and the two neighbors on either side).
The total illumination at a time unit is the number of illuminated positions, where each position is counted at most once (even if covered by multiple bulbs).
For every integer time unit covered by at least one interval in intervals, the total illumination must be at least brightness. At time units not covered by any interval, all bulbs may remain off. Each bulb that is on consumes 1 unit of energy for that time unit.
Your task is to return an integer denoting the minimum total energy required across all time units.
In other words, you must figure out the smallest number of bulb-activations needed so that during every required time unit (those falling inside the given intervals), the number of lit positions reaches at least brightness. Since a single lit bulb can cover up to 3 positions, you need at least ⌈brightness / 3⌉ bulbs on at any required time unit. Because the intervals may overlap, you should first determine the set of distinct time units that must be satisfied, then multiply the per-time-unit bulb cost by the number of such time units.
How We Pick the Algorithm
Why Heap / Sortings?
This problem maps to Heap / Sortings through a short path in the full flowchart.
Sorting the data identifies the required elements in the correct order.
Open in FlowchartIntuition
The key observation starts with understanding what a single bulb can do. When a bulb is on, it lights up its own position plus the positions immediately to its left and right. So one bulb can cover at most 3 positions. This is the most "efficient" any single bulb can be.
Now, at any required time unit, we need the total illumination to reach at least brightness. Since each bulb contributes at most 3 lit positions, the minimum number of bulbs we need to turn on is ⌈brightness / 3⌉. We can achieve this efficiency by spacing the lit bulbs so their lit regions don't overlap (for example, turning on bulbs at positions 1, 4, 7, ...), which guarantees every active bulb adds a fresh 3 positions. In integer math, ⌈brightness / 3⌉ is conveniently written as (brightness + 2) // 3.
Next, notice that each time unit is independent of the others. The bulbs we choose at one time unit have no effect on another time unit, so the cost to satisfy a single required time unit is always the same: (brightness + 2) // 3 units of energy.
This means the total energy is simply:
(cost per time unit) × (number of distinct required time units)
The only remaining piece is counting how many distinct integer time units actually need lighting. The intervals are inclusive ranges, and they may overlap or touch each other. If we naively add up the length of every interval, we would double-count time units that appear in more than one interval. To avoid this, we first merge overlapping intervals into a set of disjoint ranges. After merging, each range [start, end] contributes exactly end - start + 1 distinct time units with no overlap.
Putting it together: sort and merge the intervals, then for each merged range add (brightness + 2) // 3 * (end - start + 1) to the answer. The sum gives the minimum total energy.
Pattern Learn more about Sorting patterns.
Solution Approach
Solution 1: Interval Merge
A single bulb can illuminate at most 3 positions. To ensure the total brightness is at least brightness, the number of bulbs required to be turned on at each time unit is ⌈brightness / 3⌉. In programming, this is commonly written in integer division form as (brightness + 2) // 3.
The implementation can be broken down into the following steps:
-
Sort the intervals: We start by sorting
intervalsby their start values usingintervals.sort(). Sorting ensures that as we scan from left to right, any intervals that can be merged appear consecutively, which makes the merging step straightforward. -
Merge overlapping intervals: We maintain a list called
merged, initialized with the first interval. For each subsequent intervalx:- If the end of the last merged interval is strictly less than the start of
x(merged[-1][1] < x[0]), the two ranges do not overlap, so we appendxas a new disjoint interval. - Otherwise, they overlap or touch, so we extend the current interval's end to cover both by setting
merged[-1][1] = max(merged[-1][1], x[1]).
After this pass,
mergedholds a set of mutually disjoint continuous intervals, so no time unit is counted twice. - If the end of the last merged interval is strictly less than the start of
-
Calculate length contribution: For each merged interval
[start, end], the number of integer points (i.e., distinct time units) it covers ism = end - start + 1. Since every time unit within the interval must independently satisfy the minimum brightness, the energy required for this interval is: which in code is(brightness + 2) // 3 * m. -
Accumulate and sum: We add up the energy of all disjoint intervals into
ansand return it as the final answer.
Complexity Analysis
- Time complexity:
O(L × log L), whereLis the number of intervals. The dominant cost is sorting the intervals; the merge and accumulation passes are both linearO(L). - Space complexity:
O(L)for storing themergedlist of intervals (orO(log L)toO(L)depending on the sorting implementation's auxiliary space).
Example Walkthrough
Let's trace through a small example to see how the solution approach works.
Input:
n = 6(six bulbs indexed0through5)brightness = 7intervals = [[1, 3], [2, 5], [8, 9]]
Step 0: Compute the per-time-unit bulb cost
A single bulb covers at most 3 positions, so to reach a brightness of 7 we need:
So each required time unit costs 3 units of energy. We could, for example, turn on bulbs at positions 1, 4, and a third spaced bulb so their lit regions (0-2, 3-5, ...) don't overlap, lighting up 9 ≥ 7 distinct positions.
Step 1: Sort the intervals
The intervals sorted by start value:
[[1, 3], [2, 5], [8, 9]]
(Already sorted in this case.)
Step 2: Merge overlapping intervals
Initialize merged = [[1, 3]].
- Process
[2, 5]: Comparemerged[-1][1] = 3withx[0] = 2. Since3 < 2is false, the intervals overlap. Extend the end:merged[-1][1] = max(3, 5) = 5. Nowmerged = [[1, 5]]. - Process
[8, 9]: Comparemerged[-1][1] = 5withx[0] = 8. Since5 < 8is true, they are disjoint. Append it. Nowmerged = [[1, 5], [8, 9]].
The merging absorbed the overlap between [1, 3] and [2, 5], so time units 2 and 3 are counted only once.
Step 3: Calculate length contribution for each merged interval
- Interval
[1, 5]: coversm = 5 - 1 + 1 = 5time units (units1, 2, 3, 4, 5). Energy =3 × 5 = 15. - Interval
[8, 9]: coversm = 9 - 8 + 1 = 2time units (units8, 9). Energy =3 × 2 = 6.
Step 4: Accumulate and sum
Result: The minimum total energy required is 21.
Note on the importance of merging: If we had naively summed the raw interval lengths —
(3-1+1) + (5-2+1) + (9-8+1) = 3 + 4 + 2 = 9time units — we'd compute3 × 9 = 27, over-counting time units2and3. Merging correctly yields5 + 2 = 7distinct time units, giving the right answer of21.
Solution Implementation
1class Solution:
2 def minEnergy(self, n: int, brightness: int, intervals: list[list[int]]) -> int:
3 # Sort intervals by their start point (and end point on ties)
4 intervals.sort()
5
6 # Merge overlapping or contiguous intervals
7 merged_intervals: list[list[int]] = [intervals[0]]
8 for current in intervals[1:]:
9 last = merged_intervals[-1]
10 if last[1] < current[0]:
11 # No overlap: start a new interval
12 merged_intervals.append(current)
13 else:
14 # Overlap: extend the end of the last merged interval
15 last[1] = max(last[1], current[1])
16
17 # Accumulate the total energy needed
18 total_energy = 0
19 for start, end in merged_intervals:
20 # Number of integer points covered by this interval (inclusive)
21 length = end - start + 1
22 # Energy per point is ceil(brightness / 3), computed as (brightness + 2) // 3
23 energy_per_point = (brightness + 2) // 3
24 total_energy += energy_per_point * length
25
26 return total_energy
271class Solution {
2 public long minEnergy(int n, int brightness, int[][] intervals) {
3 // Sort intervals by their start position in ascending order
4 Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
5
6 // List to hold the merged, non-overlapping intervals
7 List<int[]> mergedIntervals = new ArrayList<>();
8 // Seed the list with the first interval
9 mergedIntervals.add(intervals[0]);
10
11 // Iterate over the remaining intervals and merge overlapping ones
12 for (int i = 1; i < intervals.length; i++) {
13 int[] current = intervals[i];
14 // Reference to the last interval already placed in the merged list
15 int[] lastInterval = mergedIntervals.get(mergedIntervals.size() - 1);
16
17 if (lastInterval[1] < current[0]) {
18 // No overlap: the current interval starts after the last one ends,
19 // so add it as a new separate interval
20 mergedIntervals.add(current);
21 } else {
22 // Overlap exists: extend the last interval's end to cover the current one
23 lastInterval[1] = Math.max(lastInterval[1], current[1]);
24 }
25 }
26
27 // Accumulate the total energy required across all merged intervals
28 long totalEnergy = 0;
29 for (int[] interval : mergedIntervals) {
30 int start = interval[0];
31 int end = interval[1];
32 // Number of integer positions covered by this interval (inclusive)
33 int length = end - start + 1;
34 // Energy per position is ceil(brightness / 3), computed as (brightness + 2) / 3.
35 // Multiply by the interval length to get the energy for this interval.
36 totalEnergy += (brightness + 2L) / 3 * length;
37 }
38
39 return totalEnergy;
40 }
41}
421class Solution {
2public:
3 long long minEnergy(int n, int brightness, vector<vector<int>>& intervals) {
4 // Sort intervals by their start point (and end point as tiebreaker)
5 sort(intervals.begin(), intervals.end());
6
7 // Initialize the merged list with the first interval
8 vector<vector<int>> mergedIntervals = {intervals[0]};
9
10 // Merge overlapping or adjacent intervals
11 for (int i = 1; i < static_cast<int>(intervals.size()); ++i) {
12 const auto& current = intervals[i];
13 // If the current interval does not overlap with the last merged one,
14 // append it as a new separate interval
15 if (mergedIntervals.back()[1] < current[0]) {
16 mergedIntervals.push_back(current);
17 } else {
18 // Otherwise, extend the end of the last merged interval
19 mergedIntervals.back()[1] = max(mergedIntervals.back()[1], current[1]);
20 }
21 }
22
23 // Accumulate the total energy across all merged intervals
24 long long totalEnergy = 0;
25 for (const auto& interval : mergedIntervals) {
26 int start = interval[0];
27 int end = interval[1];
28
29 // Number of integer points covered by this interval (inclusive)
30 int length = end - start + 1;
31
32 // Energy per point is ceil(brightness / 3), computed as (brightness + 2) / 3.
33 // Use 2LL to force a 64-bit computation and avoid overflow.
34 totalEnergy += (brightness + 2LL) / 3 * length;
35 }
36
37 return totalEnergy;
38 }
39};
401/**
2 * Calculates the minimum energy required given a set of intervals.
3 *
4 * The algorithm works in two phases:
5 * 1. Merge all overlapping intervals so that each region is counted only once.
6 * 2. For every merged interval, compute its length and accumulate the energy
7 * needed, where each unit of length costs ceil(brightness / 3) energy.
8 *
9 * @param n The total number of positions (kept for signature compatibility).
10 * @param brightness The brightness value used to derive the per-unit energy cost.
11 * @param intervals An array of [start, end] interval pairs.
12 * @returns The minimum total energy required.
13 */
14function minEnergy(n: number, brightness: number, intervals: number[][]): number {
15 // Sort intervals by their starting position in ascending order
16 // so that overlapping intervals are adjacent and can be merged.
17 intervals.sort((a, b) => a[0] - b[0]);
18
19 // Initialize the merged list with the first interval.
20 const mergedIntervals: number[][] = [intervals[0]];
21
22 // Iterate through the remaining intervals and merge them as needed.
23 for (let i = 1; i < intervals.length; i++) {
24 const current = intervals[i];
25 const last = mergedIntervals[mergedIntervals.length - 1];
26
27 if (last[1] < current[0]) {
28 // No overlap with the last merged interval: add as a new interval.
29 mergedIntervals.push(current);
30 } else {
31 // Overlap detected: extend the end of the last merged interval.
32 last[1] = Math.max(last[1], current[1]);
33 }
34 }
35
36 // Per-unit energy cost derived from the brightness value.
37 const unitCost = Math.ceil(brightness / 3);
38
39 // Accumulate the total energy across all merged intervals.
40 let totalEnergy = 0;
41 for (const [start, end] of mergedIntervals) {
42 // Inclusive length of the interval.
43 const length = end - start + 1;
44 totalEnergy += unitCost * length;
45 }
46
47 return totalEnergy;
48}
49Time and Space Complexity
-
Time Complexity:
O(n log n), wherenis the number of intervals.- The dominant operation is
intervals.sort(), which takesO(n log n)time. - The first loop merges overlapping intervals by iterating through all intervals once, taking
O(n)time. - The second loop iterates over the
mergedlist, whose size is at mostn, also takingO(n)time. - Combining these, the overall time complexity is
O(n log n) + O(n) + O(n) = O(n log n).
- The dominant operation is
-
Space Complexity:
O(n), wherenis the number of intervals.- The
mergedlist stores the merged intervals, which in the worst case (no overlaps) contains allnintervals, requiringO(n)space. - The sorting operation may also use up to
O(n)auxiliary space depending on the implementation (Python's Timsort usesO(n)in the worst case). - Therefore, the overall space complexity is
O(n).
- The
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Treating Adjacent (Touching) Intervals as Disjoint
The most frequent mistake lies in the merge condition. Because the intervals represent inclusive time ranges over discrete integer points, two intervals like [1, 3] and [4, 6] are actually contiguous — they together cover the unbroken stretch of time units 1, 2, 3, 4, 5, 6. If you only merge when intervals strictly overlap (e.g., using last[1] < current[0] becomes last[1] <= current[0] incorrectly, or you forget that touching ranges share a continuous timeline), you may or may not double-count, depending on how the length is computed.
The subtle point is that merging touching intervals does not change the total count of distinct time units in this problem, because each time unit is still counted once whether the ranges are merged or kept separate. So the real danger is the opposite: accidentally double-counting when intervals truly overlap.
Incorrect (double-counting overlaps):
# WRONG: sums every interval's length without merging total_energy = 0 for start, end in intervals: total_energy += (brightness + 2) // 3 * (end - start + 1)
For intervals = [[1, 5], [3, 8]], this counts time units 3, 4, 5 twice, inflating the answer.
Correct approach — always merge first so each time unit contributes exactly once:
intervals.sort()
merged = [intervals[0]]
for s, e in intervals[1:]:
if merged[-1][1] < s: # strictly disjoint -> new interval
merged.append([s, e])
else: # overlap -> extend
merged[-1][1] = max(merged[-1][1], e)
Pitfall 2: Mutating the Input Intervals In-Place
In the merge step, merged_intervals = [intervals[0]] stores a reference to the original sub-list, and later last[1] = max(...) mutates the caller's input data. This silent side effect can corrupt the input for any code that reuses intervals afterward, and makes the function non-idempotent.
Safer fix — copy the interval when seeding/appending:
merged_intervals = [intervals[0][:]] # copy, not reference ... merged_intervals.append(current[:]) # copy on append too
Pitfall 3: Wrong Ceiling Division for Bulbs Per Time Unit
Since one bulb illuminates up to 3 positions, you need ⌈brightness / 3⌉ bulbs per required time unit. A common error is writing brightness // 3, which floors the value and under-provisions whenever brightness is not a multiple of 3.
Incorrect:
energy_per_point = brightness // 3 # e.g., brightness=7 -> 2 (need 3!)
Correct:
energy_per_point = (brightness + 2) // 3 # ceiling division; brightness=7 -> 3
Equivalently -(-brightness // 3) or math.ceil(brightness / 3) (avoid the float version for very large values to prevent precision issues).
Pitfall 4: Not Handling an Empty intervals List
The line merged_intervals = [intervals[0]] raises an IndexError when intervals is empty. If no time unit requires illumination, the answer should simply be 0.
Guard clause:
if not intervals: return 0
Pitfall 5: Ignoring the Physical Limit Imposed by n
The problem states a bulb covers at most 3 positions, so ⌈brightness / 3⌉ bulbs suffice only if there are enough positions to spread the illumination. If brightness > n, it is physically impossible to illuminate brightness distinct positions (there are only n of them). Depending on the problem's exact constraints, you may need to detect infeasibility:
if brightness > n: return -1 # or handle per the problem's required convention
While the given solution assumes feasibility (a valid configuration always exists), overlooking this edge case can produce a numerically "correct" but physically meaningless answer when inputs violate the implicit guarantee.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapIn a binary min heap, the minimum element can be found in:
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!