Facebook Pixel

3048. Earliest Second to Mark Indices I

Problem Description

You have two 1-indexed arrays: nums of length n and changeIndices of length m. Your goal is to mark all indices in nums, which initially are all unmarked.

The process works through seconds from 1 to m. At each second s, you can perform exactly one of these operations:

  1. Decrement operation: Choose any index i in range [1, n] and decrease nums[i] by 1
  2. Mark operation: If nums[changeIndices[s]] equals 0, you can mark the index changeIndices[s]
  3. Do nothing: Skip this second without performing any action

The key constraint is that you can only mark an index when two conditions are met:

  • The value at that index in nums must be 0
  • The current second s must have changeIndices[s] pointing to that index

Your task is to find the earliest second (between 1 and m) when it's possible to mark all indices in nums by choosing operations optimally. If it's impossible to mark all indices within m seconds, return -1.

For example, if you want to mark index i, you need to:

  • First reduce nums[i] to 0 through multiple decrement operations
  • Then wait for a second s where changeIndices[s] = i to actually mark it

The challenge is to strategically choose when to decrement values and when to mark indices, considering that changeIndices dictates which indices can be marked at each second.

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

Intuition

The first key observation is that if we can mark all indices within t seconds, we can definitely mark them within any time t' > t seconds. This monotonic property immediately suggests binary search on the answer - we search for the minimum time needed.

To understand why this works, consider what happens when we have more time available. With extra seconds, we can either perform more decrements to reduce values faster, or we have more opportunities where changeIndices[s] points to indices we want to mark. More time never hurts our ability to complete the task.

For checking if a specific time t is sufficient, we need to think strategically about when to mark each index. The crucial insight is that for each index i, we should mark it at the last possible opportunity within the first t seconds. Why? Because marking an index early doesn't give us any advantage - we still need to reduce its value to 0 first. By waiting until the last occurrence of index i in changeIndices[:t], we maximize the time available to decrement its value.

This leads to our checking strategy:

  1. First, identify the last occurrence of each index within changeIndices[:t] using a last dictionary
  2. As we traverse through seconds 1 to t, we make decisions:
    • If the current second is the last chance to mark index i (i.e., last[i] == s), we must have already decremented nums[i-1] to 0. We check if we've accumulated enough decrement operations
    • If it's not the last chance for this index, we can use this second to decrement any value, adding to our decrement budget

The decrement variable acts like a budget - it tracks how many decrement operations we can perform. When we encounter the last chance to mark an index, we "spend" from this budget (the amount equals nums[i-1]). If we don't have enough budget at any point, time t is insufficient.

This greedy approach of marking at the last possible moment ensures we have maximum flexibility in scheduling our decrement operations, making it the optimal strategy for checking feasibility.

Learn more about Binary Search patterns.

Solution Approach

The solution uses binary search combined with a greedy checking function to find the minimum time needed.

Binary Search Setup:

  • We search in the range [1, m+1] where m = len(changeIndices)
  • Using bisect_left with a custom key function that returns True if time t is sufficient
  • The result needs +1 adjustment because bisect_left returns 0-indexed position

The Check Function Implementation:

For a given time t, we need to verify if all indices can be marked within t seconds:

  1. Build the last dictionary:

    last = {i: s for s, i in enumerate(changeIndices[:t])}

    This maps each index to its last occurrence position within the first t elements of changeIndices. We use 0-based indexing here for enumeration.

  2. Traverse through the first t seconds:

    for s, i in enumerate(changeIndices[:t]):

    For each second s and its corresponding index i:

  3. Decision at each second:

    • If last[i] == s (this is the last chance to mark index i):
      • Check if we have enough decrements: if decrement < nums[i - 1]: return False
      • If sufficient, consume the decrements: decrement -= nums[i - 1]
      • Mark this index: marked += 1
    • Otherwise (not the last chance for this index):
      • Add this second to our decrement budget: decrement += 1
      • We can use this second later for any decrement operation
  4. Final validation:

    return marked == len(nums)

    Check if we successfully marked all n indices.

Why this greedy approach works:

  • By marking each index at its last possible occurrence, we maximize the time available for accumulating decrements
  • The decrement counter tracks our "budget" - seconds we can use for decrementing values
  • When we must mark an index (at its last occurrence), we verify we've accumulated enough decrements to have reduced that index's value to 0
  • If at any point we don't have enough decrements, the time t is insufficient

