LeetCode 2576 - Find the Maximum Number of Marked Indices
This problem asks us to maximize the number of indices in an array that can be "marked" using a specific operation. You are given an integer array nums and can repeatedly pick two different unmarked indices i and j such that 2 nums[i] <= nums[j].
Difficulty: 🟡 Medium
Topics: Array, Two Pointers, Binary Search, Greedy, Sorting
Solution
Problem Understanding
This problem asks us to maximize the number of indices in an array that can be "marked" using a specific operation. You are given an integer array nums and can repeatedly pick two different unmarked indices i and j such that 2 * nums[i] <= nums[j]. Once selected, both indices i and j are marked. The goal is to determine the maximum number of indices that can be marked after performing this operation as many times as possible.
In simpler terms, we are pairing smaller numbers with sufficiently larger numbers to satisfy the 2 * nums[i] <= nums[j] condition. Each successful pairing marks two indices, and we want to count as many marked indices as possible.
Constraints tell us that nums can have up to 100,000 elements and numbers can be very large (<= 10^9). This implies that brute-force checking of all pairs is infeasible due to the O(n^2) time complexity.
Important edge cases include:
- Arrays with no valid pairs, e.g.,
[7,6,8]. - Arrays where multiple valid pairings exist, e.g.,
[9,2,5,4]. - Arrays with repeated elements or all equal elements.
Approaches
Brute Force
A brute-force approach would iterate over all pairs (i, j) of unmarked indices and mark them whenever 2 * nums[i] <= nums[j]. We would continue until no more valid pairs exist. While this approach is correct, it has a time complexity of O(n^2), which is too slow for n = 10^5.
Optimal Approach
The key insight is that sorting the array allows us to efficiently pair smaller numbers with larger numbers using a two-pointer or greedy strategy. After sorting, the smallest numbers are at the start, and the largest are at the end. We can attempt to pair the smallest unmarked numbers with the largest numbers that satisfy the condition 2 * nums[i] <= nums[j]. This approach ensures maximal pairing because:
- We always attempt to use the smallest available
nums[i], leaving larger numbers for potential future pairings. - Sorting guarantees that once a number
nums[i]cannot pair withnums[j], it cannot pair with any smallernums[j]either.
Using two pointers, we iterate through the array from both ends. We count how many valid pairs can be formed and multiply by two to get the total marked indices.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2) | O(n) | Iterate all pairs repeatedly |
| Optimal | O(n log n) | O(1) | Sort array + two-pointer greedy pairing |
Algorithm Walkthrough
- Sort the array
numsin ascending order. Sorting helps us efficiently find pairs where a smaller number can pair with a larger number. - Split the array into two halves: let the first half be potential
icandidates (smaller numbers) and the second half be potentialjcandidates (larger numbers). Use integer divisionn // 2to determine the split. - Initialize two pointers:
istarting at the beginning of the first half, andjstarting at the beginning of the second half. - Count valid pairs: while both pointers are within bounds, check if
2 * nums[i] <= nums[j]. If so, increment the count of valid pairs, and move both pointers forward. If the condition fails, move only thejpointer forward to find a larger number that can pair. - Compute total marked indices: each valid pair marks two indices, so multiply the number of pairs by 2.
Why it works: Sorting ensures that smaller numbers are paired with the smallest sufficient larger numbers. The two-pointer strategy ensures no overlap and maximizes pairings. By iterating in order, we guarantee that no number is wasted or skipped unnecessarily.
Python Solution
from typing import List
class Solution:
def maxNumOfMarkedIndices(self, nums: List[int]) -> int:
nums.sort()
n = len(nums)
left = 0
right = n // 2
count = 0
while left < n // 2 and right < n:
if 2 * nums[left] <= nums[right]:
count += 1
left += 1
right += 1
return count * 2
Explanation: The array is first sorted to enable efficient pairing. We use two pointers to traverse the two halves, counting the number of valid pairs. The left pointer moves only when a valid pair is found, ensuring each smaller number is used at most once. The right pointer always moves, ensuring we explore all larger numbers. Multiplying the count by 2 gives the total marked indices.
Go Solution
import "sort"
func maxNumOfMarkedIndices(nums []int) int {
sort.Ints(nums)
n := len(nums)
left, right := 0, n/2
count := 0
for left < n/2 && right < n {
if 2*nums[left] <= nums[right] {
count++
left++
}
right++
}
return count * 2
}
Explanation: The Go version mirrors the Python solution. We use sort.Ints to sort the slice and two pointers to count pairs. Slice indexing and loop conditions handle boundaries safely. Multiplying by 2 yields the number of marked indices.
Worked Examples
Example 1
nums = [3,5,2,4]
Sorted: [2,3,4,5]
Left half: [2,3], Right half: [4,5]
i=0, j=2: 2*2 <= 4 -> pair, count=1, left=1, right=3
i=1, j=3: 2*3 <= 5 -> false, right++
End loop
Return 2
Example 2
nums = [9,2,5,4]
Sorted: [2,4,5,9]
Left half: [2,4], Right half: [5,9]
i=0, j=2: 2*2 <= 5 -> true, count=1, left=1, right=3
i=1, j=3: 2*4 <= 9 -> true, count=2, left=2, right=4
Return 4
Example 3
nums = [7,6,8]
Sorted: [6,7,8]
Left half: [6], Right half: [7,8]
i=0, j=1: 2*6 <= 7 -> false, j++
i=0, j=2: 2*6 <= 8 -> false, j++
End loop
Return 0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Sorting dominates, two-pointer traversal is O(n) |
| Space | O(1) | Sorting can be done in-place, extra variables are constant |
The algorithm scales efficiently for n up to 100,000 due to sorting plus linear scan.
Test Cases
# Provided examples
assert Solution().maxNumOfMarkedIndices([3,5,2,4]) == 2 # Single valid pair
assert Solution().maxNumOfMarkedIndices([9,2,5,4]) == 4 # Two valid pairs
assert Solution().maxNumOfMarkedIndices([7,6,8]) == 0 # No valid pairs
# Boundary cases
assert Solution().maxNumOfMarkedIndices([1]) == 0 # Single element
assert Solution().maxNumOfMarkedIndices([1,1]) == 0 # Cannot pair equal small numbers
# Stress case
assert Solution().maxNumOfMarkedIndices(list(range(1, 100001))) == 100000 # Fully pairable
assert Solution().maxNumOfMarkedIndices([10**9]*100000) == 0 # All equal large numbers
| Test | Why |
|---|---|
| [3,5,2,4] | Single valid pair |
| [9,2,5,4] | Multiple valid pairs |
| [7,6,8] | No valid pairs |
| [1] | Single element edge case |
| [1,1] | Cannot pair identical small numbers |
| range(1,100001) | Stress test, fully pairable |
| [10^9]*100000 | All equal, no pairs possible |
Edge Cases
Case 1: Single element array
An array with one element cannot form a pair, so the function correctly returns 0.
Case 2: All equal numbers
If all numbers are identical, 2 * nums[i] will never be less than or equal to any other number in the array. The algorithm handles this because the two-pointer check will always fail, returning 0.
Case 3: Large array with maximal numbers
The algorithm correctly handles nums of size up to 100,000 with values up to 10^9 because all calculations involve integer multiplication within Python or Go integer ranges. Sorting and linear traversal remain efficient.
Case 4: Odd-length arrays
Splitting the array in