Successful Pairs of Spells and Potions — Sort and Binary Search [LC 2300]
Advertisement
Problem Statement
LeetCode 2300 — Successful Pairs of Spells and Potions · Difficulty: Medium
You are given two positive integer arrays spells and potions, each of length n and m respectively, and a positive integer success. A pair (spell, potion) is successful if the product spell * potion >= success. Return an integer array pairs of length n where pairs[i] is the number of potions that form a successful pair with the i-th spell.
Constraints:
n == spells.lengthm == potions.length1 <= n, m <= 10^51 <= spells[i], potions[i] <= 10^51 <= success <= 10^10
Example 1:
Input: spells = [5,1,3], potions = [1,2,3,4,5], success = 7
Output: [4,0,3]
Explanation: Spell 5: needs potion >= ceil(7/5) = 2. Potions 2,3,4,5 qualify → 4.
Spell 1: needs potion >= ceil(7/1) = 7. No potion >= 7 → 0.
Spell 3: needs potion >= ceil(7/3) = 3. Potions 3,4,5 qualify → 3.Example 2:
Input: spells = [3,1,2], potions = [8,5,8], success = 16
Output: [2,0,2]
Explanation: Spell 3: needs potion >= ceil(16/3) = 6. Potions 8,8 qualify → 2.
Spell 1: needs potion >= 16. No potion qualifies → 0.
Spell 2: needs potion >= 8. Potions 8,8 qualify → 2.Example 3:
Input: spells = [10], potions = [1,1,1,1], success = 5
Output: [4]
Explanation: Spell 10: needs potion >= ceil(5/10) = 1. All 4 potions qualify.Why This Problem Matters
This problem teaches one of the most important practical binary search patterns: sort once, query many times. The naive approach — for each spell, scan all potions — is O(n * m), which TLEs for n = m = 10^5 (10^10 operations). The optimised approach sorts potions once in O(m log m), then binary searches for each spell in O(log m), giving O((n + m) log m) overall.
This "sort once, binary search per query" pattern appears constantly in real codebases and interviews: range queries, threshold counting, proximity problems. If you can recognise when a brute-force is O(n * m) and apply sorting + binary search to reduce it to O((n + m) log m), you handle a huge class of problems efficiently.
The ceiling division trick — ceil(success / spell) gives the minimum potion value — is also a frequently tested technique that avoids floating-point errors.
The Core Insight
For a given spell s, we need s * potion >= success, which means potion >= success / s. Since both spell and potion are integers, the minimum valid potion is ceil(success / s).
Why sorting helps: once potions are sorted in ascending order, all potions from index idx onward satisfy potion >= threshold. The count is simply m - idx. We need to find idx — the leftmost index where potions[idx] >= threshold. This is a classic left-boundary binary search.
Ceiling division without floats: ceil(a / b) = (a + b - 1) // b for positive integers. This avoids floating-point precision issues that can cause off-by-one errors.
One sort, n binary searches: sort potions once, then run a binary search for each spell. This reuses the sorted structure efficiently.
Visual Dry Run
Input: spells = [5, 1, 3], potions = [1, 2, 3, 4, 5], success = 7
Step 1: Sort potions → [1, 2, 3, 4, 5] (already sorted here).
Spell = 5:
- Threshold =
ceil(7 / 5) = ceil(1.4) = 2(or(7 + 5 - 1) // 5 = 11 // 5 = 2) - Binary search for leftmost index where
potions[mid] >= 2
| Step | lo | hi | mid | potions[mid] | Condition | Decision |
|---|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 3 | 3 >= 2? Yes | hi = 2 |
| 2 | 0 | 2 | 1 | 2 | 2 >= 2? Yes | hi = 1 |
| 3 | 0 | 1 | 0 | 1 | 1 >= 2? No | lo = 1 |
| 4 | lo=hi=1 | — | — | — | — | idx = 1 |
Count = 5 - 1 = 4. Correct.
Spell = 1:
- Threshold =
ceil(7 / 1) = 7 - Binary search for leftmost index where
potions[mid] >= 7. No such index → lo = 5. - Count = 5 - 5 = 0. Correct.
Spell = 3:
- Threshold =
ceil(7 / 3) = 3(since(7 + 3 - 1) // 3 = 9 // 3 = 3) - Binary search finds idx = 2 (
potions[2] = 3 >= 3). - Count = 5 - 2 = 3. Correct.
Common Mistakes
-
Using floating-point division for threshold —
success / spellin Python 3 or JavaScript gives a float. Ifsuccess = 6, spell = 3, you get2.0exactly, which works. Butsuccess = 7, spell = 3gives2.333.... Usingmath.ceil(2.333) = 3is correct, but floating-point precision can fail at edge cases likesuccess = 10^10, spell = 2. Always prefer integer ceiling:(success + spell - 1) // spell. -
Forgetting to sort potions — sorting spells instead of potions loses the structure needed for binary search. Potions must be sorted because we binary search within them.
-
Sorting spells — sorting spells changes the output order since
pairs[i]must correspond tospells[i]. Never sort spells. Sort only potions. -
Using
bisect_rightinstead ofbisect_left—bisect_leftfinds the leftmost index wherepotions[idx] >= threshold.bisect_rightfinds the index after all equal elements, overcounting whenpotions[idx] == thresholdexactly (those should be included). -
Integer overflow in the product check —
spell * potioncan reach10^5 * 10^5 = 10^10, exceeding 32-bit int. Always use 64-bit arithmetic (Python handles this automatically; uselongin Java). -
Off-by-one in count — the answer is
m - idx(notm - idx - 1). All potions from indexidxtom-1inclusive qualify. That ism - idxpotions. -
Not handling the edge where no potion qualifies — when
loends atmafter the binary search,m - m = 0. This is correct and handled automatically, but worth verifying your implementation does not crash with an out-of-bounds access.
Solutions
Python
import bisect
import math
def successfulPairs(spells: list[int], potions: list[int], success: int) -> list[int]:
potions.sort() # sort once — O(m log m)
m = len(potions)
result = []
for spell in spells:
# Minimum potion value needed: ceil(success / spell)
# Integer ceiling: avoids floating-point precision issues
threshold = (success + spell - 1) // spell
# Find leftmost index where potions[idx] >= threshold
# bisect_left returns insertion point, which equals first index >= threshold
idx = bisect.bisect_left(potions, threshold)
# All potions from idx to m-1 qualify
result.append(m - idx)
return resultJavaScript
function successfulPairs(spells, potions, success) {
// Sort potions once — O(m log m)
potions.sort((a, b) => a - b);
const m = potions.length;
const result = [];
for (const spell of spells) {
// Minimum potion needed: ceil(success / spell)
// Use BigInt-safe integer ceiling to avoid overflow and float precision issues
const threshold = Math.ceil(success / spell);
// Left-boundary binary search: find first index where potions[idx] >= threshold
let lo = 0, hi = m;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (potions[mid] >= threshold) {
hi = mid; // threshold met: search further left
} else {
lo = mid + 1; // below threshold: first valid index is to the right
}
}
// lo is the first qualifying index; m - lo potions qualify
result.push(m - lo);
}
return result;
}Complexity Analysis
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort + Binary Search (this) | O((n + m) log m) | O(1) extra | Sort once, binary search per spell |
| Brute force (all pairs) | O(n * m) | O(1) | TLE: 10^10 operations for max input |
| Sort spells + two pointers | Not applicable | — | Sorting spells changes output order |
With n = m = 10^5, the brute-force does 10^10 operations. The sort-and-search approach does m log m + n log m ≈ 2 * 10^5 * 17 ≈ 3.4 * 10^6 operations — roughly 3000x faster.
Follow-up Questions
-
Why not sort spells? — Sorting spells would rearrange
pairs[i], breaking the correspondence betweenspells[i]andpairs[i]. If you need both sorted, keep a mapping of original indices. -
Can we use two pointers instead of binary search? — Yes, if you sort both arrays. Sort potions ascending, sort spells descending, then walk a pointer across potions for each spell. However, sorting spells loses the index correspondence unless you store and restore it. Binary search avoids this complexity.
-
How to handle
successas a large value (up to 10^10) in JavaScript? — JavaScript numbers are 64-bit floats, which can represent integers exactly up to 2^53 (about 9 * 10^15).success <= 10^10is safely below this.Math.ceil(success / spell)is also safe for these ranges. For truly giant values, use BigInt. -
What if spells or potions contain 0? — The constraints guarantee
spells[i] >= 1andpotions[i] >= 1. If spell were 0, the product would always be 0, and we could short-circuit to 0 pairs without division. -
How does this relate to the two-sum binary search pattern? — Both use "sort one array, binary search per query." Two-sum's complement search (
target - current) is the same structural idea as finding a threshold. The pattern is: reduce a 2D problem to 1D by sorting.
This Pattern Solves
- LC 2300 — Successful Pairs of Spells and Potions (this problem)
- LC 1855 — Maximum Distance Between a Pair of Values
- LC 2563 — Count the Number of Fair Pairs
- LC 1498 — Number of Subsequences That Satisfy the Given Sum Condition
- LC 981 — Time Based Key-Value Store
- Any problem where each query asks "how many elements in a sorted set satisfy a threshold condition?"
Key Takeaway
When you need to count "how many elements in array B satisfy a condition relative to each element in array A," the answer is almost always sort B, binary search per element of A. This reduces O(n * m) brute force to O((n + m) log m). For this specific problem: for each spell, compute the minimum potion threshold using integer ceiling division, then use left-boundary binary search to find the first qualifying potion. The count is m - leftmost_qualifying_index.
Key Takeaways
- LC 2300 combines sorting with per-query binary search — asked in Google and Amazon assessments to test the "sort once, query many times" optimisation pattern.
- For each spell
s, the minimum potionpthat forms a successful pair satisfiess * p >= success, sop >= ceil(success / s). - Sort potions once in O(m log m), then for each spell use left-boundary binary search to find the first qualifying potion index in O(log m).
- The count of successful pairs for spell
sism - leftBoundaryIndex— elements from the boundary to the end all qualify. - Integer ceiling division avoids floating-point:
ceil(success / s)equals(success + s - 1) // sin integer arithmetic. - Do NOT sort spells — sorting them would break the index correspondence between
spells[i]andpairs[i]. - This pattern generalises to any problem asking "for each element in A, how many elements in B satisfy a threshold condition?" — sort B, binary search per A element.
Advertisement