Facebook Pixel

2300. Successful Pairs of Spells and Potions

Problem Description

You have two arrays of positive integers: spells and potions. The spells array has length n and represents the strength of each spell. The potions array has length m and represents the strength of each potion.

You need to determine how many potions can form a successful pair with each spell. A spell and potion form a successful pair when their product (spell strength × potion strength) is at least equal to a given integer value called success.

For each spell in the spells array, you need to count how many potions from the potions array can be paired with it successfully. Return an array pairs of length n, where pairs[i] represents the number of potions that form a successful pair with the i-th spell.

For example, if a spell has strength 5 and success is 20, then any potion with strength 4 or greater would form a successful pair (since 5 × 4 = 20, which meets the threshold).

The solution approach sorts the potions array first, then for each spell v, uses binary search to find the smallest potion that satisfies potion × v ≥ success, which is equivalent to finding potion ≥ success/v. The count of valid potions is the total number of potions minus the index where this threshold is met.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that for a given spell with strength v, we need to find all potions with strength p such that v × p ≥ success. This can be rearranged to p ≥ success/v.

Once we recognize this inequality, we realize that if we sort the potions array, all valid potions will form a contiguous segment at the end of the sorted array. Why? Because if a potion with strength p works with a spell, then any potion with strength greater than p will also work.

This observation leads us to binary search. For each spell, we don't need to check every potion individually. Instead, we can:

  1. Sort the potions array once
  2. For each spell, find the minimum potion strength needed: success/v
  3. Use binary search to locate where this threshold value would fit in the sorted potions array
  4. All potions from this position to the end of the array are valid pairs

The beauty of this approach is that sorting costs O(m log m) once, and then each spell only requires O(log m) time for binary search, giving us a total time complexity of O(m log m + n log m). This is much more efficient than the naive approach of checking every spell-potion pair, which would take O(n × m) time.

The bisect_left function finds the leftmost position where we could insert success/v to keep the array sorted. This gives us the index of the first potion that meets our requirement. Subtracting this index from the total length gives us the count of valid potions.

Learn more about Two Pointers, Binary Search and Sorting patterns.

Solution Implementation

