TL;DR
Sort potions, binary search the success cutoff per spell β O((n + m) log m) time, O(1) extra space (O(n) output).
Approach 1 β Brute force
Try every spell against every potion.
from typing import List
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
pairs: List[int] = []
for s in spells:
count = 0
for p in potions:
if s * p >= success:
count += 1
pairs.append(count)
return pairs
- Time: O(nΒ·m). Space: O(1) beyond the output.
With n = m = 10^5 that is 10^10 multiplications β hours of work. The constraints exist precisely to kill this.
Approach 2 β Sort potions + binary search per spell
The insight: for a spell of strength s, potion p succeeds iff p >= success / s. That is a threshold condition β once the potions are sorted, the successes are exactly a suffix, so the count is m - (index of first success), findable by binary search.
Binary search is the classical O(log m) method of locating a boundary in a sorted array by halving the interval each step; bisect_left(a, x) returns the first index whose value is >= x.
To avoid floating-point error on values up to 10^10, use integer ceiling division: the smallest integer potion that works is need = (success + s - 1) // s.
import bisect
from typing import List
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
potions.sort()
m = len(potions)
pairs: List[int] = []
for s in spells:
need = (success + s - 1) // s # ceil(success / s), all-integer
first = bisect.bisect_left(potions, need)
pairs.append(m - first)
return pairs
Walkthrough on spells = [5,1,3], potions = [1,2,3,4,5], success = 7 (potions already sorted, m = 5):
| spell s | need = ceil(7/s) | bisect_left β first | count = 5 β first |
|---|
| 5 | 2 | 1 | 4 |
| 1 | 7 | 5 | 0 |
| 3 | 3 | 2 | 3 |
Result [4, 0, 3] β matches the expected output.
- Time: O(m log m) to sort + O(n log m) for the searches = O((n + m) log m).
- Space: O(1) beyond the output (sort is in place).
Approach 3 β Sort both + two pointers
The insight: if you process spells from strongest to weakest, the cutoff need only increases, so the boundary pointer into the sorted potions only moves rightward β one total sweep instead of n independent searches. This is the classic two-pointer/monotonic-sweep alternative to repeated binary search.
from typing import List
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
potions.sort()
m = len(potions)
order = sorted(range(len(spells)), key=lambda i: -spells[i])
pairs = [0] * len(spells)
j = 0 # potions[0..j-1] fail for the current (and all weaker) spells
for i in order:
s = spells[i]
while j < m and s * potions[j] < success:
j += 1
pairs[i] = m - j
return pairs
Walkthrough on the same example β spells in descending order are 5, 3, 1:
s = 5: advance j past potion 1 (5Β·1 = 5 < 7) β j = 1, count 5 - 1 = 4 at index 0.
s = 3: advance past potion 2 (3Β·2 = 6 < 7) β j = 2, count 3 at index 2.
s = 1: advance past 3, 4, 5 (all products < 7) β j = 5, count 0 at index 1.
Result [4, 0, 3].
- Time: O(n log n + m log m) for the sorts; the sweep itself is O(n + m).
- Space: O(n) for the index ordering.
Same asymptotics once sorting dominates; the bisect version is simpler and is the canonical answer, while the two-pointer version shines when youβd otherwise binary search with an expensive predicate.
Common pitfalls
- Computing the cutoff as
success / s in floating point β at success = 10^10 a float can round the boundary the wrong way. Use (success + s - 1) // s or compare products directly.
- Using
bisect_right instead of bisect_left: pairs meeting the threshold exactly (p == need with s * p == success) must count as successes.
- Forgetting that the output must follow the original spell order β if you sort spells, carry their indices along.
- Sorting
spells instead of potions in the bisect version: the array you binary search must be the sorted one.
Pattern takeaway
When a condition has the shape βvalue β₯ thresholdβ against a fixed collection, sort the collection once and every query becomes a binary search for the boundary; the answer is a suffix (or prefix) length. And when the queries themselves can be ordered so the threshold moves monotonically, the n binary searches collapse into one two-pointer sweep.