Time Complexity:

  • Binary search runs O(log m) iterations
  • Each check function runs in O(t) time where t ≤ m
  • Overall: O(m log m)

Space Complexity: O(n) for the last dictionary storing at most n unique indices.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Input:

  • nums = [3, 2, 3] (1-indexed, so index 1 has value 3, index 2 has value 2, index 3 has value 3)
  • changeIndices = [1, 3, 2, 2, 2, 2, 3] (length m = 7)

Goal: Find the earliest second when all three indices can be marked.

Step 1: Binary Search Setup We'll search for the minimum time in range [1, 8). Let's check if time t = 4 is sufficient.

Step 2: Check if t = 4 works

First, we look at changeIndices[:4] = [1, 3, 2, 2] and build the last dictionary:

  • Index 1 appears at position 0 (last occurrence)
  • Index 3 appears at position 1 (last occurrence)
  • Index 2 appears at positions 2 and 3 (last at position 3)

So last = {1: 0, 3: 1, 2: 3}

Now we traverse second by second:

Second 0 (changeIndices[0] = 1):

  • This is the last chance to mark index 1 (last[1] = 0)
  • We need nums[0] = 3 decrements to reduce it to 0
  • Current decrement = 0, but we need 3
  • Return False - time 4 is insufficient!

Step 3: Check if t = 7 works

Looking at changeIndices[:7] = [1, 3, 2, 2, 2, 2, 3]:

  • Build last = {1: 0, 3: 6, 2: 5}

Traverse second by second:

Second 0 (changeIndices[0] = 1):

  • Last chance for index 1 (last[1] = 0)
  • Need 3 decrements, have 0
  • Return False - Still insufficient!

Actually, let's reconsider with a better strategy. Since index 1 must be marked at second 0, we need to have already decremented it to 0 before we start. This is impossible.

Step 4: Reanalyze the problem

Looking more carefully at the full changeIndices = [1, 3, 2, 2, 2, 2, 3]:

  • We need at least 3 + 2 + 3 = 8 total operations (decrements) plus 3 marks
  • That's 11 operations minimum, but we only have 7 seconds
  • Return -1 - It's impossible!

Let's try a simpler example where it's actually possible:

Modified Example:

  • nums = [2, 1, 1]
  • changeIndices = [1, 2, 3, 2, 1, 3]

Check if t = 6 works:

Build last = {1: 4, 2: 3, 3: 5} (using 0-based indexing)

Second 0 (index 1): Not last occurrence, decrement = 1 Second 1 (index 2): Not last occurrence, decrement = 2
Second 2 (index 3): Not last occurrence, decrement = 3 Second 3 (index 2): Last chance for index 2!

  • Need nums[1] = 1 decrement
  • Have 3 decrements available
  • Use 1, mark index 2: decrement = 2, marked = 1

Second 4 (index 1): Last chance for index 1!

  • Need nums[0] = 2 decrements
  • Have 2 decrements available
  • Use 2, mark index 1: decrement = 0, marked = 2

Second 5 (index 3): Last chance for index 3!

  • Need nums[2] = 1 decrement
  • Have 0 decrements available
  • Return False - Not enough!

Since t = 6 fails, binary search would try larger values. Eventually it would find that we need more time to accumulate enough decrements before marking all indices.

Solution Implementation