1from typing import List
2from bisect import bisect_left
3
4
5class Solution:
6    def successfulPairs(
7        self, spells: List[int], potions: List[int], success: int
8    ) -> List[int]:
9        """
10        Find the number of successful pairs for each spell.
11        A pair (spell, potion) is successful if spell * potion >= success.
12
13        Args:
14            spells: List of spell strengths
15            potions: List of potion strengths
16            success: Minimum product value for a successful pair
17
18        Returns:
19            List where each element represents the count of successful pairs for each spell
20        """
21        # Sort potions array to enable binary search
22        potions.sort()
23
24        # Store the total number of potions
25        num_potions = len(potions)
26
27        # For each spell, find how many potions create a successful pair
28        result = []
29        for spell_strength in spells:
30            # Calculate minimum potion strength needed for this spell
31            # We need: spell * potion >= success, so potion >= success / spell
32            min_potion_strength = success / spell_strength
33
34            # Use binary search to find the leftmost position where
35            # potion strength >= min_potion_strength
36            insert_position = bisect_left(potions, min_potion_strength)
37
38            # All potions from insert_position to end are successful pairs
39            successful_count = num_potions - insert_position
40            result.append(successful_count)
41
42        return result
43
1class Solution {
2    public int[] successfulPairs(int[] spells, int[] potions, long success) {
3        // Sort potions array to enable binary search
4        Arrays.sort(potions);
5
6        // Get array lengths
7        int spellsLength = spells.length;
8        int potionsLength = potions.length;
9
10        // Initialize result array to store count of successful pairs for each spell
11        int[] result = new int[spellsLength];
12
13        // Iterate through each spell
14        for (int i = 0; i < spellsLength; i++) {
15            // Binary search using first_true_index template
16            // Find first potion where spell * potion >= success
17            int left = 0;
18            int right = potionsLength - 1;
19            int firstTrueIndex = potionsLength;  // Default: no valid potion found
20
21            while (left <= right) {
22                int mid = left + (right - left) / 2;
23
24                // Check if current spell multiplied by middle potion meets success threshold
25                if ((long) spells[i] * potions[mid] >= success) {
26                    // Found a valid potion, record and search for earlier one
27                    firstTrueIndex = mid;
28                    right = mid - 1;
29                } else {
30                    // Not successful, search in right half
31                    left = mid + 1;
32                }
33            }
34
35            // Calculate number of successful pairs
36            // All potions from firstTrueIndex to end form successful pairs
37            result[i] = potionsLength - firstTrueIndex;
38        }
39
40        return result;
41    }
42}
43
1class Solution {
2public:
3    vector<int> successfulPairs(vector<int>& spells, vector<int>& potions, long long success) {
4        // Sort potions array in ascending order for binary search
5        sort(potions.begin(), potions.end());
6
7        // Result vector to store count of successful pairs for each spell
8        vector<int> result;
9
10        // Get the total number of potions
11        int potionCount = potions.size();
12
13        // Iterate through each spell
14        for (int& spellStrength : spells) {
15            // Binary search using first_true_index template
16            // Find first potion where spell * potion >= success
17            int left = 0;
18            int right = potionCount - 1;
19            int firstTrueIndex = potionCount;  // Default: no valid potion found
20
21            while (left <= right) {
22                int mid = left + (right - left) / 2;
23
24                // Check if current spell * potion meets success threshold
25                if ((long long) spellStrength * potions[mid] >= success) {
26                    // Found a valid potion, record and search for earlier one
27                    firstTrueIndex = mid;
28                    right = mid - 1;
29                } else {
30                    // Not successful, search in right half
31                    left = mid + 1;
32                }
33            }
34
35            // Calculate number of successful pairs
36            // All potions from firstTrueIndex to end form successful pairs
37            result.push_back(potionCount - firstTrueIndex);
38        }
39
40        return result;
41    }
42};
43
1/**
2 * Counts the number of successful pairs for each spell with potions
3 * A pair is successful if spell * potion >= success threshold
4 *
5 * @param spells - Array of spell strengths
6 * @param potions - Array of potion strengths
7 * @param success - Minimum product value for a successful pair
8 * @returns Array where each element represents the count of successful pairs for each spell
9 */
10function successfulPairs(spells: number[], potions: number[], success: number): number[] {
11    // Sort potions in ascending order for binary search
12    potions.sort((a: number, b: number) => a - b);
13
14    const potionCount: number = potions.length;
15    const result: number[] = [];
16
17    // Process each spell
18    for (const spellStrength of spells) {
19        // Binary search using first_true_index template
20        // Find first potion where spell * potion >= success
21        let left: number = 0;
22        let right: number = potionCount - 1;
23        let firstTrueIndex: number = potionCount;  // Default: no valid potion found
24
25        while (left <= right) {
26            const mid: number = Math.floor((left + right) / 2);
27
28            // Check if current potion creates a successful pair with the spell
29            if (spellStrength * potions[mid] >= success) {
30                // Found a valid potion, record and search for earlier one
31                firstTrueIndex = mid;
32                right = mid - 1;
33            } else {
34                // Not successful, search in right half
35                left = mid + 1;
36            }
37        }
38
39        // Count of successful pairs = total potions - firstTrueIndex
40        result.push(potionCount - firstTrueIndex);
41    }
42
43    return result;
44}
45

Solution Approach

The implementation follows a sorting and binary search strategy.

Step 1: Sort the potions array

potions.sort()

We sort the potions array in ascending order. This preprocessing step enables us to use binary search efficiently.

Step 2: Binary Search Using first_true_index Template

For each spell with strength v, we need to find the first potion that forms a successful pair (where potion >= success / v).

Using the first_true_index template:

def bisect_left(nums, target):
    # Find first index where nums[idx] >= target
    left, right = 0, len(nums) - 1
    first_true_index = len(nums)  # Default: no element >= target

    while left <= right:
        mid = (left + right) // 2
        if nums[mid] >= target:
            first_true_index = mid
            right = mid - 1
        else:
            left = mid + 1

    return first_true_index

Counting Logic:

  • first_true_index = bisect_left(potions, success / v) finds the first potion >= threshold
  • Number of valid potions = len(potions) - first_true_index

Example walkthrough: If potions = [1, 2, 3, 4, 5], success = 7, and current spell strength is 2:

  • Minimum potion needed: 7 / 2 = 3.5
  • bisect_left([1, 2, 3, 4, 5], 3.5) returns 3 (first index where value >= 3.5)
  • Valid potions are at indices 3 and 4 (values 4 and 5)
  • Count of valid potions: 5 - 3 = 2

Time Complexity: O((m + n) × log m) - sort potions + binary search per spell.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand the solution approach:

Given:

  • spells = [3, 1, 2]
  • potions = [8, 5, 8]
  • success = 16

Step 1: Sort the potions array

  • potions = [5, 8, 8] (sorted in ascending order)
  • m = 3 (length of potions)

Step 2: Process each spell using binary search

For spell[0] = 3:

  • We need: 3 × potion ≥ 16
  • Minimum potion strength needed: 16 / 3 = 5.33...
  • Use bisect_left([5, 8, 8], 5.33) to find where 5.33 would be inserted
  • Binary search finds index 1 (5.33 would go after the 5 at index 0)
  • Valid potions are at indices 1 and 2 (values 8 and 8)
  • Count: 3 - 1 = 2 successful pairs

