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 creates a pattern like: false, false, ..., true, true as we increase the time (where true means "time is sufficient to mark all indices").

We want to find the first true position (minimum sufficient time). This is exactly what our standard binary search template finds. The feasible(t) function returns true if time t is sufficient to mark all indices.

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.

Learn more about Binary Search patterns.

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        Uses standard binary search template: find first time that is sufficient.
8        """
9        total_seconds = len(changeIndices)
10
11        def can_mark_all_by_second(seconds: int) -> bool:
12            """
13            Check if all indices can be marked within the given number of seconds.
14            Returns True if feasible, False otherwise.
15            """
16            available_decrements = 0
17            marked_count = 0
18
19            # Find the last occurrence of each index within the time limit
20            last_occurrence = {}
21            for second, index in enumerate(changeIndices[:seconds]):
22                last_occurrence[index] = second
23
24            # Process each second in order
25            for current_second, current_index in enumerate(changeIndices[:seconds]):
26                if last_occurrence[current_index] == current_second:
27                    # Last chance to mark this index
28                    required_decrements = nums[current_index - 1]
29
30                    if available_decrements < required_decrements:
31                        return False
32
33                    available_decrements -= required_decrements
34                    marked_count += 1
35                else:
36                    available_decrements += 1
37
38            return marked_count == len(nums)
39
40        # Binary search using template: find first time that is sufficient
41        left, right = 1, total_seconds
42        first_true_index = -1
43
44        while left <= right:
45            mid = (left + right) // 2
46
47            if can_mark_all_by_second(mid):
48                first_true_index = mid
49                right = mid - 1
50            else:
51                left = mid + 1
52
53        return first_true_index if first_true_index != -1 else -1
54
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 standard binary search template: find first time that is sufficient.
8     */
9    public int earliestSecondToMarkIndices(int[] nums, int[] changeIndices) {
10        this.nums = nums;
11        this.changeIndices = changeIndices;
12
13        int totalSeconds = changeIndices.length;
14
15        // Binary search using template: find first time that is sufficient
16        int left = 1;
17        int right = totalSeconds;
18        int firstTrueIndex = -1;
19
20        while (left <= right) {
21            int mid = left + (right - left) / 2;
22
23            if (canMarkAll(mid)) {
24                firstTrueIndex = mid;
25                right = mid - 1;
26            } else {
27                left = mid + 1;
28            }
29        }
30
31        return firstTrueIndex;
32    }
33
34    /**
35     * Checks if all indices can be marked within the given time limit.
36     * Returns true if feasible, false otherwise.
37     */
38    private boolean canMarkAll(int timeLimit) {
39        int[] lastOccurrence = new int[nums.length + 1];
40
41        for (int second = 0; second < timeLimit; ++second) {
42            lastOccurrence[changeIndices[second]] = second;
43        }
44
45        int availableDecrements = 0;
46        int markedCount = 0;
47
48        for (int second = 0; second < timeLimit; ++second) {
49            int currentIndex = changeIndices[second];
50
51            if (lastOccurrence[currentIndex] == second) {
52                if (availableDecrements < nums[currentIndex - 1]) {
53                    return false;
54                }
55                availableDecrements -= nums[currentIndex - 1];
56                ++markedCount;
57            } else {
58                ++availableDecrements;
59            }
60        }
61
62        return markedCount == nums.length;
63    }
64}
65
1class Solution {
2public:
3    int earliestSecondToMarkIndices(vector<int>& nums, vector<int>& changeIndices) {
4        int n = nums.size();
5        int totalSeconds = changeIndices.size();
6
7        // Lambda: check if we can mark all indices within 't' seconds
8        auto canMarkAll = [&](int secondsLimit) {
9            int lastOccurrence[n + 1];
10            memset(lastOccurrence, 0, sizeof(lastOccurrence));
11
12            for (int second = 0; second < secondsLimit; ++second) {
13                lastOccurrence[changeIndices[second]] = second;
14            }
15
16            int availableDecrements = 0;
17            int markedCount = 0;
18
19            for (int second = 0; second < secondsLimit; ++second) {
20                int currentIndex = changeIndices[second];
21
22                if (lastOccurrence[currentIndex] == second) {
23                    if (availableDecrements < nums[currentIndex - 1]) {
24                        return false;
25                    }
26                    availableDecrements -= nums[currentIndex - 1];
27                    ++markedCount;
28                } else {
29                    ++availableDecrements;
30                }
31            }
32
33            return markedCount == n;
34        };
35
36        // Binary search using template: find first time that is sufficient
37        int left = 1, right = totalSeconds;
38        int firstTrueIndex = -1;
39
40        while (left <= right) {
41            int mid = left + (right - left) / 2;
42
43            if (canMarkAll(mid)) {
44                firstTrueIndex = mid;
45                right = mid - 1;
46            } else {
47                left = mid + 1;
48            }
49        }
50
51        return firstTrueIndex;
52    }
53};
54
1/**
2 * Finds the earliest second at which all indices can be marked.
3 * Uses standard binary search template: find first time that is sufficient.
4 */
5function earliestSecondToMarkIndices(nums: number[], changeIndices: number[]): number {
6    const n: number = nums.length;
7    const m: number = changeIndices.length;
8
9    /**
10     * Checks if all indices can be marked within the given time limit.
11     * Returns true if feasible, false otherwise.
12     */
13    const canMarkAll = (timeLimit: number): boolean => {
14        const lastOccurrence: number[] = Array(n + 1).fill(0);
15
16        for (let second = 0; second < timeLimit; second++) {
17            lastOccurrence[changeIndices[second]] = second;
18        }
19
20        let availableDecrements: number = 0;
21        let markedCount: number = 0;
22
23        for (let second = 0; second < timeLimit; second++) {
24            const currentIndex: number = changeIndices[second];
25
26            if (lastOccurrence[currentIndex] === second) {
27                if (availableDecrements < nums[currentIndex - 1]) {
28                    return false;
29                }
30                availableDecrements -= nums[currentIndex - 1];
31                markedCount++;
32            } else {
33                availableDecrements++;
34            }
35        }
36
37        return markedCount === n;
38    };
39
40    // Binary search using template: find first time that is sufficient
41    let left: number = 1;
42    let right: number = m;
43    let firstTrueIndex: number = -1;
44
45    while (left <= right) {
46        const mid: number = Math.floor((left + right) / 2);
47
48        if (canMarkAll(mid)) {
49            firstTrueIndex = mid;
50            right = mid - 1;
51        } else {
52            left = mid + 1;
53        }
54    }
55
56    return firstTrueIndex;
57}
58

Solution Approach

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

Binary Search Template Setup

We use the standard binary search template to find the first time that is sufficient:

left, right = 1, m
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if can_mark_all(mid):  # feasible(mid) = "can mark all indices in mid seconds"
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index if first_true_index != -1 else -1

The feasible(t) function returns True if time t is sufficient to mark all indices. This directly fits the template pattern.

The Feasible Function Implementation

For a given time t, we 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.

  2. Traverse through the first t seconds: 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
  4. Final validation:

    return marked == len(nums)

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.

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. Using Wrong Binary Search Template

Pitfall: Not using the standard binary search template with first_true_index tracking.

Common mistakes:

# WRONG: Using while left < right with different update rules
while left < right:
    mid = (left + right) >> 1
    if can_mark_all(mid):
        right = mid
    else:
        left = mid + 1
return left if left <= m else -1

Correct approach using template:

# CORRECT: Standard template with first_true_index
left, right = 1, m
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if can_mark_all(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index if first_true_index != -1 else -1

2. 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
  • changeIndices contains 1-based index values

Pitfall Example:

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

Solution:

required_decrements = nums[current_index - 1]  # Correct conversion

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 incorrect results.

Solution: The algorithm naturally handles this because marked_count won't reach len(nums) if some indices never appear.

4. 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

Solution: Only accumulate decrements when NOT marking:

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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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

Load More