1class Solution:
2    def earliestSecondToMarkIndices(
3        self, nums: List[int], changeIndices: List[int]
4    ) -> int:
5        """
6        Find the earliest second when all indices can be marked.
7      
8        Args:
9            nums: List of values at each index (1-indexed in changeIndices)
10            changeIndices: List indicating which index can be changed at each second
11      
12        Returns:
13            The earliest second to mark all indices, or -1 if impossible
14        """
15      
16        def can_mark_all_by_second(seconds: int) -> bool:
17            """
18            Check if all indices can be marked within the given number of seconds.
19          
20            Strategy: 
21            - Track available decrement operations
22            - For each index, we can only mark it at its last occurrence
23            - Before marking, we need to decrement its value to 0
24            """
25            available_decrements = 0
26            marked_count = 0
27          
28            # Find the last occurrence of each index within the time limit
29            # changeIndices uses 1-based indexing
30            last_occurrence = {}
31            for second, index in enumerate(changeIndices[:seconds]):
32                last_occurrence[index] = second
33          
34            # Process each second in order
35            for current_second, current_index in enumerate(changeIndices[:seconds]):
36                # Check if this is the last chance to mark this index
37                if last_occurrence[current_index] == current_second:
38                    # Need to have decremented nums[current_index - 1] to 0
39                    # (converting from 1-based to 0-based indexing)
40                    required_decrements = nums[current_index - 1]
41                  
42                    if available_decrements < required_decrements:
43                        # Not enough decrements available, can't mark this index
44                        return False
45                  
46                    # Use decrements and mark this index
47                    available_decrements -= required_decrements
48                    marked_count += 1
49                else:
50                    # This second can be used for a decrement operation
51                    available_decrements += 1
52          
53            # Check if all indices were successfully marked
54            return marked_count == len(nums)
55      
56        # Binary search for the minimum seconds needed
57        total_seconds = len(changeIndices)
58      
59        # Search in range [1, total_seconds + 1] for the first valid second count
60        # Using bisect_left with key function to find first True value
61        minimum_seconds = bisect_left(
62            range(1, total_seconds + 2), 
63            True, 
64            key=can_mark_all_by_second
65        ) + 1
66      
67        # Return -1 if no valid solution exists within available seconds
68        return -1 if minimum_seconds > total_seconds else minimum_seconds
69
1class Solution {
2    private int[] nums;
3    private int[] changeIndices;
4
5    /**
6     * Finds the earliest second at which all indices can be marked.
7     * Uses binary search to find the minimum time needed.
8     * 
9     * @param nums Array where nums[i] represents the value that must be decremented to 0 before marking index i+1
10     * @param changeIndices Array where changeIndices[s] represents which index can be marked at second s+1
11     * @return The earliest second to mark all indices, or -1 if impossible
12     */
13    public int earliestSecondToMarkIndices(int[] nums, int[] changeIndices) {
14        this.nums = nums;
15        this.changeIndices = changeIndices;
16      
17        int totalSeconds = changeIndices.length;
18      
19        // Binary search on the answer: minimum seconds needed
20        int left = 1;
21        int right = totalSeconds + 1;
22      
23        while (left < right) {
24            int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
25          
26            if (check(mid)) {
27                // If we can mark all indices in 'mid' seconds, try fewer seconds
28                right = mid;
29            } else {
30                // If we cannot mark all indices in 'mid' seconds, we need more time
31                left = mid + 1;
32            }
33        }
34      
35        // If left exceeds totalSeconds, it's impossible to mark all indices
36        return left > totalSeconds ? -1 : left;
37    }
38
39    /**
40     * Checks if all indices can be marked within the given time limit.
41     * 
42     * @param timeLimit The number of seconds available
43     * @return true if all indices can be marked within timeLimit seconds, false otherwise
44     */
45    private boolean check(int timeLimit) {
46        // Track the last occurrence of each index within the time limit
47        // last[i] stores the last second when index i appears in changeIndices
48        int[] lastOccurrence = new int[nums.length + 1];
49      
50        for (int second = 0; second < timeLimit; ++second) {
51            lastOccurrence[changeIndices[second]] = second;
52        }
53      
54        // Track available decrement operations and successfully marked indices
55        int availableDecrements = 0;
56        int markedCount = 0;
57      
58        // Process each second up to the time limit
59        for (int second = 0; second < timeLimit; ++second) {
60            int currentIndex = changeIndices[second];
61          
62            // Check if this is the last chance to mark this index
63            if (lastOccurrence[currentIndex] == second) {
64                // We must mark this index now (last opportunity)
65                // Check if we have enough decrements to reduce nums[currentIndex-1] to 0
66                if (availableDecrements < nums[currentIndex - 1]) {
67                    // Not enough decrements accumulated, cannot mark this index
68                    return false;
69                }
70              
71                // Use the required decrements and mark the index
72                availableDecrements -= nums[currentIndex - 1];
73                ++markedCount;
74            } else {
75                // This index appears again later, so we can use this second for decrementing
76                ++availableDecrements;
77            }
78        }
79      
80        // Check if all indices have been marked
81        return markedCount == nums.length;
82    }
83}
84
1class Solution {
2public:
3    int earliestSecondToMarkIndices(vector<int>& nums, vector<int>& changeIndices) {
4        int n = nums.size();
5      
6        // Lambda function to check if we can mark all indices within 't' seconds
7        auto canMarkAllIndices = [&](int secondsLimit) {
8            // Track the last occurrence of each index in changeIndices within the time limit
9            int lastOccurrence[n + 1];
10            memset(lastOccurrence, 0, sizeof(lastOccurrence));
11          
12            // Find the last second when each index appears
13            for (int second = 0; second < secondsLimit; ++second) {
14                lastOccurrence[changeIndices[second]] = second;
15            }
16          
17            // Track available operations for decrementing and number of marked indices
18            int availableDecrements = 0;
19            int markedCount = 0;
20          
21            // Process each second in order
22            for (int second = 0; second < secondsLimit; ++second) {
23                int currentIndex = changeIndices[second];
24              
25                // Check if this is the last occurrence of this index
26                if (lastOccurrence[currentIndex] == second) {
27                    // We must mark this index now (last chance)
28                    // Check if we have enough decrements to reduce nums[i-1] to 0
29                    if (availableDecrements < nums[currentIndex - 1]) {
30                        return false;  // Not enough operations to prepare this index
31                    }
32                  
33                    // Use the required decrements and mark the index
34                    availableDecrements -= nums[currentIndex - 1];
35                    ++markedCount;
36                } else {
37                    // This index will appear again, so we can use this second for decrementing
38                    ++availableDecrements;
39                }
40            }
41          
42            // Check if all indices have been marked
43            return markedCount == n;
44        };
45      
46        // Binary search for the minimum number of seconds needed
47        int totalSeconds = changeIndices.size();
48        int left = 1;
49        int right = totalSeconds + 1;
50      
51        while (left < right) {
52            int mid = (left + right) >> 1;
53          
54            if (canMarkAllIndices(mid)) {
55                right = mid;  // Try to find an earlier time
56            } else {
57                left = mid + 1;  // Need more time
58            }
59        }
60      
61        // Return -1 if impossible, otherwise return the minimum seconds needed
62        return left > totalSeconds ? -1 : left;
63    }
64};
65
1/**
2 * Finds the earliest second at which all indices can be marked.
3 * Uses binary search to find the minimum time needed.
4 * 
5 * @param nums - Array where nums[i] represents the value at index i+1
6 * @param changeIndices - Array where changeIndices[s] represents the index that can be changed at second s+1
7 * @returns The earliest second to mark all indices, or -1 if impossible
8 */
9function earliestSecondToMarkIndices(nums: number[], changeIndices: number[]): number {
10    const n: number = nums.length;
11    const m: number = changeIndices.length;
12  
13    // Binary search bounds: minimum 1 second, maximum m+1 seconds
14    let left: number = 1;
15    let right: number = m + 1;
16  
17    /**
18     * Checks if all indices can be marked within the given time limit.
19     * 
20     * @param timeLimit - The number of seconds available
21     * @returns true if all indices can be marked within timeLimit seconds
22     */
23    const canMarkAllIndices = (timeLimit: number): boolean => {
24        // Track the last occurrence of each index in changeIndices within timeLimit
25        const lastOccurrence: number[] = Array(n + 1).fill(0);
26      
27        // Find the last second each index appears
28        for (let second = 0; second < timeLimit; second++) {
29            lastOccurrence[changeIndices[second]] = second;
30        }
31      
32        // Track available decrements and number of marked indices
33        let availableDecrements: number = 0;
34        let markedCount: number = 0;
35      
36        // Process each second up to timeLimit
37        for (let second = 0; second < timeLimit; second++) {
38            const currentIndex: number = changeIndices[second];
39          
40            // If this is the last occurrence of this index, try to mark it
41            if (lastOccurrence[currentIndex] === second) {
42                // Check if we have enough decrements to reduce the value to 0
43                if (availableDecrements < nums[currentIndex - 1]) {
44                    return false; // Not enough decrements available
45                }
46              
47                // Use decrements to reduce value to 0 and mark the index
48                availableDecrements -= nums[currentIndex - 1];
49                markedCount++;
50            } else {
51                // This second can be used for a decrement operation
52                availableDecrements++;
53            }
54        }
55      
56        // Check if all indices have been marked
57        return markedCount === n;
58    };
59  
60    // Binary search for the minimum time needed
61    while (left < right) {
62        const mid: number = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
63      
64        if (canMarkAllIndices(mid)) {
65            // If possible with mid seconds, try fewer seconds
66            right = mid;
67        } else {
68            // If not possible with mid seconds, need more seconds
69            left = mid + 1;
70        }
71    }
72  
73    // Return the result: -1 if impossible, otherwise the minimum seconds needed
74    return left > m ? -1 : left;
75}
76

