2300. Successful Pairs of Spells and Potions - Explanation
Description
A spell and a potion form a successful pair when the product of their strengths is at least success. For each value in spells, count how many entries of potions form a successful pair with it. Return those counts in the same order as spells. Repeated potion strengths count separately, and the same potion can be counted for multiple spells.
Example 1:
Input: spells = [2,5,1]
potions = [2,3,6]
success = 10
Output: [1,3,0]Explanation: The spell of strength 2 succeeds only with potion 6. The spell of strength 5 succeeds with all three potions, while the spell of strength 1 succeeds with none. Return [1,3,0].
Example 2:
Input: spells = [4,2]
potions = [3,3,5]
success = 12
Output: [3,0]Explanation: For strength 4, both potions of strength 3 and the potion of strength 5 succeed. Neither strength-3 potion nor strength-5 potion succeeds with the spell of strength 2, so return [3,0].
Example 3:
Input: spells = [1,100000]
potions = [1,100000]
success = 10000000000
Output: [0,1]Explanation: A spell of strength 1 cannot reach the threshold. The strength-100000 spell succeeds only with the strength-100000 potion, whose product is exactly 10000000000. Return [0,1].
Constraints:
1 <= spells.length, potions.length <= 1000001 <= spells[i], potions[i] <= 1000001 <= success <= 10000000000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Sorting - Required to arrange potions in order so that successful pairs form a contiguous range
- Binary Search - Used to efficiently find the threshold index where potions become successful
- Two Pointers - Alternative approach processes sorted spells and potions simultaneously
1. Brute Force
Intuition
For each spell, we need to count how many potions form a successful pair. A pair is successful when spell * potion >= success. The simplest approach is to check every spell against every potion and count the valid combinations.
Algorithm
- Create a
resultarrayresto store the count for each spell. - For each spell
sinspells:- Initialize a counter
cnt = 0. - For each potion
pinpotions:- If
s * p >= success, incrementcnt.
- If
- Append
cnttores.
- Initialize a counter
- Return
res.
Time & Space Complexity
- Time complexity:
- Space complexity:
The output array is not counted towards space complexity, as the standard convention measures only auxiliary working space.
Where and are the sizes of the arrays and respectively.
2. Sorting + Binary Search
Intuition
If we sort the potions array, all successful potions for a given spell will be contiguous at the end. For a spell s, we need the minimum potion strength p such that s * p >= success, which means p >= success / s. Binary search can efficiently find this threshold index, and all potions from that index onward form successful pairs.
Algorithm
- Sort the
potionsarray in ascending order. - For each spell
sinspells:- Use binary search to find the smallest index
idxwheres * potions[idx] >= success. - The count of successful pairs is
len(potions) - idx. - Store this count in the
result.
- Use binary search to find the smallest index
- Return the
resultarray.
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
potions.sort()
res = []
for s in spells:
l, r = 0, len(potions) - 1
idx = len(potions)
while l <= r:
m = (l + r) // 2
if s * potions[m] >= success:
r = m - 1
idx = m
else:
l = m + 1
res.append(len(potions) - idx)
return resTime & Space Complexity
- Time complexity:
- Space complexity:
- or extra space depending on the sorting algorithm.
- space for the output array.
Where and are the sizes of the arrays and respectively.
3. Sorting + Two Pointers
Intuition
If we sort both arrays, we can use the two pointer technique. A weaker spell needs a stronger potion to succeed, and a stronger spell needs at least as weak a potion. By processing spells in ascending order and potions in descending order, we can reuse the potion pointer position. Once a potion works for a spell, it works for all stronger spells too.
Algorithm
- Save the original
spellsarray and sort bothspellsandpotions. - Use a pointer
jstarting at the end of sortedpotions. - For each spell in sorted order:
- Move
jleft whilespell * potions[j] >= success. - Store the count
m - j - 1in a map keyed by spell strength.
- Move
- Build the
resultby looking up each original spell's count in the map. - Return the
result.
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
n, m = len(spells), len(potions)
S = spells[:]
count = defaultdict(int)
spells.sort()
potions.sort()
j = m - 1
for i in range(n):
while j >= 0 and spells[i] * potions[j] >= success:
j -= 1
count[spells[i]] = m - j - 1
res = [0] * n
for i in range(n):
res[i] = count[S[i]]
return resTime & Space Complexity
- Time complexity:
- Space complexity:
- or extra space depending on the sorting algorithm.
- space for the output array.
Where and are the sizes of the arrays and respectively.
4. Sorting + Two Pointers (Optimal)
Intuition
The previous approach uses extra space to map spell values to their counts. We can avoid this by sorting indices of spells rather than the spells themselves. This way, we can directly write results to the correct positions in the output array without needing a lookup map.
Algorithm
- Create an array of indices
sIdxand sort it by corresponding spell strength. - Sort the
potionsarray. - Initialize pointer
jat the end ofpotionsandresultarrayres. - For each
indexiin sorted order:- Move
jleft whilespells[sIdx[i]] * potions[j] >= success. - Set
res[sIdx[i]] = m - j - 1.
- Move
- Return
res.
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
n, m = len(spells), len(potions)
sIdx = list(range(n))
sIdx.sort(key=lambda x: spells[x])
potions.sort()
j = m - 1
res = [0] * n
for i in range(n):
while j >= 0 and spells[sIdx[i]] * potions[j] >= success:
j -= 1
res[sIdx[i]] = m - j - 1
return resTime & Space Complexity
- Time complexity:
- Space complexity:
- or extra space depending on the sorting algorithm.
- space for the output array.
Where and are the sizes of the arrays and respectively.
Common Pitfalls
Integer Overflow in Multiplication
When multiplying spell * potion, the result can exceed the range of a 32-bit integer since both values can be up to 10^5, making the product up to 10^10. Always cast to long before multiplication or use a 64-bit integer type to avoid overflow and incorrect comparisons.
Off-by-One Errors in Binary Search
A common mistake is incorrectly calculating the count of successful pairs after binary search. The binary search finds the first index where the condition is satisfied, and the count should be len(potions) - idx. Errors occur when using <= vs < in the binary search condition or when the index represents the wrong boundary.
Forgetting to Sort the Potions Array
Binary search only works on sorted arrays. A frequent oversight is attempting to use binary search on the original unsorted potions array, which leads to incorrect results. The potions array must be sorted first, and for the two-pointer approach, both arrays need to be sorted while preserving the original spell order for the output.
Sign in to join the discussion