Skip to content
← All posts

Successful Pairs of Spells and Potions: Binary Search on Sorted Array

5 min read
leetcodeproblemmediumarraysbinary-searchsorting

Successful Pairs of Spells and Potions is LeetCode 2300. A spell and potion pair is "successful" if their product is at least success. The brute force way is to check every spell against every potion, but that is too slow for the given constraints. The key insight: sort the potions array once, then binary search for each spell to find where the threshold lies. Everything to the right of the threshold is a successful pair.

The problem

You are given two arrays, spells and potions, along with a long integer success. A pair (i, j) is successful if spells[i] * potions[j] >= success. For each spell, return the number of potions that form a successful pair with it.

Example: spells = [5, 1, 3], potions = [1, 2, 3, 4, 5], success = 7. Output: [4, 0, 3].

spell = 5, success = 7potions1idx 02idx 13idx 24idx 35idx 45 x 1 = 55 x 2 = 105 x 3 = 155 x 4 = 205 x 5 = 25threshold4 successful pairsproduct 7 (success)product < 7 (fail)
spells = [5, 1, 3], potions = [1, 2, 3, 4, 5] (sorted), success = 7. For spell 5, potions 2, 3, 4, and 5 all produce products at or above 7.

Why sorting enables binary search

The brute force approach checks every spell-potion pair, which is O(n * m). That is too slow when both arrays can have up to 10^5 elements.

Here is the observation that changes everything. If you sort the potions array, then for any given spell, the products spell * potion increase as you move right through the sorted potions. That means there is a boundary: every potion to the left of the boundary produces a product below success, and every potion at or to the right of the boundary produces a product at or above success.

Finding that boundary in a sorted array is exactly what binary search does. Instead of checking all n potions for each spell, you binary search in O(log n) time.

The approach

For each spell s, you need a potion p such that s * p >= success. Rearranging, you need p >= success / s. Since potions are integers, you need p >= ceil(success / s). Sort the potions array once, then for each spell, binary search for the leftmost position where potions[idx] >= ceil(success / s). The number of successful potions is len(potions) - idx.

Visual walkthrough

Step 1: Sort potions

unsorted12345already sorted

Sorting lets us binary search for the threshold potion value.

Step 2: spell = 5. Need potion ≥ ceil(7/5) = 2. Binary search for 2.

1idx 02idx 13idx 24idx 35idx 4lomidhi
potions[1] = 2 2. Found at index 1. Result: 5 - 1 = 4

Binary search finds index 1. Count = 5 - 1 = 4 successful potions.

Step 3: spell = 1. Need potion ≥ ceil(7/1) = 7. Binary search for 7.

1idx 02idx 13idx 24idx 35idx 4
No potion 7. Index = 5 (past end). Result: 5 - 5 = 0

Binary search lands past the end at index 5. Count = 5 - 5 = 0 successful potions.

Step 4: spell = 3. Need potion ≥ ceil(7/3) = 3. Binary search for 3.

1idx 02idx 13idx 24idx 35idx 4lomidhi
potions[2] = 3 3. Found at index 2. Result: 5 - 2 = 3

Binary search finds index 2. Count = 5 - 2 = 3 successful potions.

Result: [4, 0, 3]

spells513result403

Each entry tells you how many potions pair successfully with that spell.

For each spell, the binary search finds the leftmost potion that meets the threshold. Everything from that index to the end of the sorted array is a successful pair. The total work per spell is O(log n), and with m spells that gives O(m log n) for the search phase on top of the O(n log n) sort.

The solution

def successfulPairs(spells, potions, success):
    potions.sort()
    n = len(potions)
    result = []

    for spell in spells:
        min_potion = -(-success // spell)
        lo, hi = 0, n
        while lo < hi:
            mid = (lo + hi) // 2
            if potions[mid] >= min_potion:
                hi = mid
            else:
                lo = mid + 1
        result.append(n - lo)

    return result

Let's break this down piece by piece.

potions.sort() sorts the potions array in ascending order. This is the one-time cost that enables binary search for every spell.

min_potion = -(-success // spell) computes ceil(success / spell) using integer arithmetic. This is the minimum potion value needed for a successful pair with this spell.

lo, hi = 0, n sets up a half-open search range [0, n). Note hi = n, not n - 1, because the answer might be past the end of the array (meaning no potion is large enough).

if potions[mid] >= min_potion: hi = mid keeps the current position as a candidate and searches left for an even earlier valid position. This finds the leftmost valid index.

else: lo = mid + 1 eliminates the left half since those potions are too small.

n - lo is the count of potions from index lo to the end, all of which form successful pairs.

The expression -(-success // spell) computes ceil(success / spell) using pure integer arithmetic. In Python, // performs floor division. Negating before and after the floor division flips it into ceiling division. This avoids floating-point precision issues that come with math.ceil(success / spell), which matters when success can be as large as 10^10.

Complexity analysis

MetricValueReasoning
TimeO((m + n) log n)Sorting potions is O(n log n). Binary search for each of m spells is O(m log n). Total: O((m + n) log n).
SpaceO(m)The result array. Sorting may use O(n) depending on implementation.

The building blocks

Sort then binary search

When you need to count elements satisfying a threshold condition, sort the array and binary search for the boundary. Everything on one side of the boundary satisfies the condition. This reduces per-query time from O(n) to O(log n).

The pattern shows up frequently. In this problem, you sort potions and find the minimum potion that makes a product large enough. In Two Sum II, you work with a sorted array and use two pointers. In Koko Eating Bananas, you binary search over the answer space rather than a data array, but the search mechanics are the same. Any time you see a threshold condition over sorted data, think binary search.

Edge cases

  • Spell value is 0. No product can reach a positive success. Result is 0 for that spell. (Problem constraints may guarantee spell is at least 1.)
  • All potions succeed. When even the smallest potion times the spell is at least success. Binary search returns index 0, count = n.
  • No potions succeed. When even the largest potion times the spell is less than success. Binary search returns index n, count = 0.
  • success is 1. Every non-zero spell-potion pair succeeds, so the answer is n for every spell.
  • Large values. success can be up to 10^10. Use integer ceiling division to avoid float precision issues.

From understanding to recall

This problem combines two fundamental skills: sorting and binary search. The core logic is clean. Sort the potions, compute the minimum potion threshold with ceiling division, and binary search for the leftmost position meeting that threshold.

The tricky part is getting the details right under pressure. Is hi initialized to n or n - 1? Do you use hi = mid or hi = mid - 1? What is the ceiling division formula again? These small decisions are easy to second-guess during an interview. Spaced repetition drills them into muscle memory so you can write the solution without hesitation and spend your time on problems that actually require creative thinking.

Related posts