Time and Space Complexity

Time Complexity: O(m × log m)

The algorithm uses binary search on the range [1, m+1] where m is the length of changeIndices. The binary search performs O(log m) iterations. For each iteration, the check function is called, which:

  • Creates a dictionary last by iterating through changeIndices[:t], taking O(t) time where t ≤ m
  • Iterates through changeIndices[:t] once more to validate the marking process, taking O(t) time

Since t can be at most m, each call to check takes O(m) time. Therefore, the total time complexity is O(m × log m).

Space Complexity: O(n)

The space complexity comes from:

  • The last dictionary which stores at most n key-value pairs (where n is the length of nums), since there are n unique indices that can be marked
  • The local variables decrement and marked use O(1) space

Therefore, the overall space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Index Confusion Between 0-based and 1-based Systems

The most common pitfall in this problem is mixing up the indexing systems:

  • nums array uses 0-based indexing in Python
  • changeIndices contains 1-based index values
  • The enumeration in the loop uses 0-based indexing

Pitfall Example:

# WRONG: Forgetting to convert from 1-based to 0-based
required_decrements = nums[current_index]  # Should be nums[current_index - 1]

Solution: Always remember to subtract 1 when accessing nums array using indices from changeIndices:

required_decrements = nums[current_index - 1]  # Correct conversion

