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.
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:
- Sort the potions array once
- For each spell, find the minimum potion strength needed:
success/v
- Use binary search to locate where this threshold value would fit in the sorted potions array
- 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 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: Store the length of potions array
m = len(potions)
We store the total number of potions for later calculations.
Step 3: Process each spell using binary search
return [m - bisect_left(potions, success / v) for v in spells]
For each spell v
in the spells
array:
- Calculate the minimum potion strength needed:
success / v
- Use
bisect_left(potions, success / v)
to find the leftmost insertion point for this value in the sorted potions array - This gives us the index
i
where the first valid potion would be located - The number of valid potions is
m - i
(all potions from indexi
to the end)
Why bisect_left
?
The bisect_left
function performs binary search to find the leftmost position where success / v
could be inserted while maintaining sorted order. This position represents:
- The index of the first potion with strength
≥ success / v
- All potions from this index onwards will form successful pairs with the current spell
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)
returns3
(index where 3.5 would go)- Valid potions are at indices 3 and 4 (values 4 and 5)
- Count of valid potions:
5 - 3 = 2
The list comprehension builds the result array by applying this process to each spell, creating an array where each element represents the count of successful pairs for the corresponding spell.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample 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.
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 to find the leftmost potion that forms a successful pair
16 int left = 0;
17 int right = potionsLength;
18
19 while (left < right) {
20 // Calculate middle index using bit shift (equivalent to division by 2)
21 int mid = (left + right) >> 1;
22
23 // Check if current spell multiplied by middle potion meets success threshold
24 if ((long) spells[i] * potions[mid] >= success) {
25 // If successful, search in left half (including mid)
26 right = mid;
27 } else {
28 // If not successful, search in right half (excluding mid)
29 left = mid + 1;
30 }
31 }
32
33 // Calculate number of successful pairs
34 // All potions from index 'left' to end form successful pairs
35 result[i] = potionsLength - left;
36 }
37
38 return result;
39 }
40}
41
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 // Calculate minimum potion strength needed for this spell
16 // Using ceiling division: (success + spellStrength - 1) / spellStrength
17 // to avoid floating point arithmetic issues
18 long long minPotionStrength = (success + spellStrength - 1) / spellStrength;
19
20 // Find the first potion that meets the minimum strength requirement
21 // using binary search (lower_bound)
22 int firstValidIndex = lower_bound(potions.begin(), potions.end(), minPotionStrength) - potions.begin();
23
24 // Calculate number of successful pairs (all potions from firstValidIndex to end)
25 int successfulPairCount = potionCount - firstValidIndex;
26
27 // Add the count to result vector
28 result.push_back(successfulPairCount);
29 }
30
31 return result;
32 }
33};
34
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 to find the first potion that creates a successful pair
20 let leftIndex: number = 0;
21 let rightIndex: number = potionCount;
22
23 while (leftIndex < rightIndex) {
24 // Calculate middle index using bit shift for efficiency
25 const middleIndex: number = (leftIndex + rightIndex) >> 1;
26
27 // Check if current potion creates a successful pair with the spell
28 if (spellStrength * potions[middleIndex] >= success) {
29 // Move right boundary to search for smaller potions
30 rightIndex = middleIndex;
31 } else {
32 // Move left boundary to search for larger potions
33 leftIndex = middleIndex + 1;
34 }
35 }
36
37 // Count of successful pairs = total potions - index of first successful potion
38 result.push(potionCount - leftIndex);
39 }
40
41 return result;
42}
43
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, wherem
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 takesO(log m)
time per spell - Since we have
n
spells, the total time for all binary searches isO(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
usesO(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. 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).
2. 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
3. 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]
4. 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()
5. 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
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!