683. K Empty Slots 🔒
Problem Description
You have n
bulbs arranged in a row, numbered from 1
to n
. Initially, all bulbs are turned off. Each day, you turn on exactly one bulb, and after n
days, all bulbs will be turned on.
You're given an array bulbs
of length n
where bulbs[i] = x
means that on day (i+1)
, you turn on the bulb at position x
. Note that i
is 0-indexed (starts from 0) while bulb positions x
are 1-indexed (start from 1).
Given an integer k
, you need to find the minimum day number where there exist two turned-on bulbs that have exactly k
bulbs between them, and all those k
bulbs in between are turned off.
For example, if k = 1
and on some day you have bulbs at positions 3 and 5 turned on with bulb 4 turned off, this would satisfy the condition (exactly 1 bulb between them that is off).
If no such day exists where this condition is met, return -1
.
The solution uses a Binary Indexed Tree (Fenwick Tree) to efficiently track which bulbs are turned on and check if there are exactly k
turned-off bulbs between any two turned-on bulbs. On each day when a bulb is turned on, it checks:
- If there's a turned-on bulb exactly
k+1
positions to the left with all bulbs in between turned off - If there's a turned-on bulb exactly
k+1
positions to the right with all bulbs in between turned off
The Binary Indexed Tree helps query the count of turned-on bulbs in any range in O(log n)
time, making it efficient to verify if all k
bulbs between two positions are turned off.
Intuition
The key insight is that we need to efficiently check, for each newly turned-on bulb, whether it forms a valid pair with any previously turned-on bulb with exactly k
turned-off bulbs between them.
When we turn on a bulb at position x
, we only need to check two specific positions:
- Position
x - k - 1
(a bulb that would bek+1
positions to the left) - Position
x + k + 1
(a bulb that would bek+1
positions to the right)
Why only these two positions? Because if there's a valid pair involving the current bulb, the other bulb must be exactly k+1
positions away. Any other distance wouldn't give us exactly k
bulbs in between.
The challenge is verifying that all k
bulbs between two positions are turned off. A naive approach would check each bulb individually, taking O(k)
time per check. Since we might do this check for each of n
days, this could be inefficient.
This is where the Binary Indexed Tree comes in. By maintaining a prefix sum of turned-on bulbs, we can query how many bulbs are turned on in any range in O(log n)
time. For positions y
and x
with y < x
, if exactly 0 bulbs are turned on between them, then query(x-1) - query(y)
should equal 0. This means the cumulative count of turned-on bulbs up to position x-1
minus the count up to position y
equals zero, confirming all bulbs between y
and x
are off.
The vis
array tracks which bulbs are already on, so we can quickly check if the potential partner bulb at position y
is turned on before checking the range between them.
By processing bulbs in the order they're turned on and checking these conditions immediately, we can find the earliest day when the condition is satisfied.
Learn more about Segment Tree, Queue, Sliding Window, Monotonic Queue and Heap (Priority Queue) patterns.
Solution Approach
The solution uses a Binary Indexed Tree (Fenwick Tree) to efficiently track and query the state of bulbs. Let's walk through the implementation step by step:
1. Binary Indexed Tree Setup
The BinaryIndexedTree
class maintains a prefix sum structure:
__init__(n)
: Creates an arrayc
of sizen+1
initialized with zerosupdate(x, delta)
: Addsdelta
to positionx
and updates all affected nodes using the formulax += x & -x
to traverse up the treequery(x)
: Returns the sum from position 1 tox
by traversing down usingx -= x & -x
2. Main Algorithm
Initialize:
- Create a Binary Indexed Tree of size
n
- Create a boolean array
vis
of sizen+1
to track which bulbs are turned on - Process bulbs day by day using
enumerate(bulbs, 1)
to get both day number and bulb position
3. For Each Day
When turning on bulb at position x
on day i
:
a. Update the state:
- Call
tree.update(x, 1)
to mark this bulb as turned on in the BIT - Set
vis[x] = True
to mark this bulb as visible
b. Check left neighbor:
- Calculate potential left partner:
y = x - k - 1
- If
y > 0
(valid position) andvis[y]
(bulb at y is on):- Check if all bulbs between
y
andx
are off using:tree.query(x - 1) - tree.query(y) == 0
- If true, return current day
i
- Check if all bulbs between
c. Check right neighbor:
- Calculate potential right partner:
y = x + k + 1
- If
y <= n
(valid position) andvis[y]
(bulb at y is on):- Check if all bulbs between
x
andy
are off using:tree.query(y - 1) - tree.query(x) == 0
- If true, return current day
i
- Check if all bulbs between
4. Range Query Logic
The expression tree.query(b) - tree.query(a)
gives the count of turned-on bulbs in range (a, b]
. When this equals 0, all bulbs in that range are off.
5. Return Value
If no valid configuration is found after processing all days, return -1
.
Time Complexity: O(n log n)
- We process n
bulbs, and each update/query operation takes O(log n)
time.
Space Complexity: O(n)
- For the BIT array and visibility array.
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 bulbs = [2, 1, 3, 5, 4]
and k = 1
.
We need to find the minimum day where two turned-on bulbs have exactly 1 bulb between them that is turned off.
Initial Setup:
- Binary Indexed Tree: tracks turned-on bulbs (initially all 0)
- Visibility array
vis
: [False, False, False, False, False, False] (index 0 unused) - Bulb positions: [1, 2, 3, 4, 5]
Day 1: Turn on bulb at position 2
- Update BIT at position 2, set
vis[2] = True
- Check left neighbor: position
2 - 1 - 1 = 0
(invalid, skip) - Check right neighbor: position
2 + 1 + 1 = 4
vis[4] = False
(bulb 4 not on yet, skip)
- Current state: [OFF, ON, OFF, OFF, OFF]
Day 2: Turn on bulb at position 1
- Update BIT at position 1, set
vis[1] = True
- Check left neighbor: position
1 - 1 - 1 = -1
(invalid, skip) - Check right neighbor: position
1 + 1 + 1 = 3
vis[3] = False
(bulb 3 not on yet, skip)
- Current state: [ON, ON, OFF, OFF, OFF]
Day 3: Turn on bulb at position 3
- Update BIT at position 3, set
vis[3] = True
- Check left neighbor: position
3 - 1 - 1 = 1
vis[1] = True
(bulb 1 is on!)- Check range (1, 3):
tree.query(2) - tree.query(1) = 1 - 1 = 0
- Wait, this gives us count in range (1, 2], which is just position 2
- Position 2 has a turned-on bulb, so count = 1 ≠0 (not valid)
- Check right neighbor: position
3 + 1 + 1 = 5
vis[5] = False
(bulb 5 not on yet, skip)
- Current state: [ON, ON, ON, OFF, OFF]
Day 4: Turn on bulb at position 5
- Update BIT at position 5, set
vis[5] = True
- Check left neighbor: position
5 - 1 - 1 = 3
vis[3] = True
(bulb 3 is on!)- Check range (3, 5):
tree.query(4) - tree.query(3) = 3 - 3 = 0
- All bulbs between 3 and 5 (just position 4) are off! ✓
- Found valid configuration: bulbs 3 and 5 are on with bulb 4 off between them
- Return day 4
The answer is 4 - this is the first day where we have two turned-on bulbs with exactly k=1 turned-off bulb between them.
Solution Implementation
1class BinaryIndexedTree:
2 """
3 Binary Indexed Tree (Fenwick Tree) for efficient range sum queries
4 and point updates in O(log n) time.
5 """
6
7 def __init__(self, n: int) -> None:
8 """
9 Initialize a Binary Indexed Tree with n elements.
10
11 Args:
12 n: The number of elements (1-indexed)
13 """
14 self.size = n
15 self.tree = [0] * (n + 1) # 1-indexed array for BIT
16
17 def update(self, index: int, delta: int) -> None:
18 """
19 Add delta to the element at the given index.
20
21 Args:
22 index: The position to update (1-indexed)
23 delta: The value to add to the element
24 """
25 while index <= self.size:
26 self.tree[index] += delta
27 # Move to next index by adding the least significant bit
28 index += index & (-index)
29
30 def query(self, index: int) -> int:
31 """
32 Calculate the prefix sum from index 1 to the given index.
33
34 Args:
35 index: The end position for the prefix sum (1-indexed)
36
37 Returns:
38 The sum of elements from position 1 to index
39 """
40 prefix_sum = 0
41 while index > 0:
42 prefix_sum += self.tree[index]
43 # Move to parent index by removing the least significant bit
44 index -= index & (-index)
45 return prefix_sum
46
47
48class Solution:
49 def kEmptySlots(self, bulbs: List[int], k: int) -> int:
50 """
51 Find the earliest day when there are exactly k empty slots between
52 two turned-on bulbs.
53
54 Args:
55 bulbs: Array where bulbs[i] is the position of bulb turned on at day i+1
56 k: The required number of empty slots between two turned-on bulbs
57
58 Returns:
59 The earliest day (1-indexed) when the condition is met, or -1 if never
60 """
61 n = len(bulbs)
62 fenwick_tree = BinaryIndexedTree(n)
63 is_turned_on = [False] * (n + 1) # Track which bulbs are turned on
64
65 # Process each day (1-indexed)
66 for day, bulb_position in enumerate(bulbs, start=1):
67 # Turn on the bulb at the current position
68 fenwick_tree.update(bulb_position, 1)
69 is_turned_on[bulb_position] = True
70
71 # Check left neighbor: position that is k+1 slots to the left
72 left_neighbor = bulb_position - k - 1
73 if left_neighbor > 0 and is_turned_on[left_neighbor]:
74 # Count turned-on bulbs between left_neighbor and current position
75 bulbs_between = fenwick_tree.query(bulb_position - 1) - fenwick_tree.query(left_neighbor)
76 if bulbs_between == 0: # Exactly k empty slots between them
77 return day
78
79 # Check right neighbor: position that is k+1 slots to the right
80 right_neighbor = bulb_position + k + 1
81 if right_neighbor <= n and is_turned_on[right_neighbor]:
82 # Count turned-on bulbs between current position and right_neighbor
83 bulbs_between = fenwick_tree.query(right_neighbor - 1) - fenwick_tree.query(bulb_position)
84 if bulbs_between == 0: # Exactly k empty slots between them
85 return day
86
87 # No valid day found
88 return -1
89
1class Solution {
2 /**
3 * Find the earliest day when there are exactly k empty slots between two blooming flowers.
4 *
5 * @param bulbs Array where bulbs[i] represents the position of the bulb that blooms on day i+1
6 * @param k The required number of empty slots between two blooming flowers
7 * @return The earliest day (1-indexed) when the condition is met, or -1 if never
8 */
9 public int kEmptySlots(int[] bulbs, int k) {
10 int n = bulbs.length;
11
12 // Binary Indexed Tree to efficiently query range sums (count of blooming bulbs in a range)
13 BinaryIndexedTree fenwickTree = new BinaryIndexedTree(n);
14
15 // Track which positions have blooming bulbs
16 boolean[] isBlooming = new boolean[n + 1];
17
18 // Process each day
19 for (int day = 1; day <= n; ++day) {
20 // Get the position of the bulb that blooms on this day
21 int currentPosition = bulbs[day - 1];
22
23 // Mark this position as blooming and update the tree
24 fenwickTree.update(currentPosition, 1);
25 isBlooming[currentPosition] = true;
26
27 // Check if there's a blooming bulb exactly k+1 positions to the left
28 int leftPosition = currentPosition - k - 1;
29 if (leftPosition > 0 && isBlooming[leftPosition]) {
30 // Count blooming bulbs between leftPosition and currentPosition (exclusive)
31 int bloomingBetween = fenwickTree.query(currentPosition - 1) - fenwickTree.query(leftPosition);
32
33 // If no bulbs are blooming between them, we found our answer
34 if (bloomingBetween == 0) {
35 return day;
36 }
37 }
38
39 // Check if there's a blooming bulb exactly k+1 positions to the right
40 int rightPosition = currentPosition + k + 1;
41 if (rightPosition <= n && isBlooming[rightPosition]) {
42 // Count blooming bulbs between currentPosition and rightPosition (exclusive)
43 int bloomingBetween = fenwickTree.query(rightPosition - 1) - fenwickTree.query(currentPosition);
44
45 // If no bulbs are blooming between them, we found our answer
46 if (bloomingBetween == 0) {
47 return day;
48 }
49 }
50 }
51
52 // No valid day found
53 return -1;
54 }
55}
56
57/**
58 * Binary Indexed Tree (Fenwick Tree) for efficient range sum queries and point updates
59 */
60class BinaryIndexedTree {
61 private int size;
62 private int[] tree;
63
64 /**
65 * Initialize a Binary Indexed Tree with given size
66 *
67 * @param n The size of the tree
68 */
69 public BinaryIndexedTree(int n) {
70 this.size = n;
71 this.tree = new int[n + 1]; // 1-indexed array
72 }
73
74 /**
75 * Update the value at position x by adding delta
76 *
77 * @param x The position to update (1-indexed)
78 * @param delta The value to add
79 */
80 public void update(int x, int delta) {
81 // Traverse all affected nodes in the tree
82 for (; x <= size; x += x & -x) {
83 tree[x] += delta;
84 }
85 }
86
87 /**
88 * Query the prefix sum from position 1 to x
89 *
90 * @param x The end position of the range (1-indexed)
91 * @return The sum of elements from position 1 to x
92 */
93 public int query(int x) {
94 int sum = 0;
95 // Traverse the tree to calculate prefix sum
96 for (; x > 0; x -= x & -x) {
97 sum += tree[x];
98 }
99 return sum;
100 }
101}
102
1class BinaryIndexedTree {
2public:
3 int size; // Size of the tree (number of elements)
4 vector<int> tree; // Binary indexed tree array (1-indexed)
5
6 // Constructor: Initialize BIT with given size
7 BinaryIndexedTree(int n) : size(n), tree(n + 1) {}
8
9 // Update operation: Add delta to position x
10 // Time complexity: O(log n)
11 void update(int x, int delta) {
12 // Traverse all affected nodes in the tree
13 // x & -x gives the lowest set bit (LSB)
14 for (; x <= size; x += x & -x) {
15 tree[x] += delta;
16 }
17 }
18
19 // Query operation: Get prefix sum from 1 to x
20 // Time complexity: O(log n)
21 int query(int x) {
22 int sum = 0;
23 // Traverse parents by removing LSB each time
24 for (; x > 0; x -= x & -x) {
25 sum += tree[x];
26 }
27 return sum;
28 }
29};
30
31class Solution {
32public:
33 int kEmptySlots(vector<int>& bulbs, int k) {
34 int n = bulbs.size();
35
36 // Create Binary Indexed Tree to track turned-on bulbs
37 BinaryIndexedTree* bit = new BinaryIndexedTree(n);
38
39 // Track which bulbs are turned on (1-indexed)
40 vector<bool> isTurnedOn(n + 1, false);
41
42 // Process each day
43 for (int day = 1; day <= n; ++day) {
44 // Get the bulb position to turn on (1-indexed)
45 int currentBulb = bulbs[day - 1];
46
47 // Mark this bulb as turned on in BIT
48 bit->update(currentBulb, 1);
49 isTurnedOn[currentBulb] = true;
50
51 // Check left candidate: bulb at position (currentBulb - k - 1)
52 int leftCandidate = currentBulb - k - 1;
53 if (leftCandidate > 0 && isTurnedOn[leftCandidate]) {
54 // Count bulbs between leftCandidate and currentBulb (exclusive)
55 int bulbsBetween = bit->query(currentBulb - 1) - bit->query(leftCandidate);
56 if (bulbsBetween == 0) {
57 return day; // Found k empty slots
58 }
59 }
60
61 // Check right candidate: bulb at position (currentBulb + k + 1)
62 int rightCandidate = currentBulb + k + 1;
63 if (rightCandidate <= n && isTurnedOn[rightCandidate]) {
64 // Count bulbs between currentBulb and rightCandidate (exclusive)
65 int bulbsBetween = bit->query(rightCandidate - 1) - bit->query(currentBulb);
66 if (bulbsBetween == 0) {
67 return day; // Found k empty slots
68 }
69 }
70 }
71
72 // No valid configuration found
73 return -1;
74 }
75};
76
1// Binary Indexed Tree (Fenwick Tree) for range sum queries
2// Array to store the tree values (1-indexed)
3let treeSize: number;
4let treeArray: number[];
5
6// Initialize the Binary Indexed Tree with given size
7function initializeBIT(n: number): void {
8 treeSize = n;
9 treeArray = Array(n + 1).fill(0);
10}
11
12// Update the tree by adding delta to position x
13// Uses the BIT update formula: move to next index by adding lowbit
14function update(x: number, delta: number): void {
15 // Traverse all affected nodes in the tree
16 for (; x <= treeSize; x += x & -x) {
17 treeArray[x] += delta;
18 }
19}
20
21// Query the prefix sum from index 1 to x
22// Uses the BIT query formula: move to parent by subtracting lowbit
23function query(x: number): number {
24 let sum = 0;
25 // Traverse ancestors to calculate prefix sum
26 for (; x > 0; x -= x & -x) {
27 sum += treeArray[x];
28 }
29 return sum;
30}
31
32// Find the first day when there are exactly k empty slots between two bloomed flowers
33function kEmptySlots(bulbs: number[], k: number): number {
34 const totalBulbs = bulbs.length;
35
36 // Initialize the Binary Indexed Tree
37 initializeBIT(totalBulbs);
38
39 // Track which positions have bloomed flowers
40 const hasBloomed: boolean[] = Array(totalBulbs + 1).fill(false);
41
42 // Process each day
43 for (let day = 1; day <= totalBulbs; ++day) {
44 // Get the position of the bulb that blooms on this day
45 const currentPosition = bulbs[day - 1];
46
47 // Mark this position as bloomed in the BIT
48 update(currentPosition, 1);
49 hasBloomed[currentPosition] = true;
50
51 // Check if there's a bloomed flower k+1 positions to the left
52 let leftPosition = currentPosition - k - 1;
53 if (leftPosition > 0 && hasBloomed[leftPosition]) {
54 // Count bloomed flowers between leftPosition and currentPosition (exclusive)
55 const bloomedBetween = query(currentPosition - 1) - query(leftPosition);
56 if (bloomedBetween === 0) {
57 return day;
58 }
59 }
60
61 // Check if there's a bloomed flower k+1 positions to the right
62 let rightPosition = currentPosition + k + 1;
63 if (rightPosition <= totalBulbs && hasBloomed[rightPosition]) {
64 // Count bloomed flowers between currentPosition and rightPosition (exclusive)
65 const bloomedBetween = query(rightPosition - 1) - query(currentPosition);
66 if (bloomedBetween === 0) {
67 return day;
68 }
69 }
70 }
71
72 // No valid day found
73 return -1;
74}
75
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm iterates through all n
bulbs exactly once in the main loop. For each bulb at position i
:
- The
update
operation on the Binary Indexed Tree takesO(log n)
time, as it traverses at mostlog n
nodes by incrementingx
by its least significant bit (x += x & -x
) - The
query
operation is called at most 4 times (twice for each potential valid position check), and each query takesO(log n)
time as it traverses the tree by removing the least significant bit (x -= x & -x
) - The visibility checks and arithmetic operations take
O(1)
time
Therefore, the total time complexity is O(n)
iterations × O(log n)
operations per iteration = O(n × log n)
.
Space Complexity: O(n)
The algorithm uses:
- Binary Indexed Tree array
c
of sizen + 1
:O(n)
space - Boolean array
vis
of sizen + 1
to track which bulbs are turned on:O(n)
space - A few constant variables (
i
,x
,y
,s
):O(1)
space
The total space complexity is O(n) + O(n) + O(1) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Index Systems (0-indexed vs 1-indexed)
One of the most common mistakes in this problem is mixing up the different indexing conventions:
- The
bulbs
array is 0-indexed (positions 0 to n-1) - The bulb positions within the array are 1-indexed (bulbs numbered 1 to n)
- The Binary Indexed Tree uses 1-indexed positions
- Days are counted starting from 1
Pitfall Example:
# WRONG: Using 0-indexed for BIT or vis array
vis = [False] * n # Should be n+1 to accommodate 1-indexed positions
for i, x in enumerate(bulbs): # Missing the day offset
tree.update(x, 1)
# Checking vis[x] would cause index out of bounds
Solution:
# CORRECT: Properly handle indexing
vis = [False] * (n + 1) # Extra space for 1-indexed positions
for day, bulb_position in enumerate(bulbs, start=1): # Start days from 1
tree.update(bulb_position, 1)
vis[bulb_position] = True
2. Incorrect Range Query for Counting Bulbs Between Two Positions
When checking if all k bulbs between two positions are off, it's crucial to query the correct range boundaries.
Pitfall Example:
# WRONG: Including the endpoints in the range check left_neighbor = x - k - 1 if vis[left_neighbor]: # This includes both endpoints, counting k+2 positions total bulbs_between = tree.query(x) - tree.query(left_neighbor - 1)
Solution:
# CORRECT: Exclude both endpoints from the range left_neighbor = x - k - 1 if vis[left_neighbor]: # Query from (left_neighbor, x) exclusive on both ends bulbs_between = tree.query(x - 1) - tree.query(left_neighbor)
3. Forgetting to Check Boundary Conditions
Not validating whether neighbor positions are within valid bounds before accessing arrays.
Pitfall Example:
# WRONG: Not checking if positions are valid left_neighbor = x - k - 1 if vis[left_neighbor]: # Could cause negative index access # Process... right_neighbor = x + k + 1 if vis[right_neighbor]: # Could cause index out of bounds # Process...
Solution:
# CORRECT: Always validate position bounds first left_neighbor = x - k - 1 if left_neighbor > 0 and vis[left_neighbor]: # Safe to process... right_neighbor = x + k + 1 if right_neighbor <= n and vis[right_neighbor]: # Safe to process...
4. Misunderstanding the Problem Requirements
A subtle but critical pitfall is misinterpreting what "exactly k bulbs between them" means.
Pitfall Example:
# WRONG: Looking for k+1 distance instead of k bulbs between left_neighbor = x - k # Should be x - k - 1 right_neighbor = x + k # Should be x + k + 1
Solution: If we need exactly k bulbs between two turned-on bulbs, the distance between them should be k+1. For example, if k=2 and we have bulbs at positions 3 and 6, there are exactly 2 bulbs between them (positions 4 and 5).
5. Inefficient Implementation Without Binary Indexed Tree
Attempting to solve this with naive array scanning for each check.
Pitfall Example:
# WRONG: O(nk) approach - checking each bulb individually
def check_range(start, end, vis):
for i in range(start + 1, end):
if vis[i]:
return False
return True
Solution: Use the Binary Indexed Tree as shown in the main solution to achieve O(log n) range queries, resulting in O(n log n) total time complexity instead of O(nk).
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Want a Structured Path to Master System Design Too? Don’t Miss This!