2. Incorrect Binary Search Range

Pitfall Example:

# WRONG: Using 0-based range
minimum_seconds = bisect_left(range(0, total_seconds), True, key=can_mark_all_by_second)

This would search starting from 0 seconds, which doesn't make sense since we need at least 1 second.

Solution: Use the correct 1-based range and adjust the result:

minimum_seconds = bisect_left(
    range(1, total_seconds + 2), 
    True, 
    key=can_mark_all_by_second
) + 1

3. Not Handling Indices That Never Appear

Pitfall Example: If an index from nums never appears in changeIndices[:seconds], it won't be in the last_occurrence dictionary, leading to a KeyError when checking last_occurrence[current_index].

Solution: First verify that all indices appear at least once:

def can_mark_all_by_second(seconds: int) -> bool:
    last_occurrence = {}
    for second, index in enumerate(changeIndices[:seconds]):
        last_occurrence[index] = second
  
    # Check if all indices appear at least once
    if len(last_occurrence) < len(nums):
        return False  # Some indices never appear, impossible to mark all
  
    # Continue with the rest of the logic...

4. Off-by-One Error in Binary Search Result

Pitfall Example:

# WRONG: Forgetting to add 1 to bisect_left result
minimum_seconds = bisect_left(range(1, total_seconds + 2), True, key=can_mark_all_by_second)

Since bisect_left returns a 0-based index position but we're searching in a 1-based range, we need to adjust.

Solution: Add 1 to convert from position to actual second value:

minimum_seconds = bisect_left(...) + 1

5. Incorrect Greedy Logic for Accumulating Decrements

Pitfall Example:

# WRONG: Accumulating decrements even when marking
if last_occurrence[current_index] == current_second:
    available_decrements -= nums[current_index - 1]
    available_decrements += 1  # WRONG: shouldn't add here
    marked_count += 1

Solution: Only accumulate decrements when NOT marking (using the second for other operations):

if last_occurrence[current_index] == current_second:
    available_decrements -= nums[current_index - 1]
    marked_count += 1
else:
    available_decrements += 1  # Only add when not marking
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More