For spell[1] = 1:

  • We need: 1 × potion ≥ 16
  • Minimum potion strength needed: 16 / 1 = 16
  • Use bisect_left([5, 8, 8], 16) to find where 16 would be inserted
  • Binary search finds index 3 (16 would go at the end)
  • No potions meet this threshold
  • Count: 3 - 3 = 0 successful pairs

For spell[2] = 2:

  • We need: 2 × potion ≥ 16
  • Minimum potion strength needed: 16 / 2 = 8
  • Use bisect_left([5, 8, 8], 8) to find where 8 would be inserted
  • Binary search finds index 1 (the leftmost position of value 8)
  • Valid potions are at indices 1 and 2 (values 8 and 8)
  • Count: 3 - 1 = 2 successful pairs

Final Result: [2, 0, 2]

This example demonstrates how sorting the potions array once allows us to efficiently find the count of valid pairs for each spell using binary search, avoiding the need to check every spell-potion combination individually.

Time and Space Complexity

Time Complexity: O(m log m + n log m) which simplifies to O((m + n) × log m)

  • Sorting the potions array takes O(m log m) time, where m is the length of the potions array
  • For each spell in the spells array, we perform a binary search using bisect_left on the sorted potions array, which takes O(log m) time per spell
  • Since we have n spells, the total time for all binary searches is O(n log m)
  • Overall time complexity: O(m log m + n log m) = O((m + n) × log m)

Space Complexity: O(log m)

  • The sorting operation on the potions array uses O(log m) space for the recursion stack in Python's Timsort algorithm
  • The list comprehension creates a new list of size n, but this is the output and typically not counted in space complexity analysis
  • The binary search operation bisect_left uses O(1) additional space
  • Overall auxiliary space complexity: O(log m)

Note: The reference answer states O(log n) for space complexity, but based on the code analysis, it should be O(log m) since the sorting is performed on the potions array of length m, not the spells array of length n.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using Wrong Binary Search Template

The problem uses bisect_left which finds the first index where element >= target.

Wrong approach:

while left < right:
    mid = (left + right) >> 1
    if spell * potions[mid] >= success:
        right = mid
    else:
        left = mid + 1
return left  # Returns left directly

Correct approach (first_true_index template):

first_true_index = len(potions)  # Default
while left <= right:
    mid = (left + right) // 2
    if spell * potions[mid] >= success:
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
return first_true_index

2. Integer Division vs Float Division

The Pitfall: Using integer division (//) instead of float division (/) when calculating the minimum potion strength.

# WRONG - This will truncate the result
min_potion_strength = success // spell_strength

# CORRECT - Preserves decimal values
min_potion_strength = success / spell_strength

Why it matters: If success = 7 and spell_strength = 2, we need potions with strength ≥ 3.5. Using integer division would give us 3, incorrectly including potions with strength 3 (since 2 × 3 = 6 < 7).

3. Using bisect_right Instead of bisect_left

The Pitfall: Confusing bisect_right with bisect_left, which would give incorrect counts when exact matches exist.

# WRONG - May miss valid potions when exact threshold exists
insert_position = bisect_right(potions, min_potion_strength)

# CORRECT - Finds the leftmost valid position
insert_position = bisect_left(potions, min_potion_strength)

Example: If potions = [1, 2, 4, 4, 5] and we need min_potion_strength = 4:

  • bisect_left returns index 2 (first occurrence of 4)
  • bisect_right returns index 4 (after last occurrence of 4)
  • Using bisect_right would incorrectly exclude the two potions with strength 4

4. Forgetting to Sort the Potions Array

The Pitfall: Applying binary search on an unsorted array, leading to incorrect results.

# WRONG - Binary search requires sorted array
result = [m - bisect_left(potions, success / v) for v in spells]

# CORRECT - Sort first, then apply binary search
potions.sort()
result = [m - bisect_left(potions, success / v) for v in spells]

5. Modifying Original Potions Array

The Pitfall: Sorting the original potions array in-place might be problematic if the original order needs to be preserved elsewhere.

Solution: Create a copy if needed:

sorted_potions = sorted(potions)  # Creates a new sorted list
# OR
sorted_potions = potions.copy()
sorted_potions.sort()

6. Edge Case: Very Small Spell Strength

The Pitfall: When spell strength approaches zero, success / spell_strength can become very large or cause floating-point precision issues.

Solution: While the problem states all values are positive integers, it's good practice to handle edge cases:

for spell_strength in spells:
    if spell_strength == 0:  # Though problem guarantees positive integers
        result.append(0)
        continue
    min_potion_strength = success / spell_strength
    # ... rest of the logic
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More