Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 be k+1 positions to the left)
  • Position x + k + 1 (a bulb that would be k+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 array c of size n+1 initialized with zeros
  • update(x, delta): Adds delta to position x and updates all affected nodes using the formula x += x & -x to traverse up the tree
  • query(x): Returns the sum from position 1 to x by traversing down using x -= x & -x

2. Main Algorithm

Initialize:

  • Create a Binary Indexed Tree of size n
  • Create a boolean array vis of size n+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) and vis[y] (bulb at y is on):
    • Check if all bulbs between y and x are off using: tree.query(x - 1) - tree.query(y) == 0
    • If true, return current day i

c. Check right neighbor:

  • Calculate potential right partner: y = x + k + 1
  • If y <= n (valid position) and vis[y] (bulb at y is on):
    • Check if all bulbs between x and y are off using: tree.query(y - 1) - tree.query(x) == 0
    • If true, return current day i

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 Evaluator

Example 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 takes O(log n) time, as it traverses at most log n nodes by incrementing x 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 takes O(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 size n + 1: O(n) space
  • Boolean array vis of size n + 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).

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More