Successful Pairs of Spells and Potions

MEDIUM

Description

You are given two positive integer arrays spells and potions, of length n and m respectively, where spells[i] represents the strength of the ith spell and potions[j] represents the strength of the jth potion.

You are also given an integer success. A spell and potion pair is considered successful if the product of their strengths is at least success.

Return an integer array pairs of length n where pairs[i] is the number of potions that will form a successful pair with the ith spell.

 

Example 1:

Input: spells = [5,1,3], potions = [1,2,3,4,5], success = 7
Output: [4,0,3]
Explanation:
- 0th spell: 5 * [1,2,3,4,5] = [5,10,15,20,25]. 4 pairs are successful.
- 1st spell: 1 * [1,2,3,4,5] = [1,2,3,4,5]. 0 pairs are successful.
- 2nd spell: 3 * [1,2,3,4,5] = [3,6,9,12,15]. 3 pairs are successful.
Thus, [4,0,3] is returned.

Example 2:

Input: spells = [3,1,2], potions = [8,5,8], success = 16
Output: [2,0,2]
Explanation:
- 0th spell: 3 * [8,5,8] = [24,15,24]. 2 pairs are successful.
- 1st spell: 1 * [8,5,8] = [8,5,8]. 0 pairs are successful. 
- 2nd spell: 2 * [8,5,8] = [16,10,16]. 2 pairs are successful. 
Thus, [2,0,2] is returned.

 

Constraints:

  • n == spells.length
  • m == potions.length
  • 1 <= n, m <= 105
  • 1 <= spells[i], potions[i] <= 105
  • 1 <= success <= 1010

Approaches

Checkout 3 different approaches to solve Successful Pairs of Spells and Potions. Click on different approaches to view the approach and algorithm in detail.

Sorting Both Arrays with Two Pointers

This is a highly optimized approach that involves sorting both the spells and potions arrays and then using a two-pointer technique to find the counts efficiently. By processing spells in increasing order of strength, we can avoid re-scanning the potions array for each spell.

Algorithm

    1. Create a 2D array sortedSpells of size n x 2 to store (spell, original_index).
    1. Sort sortedSpells based on spell strength.
    1. Sort the potions array.
    1. Initialize potionIndex = m - 1 and the result array pairs.
    1. Iterate i from 0 to n-1 through sortedSpells.
    1. Let spell = sortedSpells[i][0] and originalIndex = sortedSpells[i][1].
    1. While potionIndex >= 0 and (long) spell * potions[potionIndex] >= success:
    1.    Decrement `potionIndex`.
      
    1. The count of successful potions is m - 1 - potionIndex.
    1. Set pairs[originalIndex] to this count.
    1. Return pairs.

The main challenge with sorting spells is that the final result must correspond to the original order of spells. To handle this, we create a 2D array to store each spell's value along with its original index. First, we populate this new structure with (spells[i], i). We then sort this array based on the spell strength and also sort the potions array. We initialize two pointers: one for the sorted spells (say i, from 0 to n-1) and one for the potions array (say j, from m-1 down to 0). We iterate through the sorted spells. The key insight is that as we move to a stronger spell, the required potion strength decreases. Therefore, the j pointer only needs to move from right to left across the potions array and never needs to be reset. For each spell, we move the j pointer to the left (j--) as long as the product is successful. After the inner loop, j points to the strongest potion that is not successful. The number of successful potions is m - 1 - j. We store this count in the result array at the spell's original index, which we saved earlier. Because the j pointer traverses the potions array at most once, the scanning part is very efficient.

import java.util.Arrays;
import java.util.Comparator;

class Solution {
    public int[] successfulPairs(int[] spells, int[] potions, long success) {
        int n = spells.length;
        int m = potions.length;
        
        int[][] sortedSpells = new int[n][2];
        for (int i = 0; i < n; i++) {
            sortedSpells[i][0] = spells[i];
            sortedSpells[i][1] = i;
        }
        
        Arrays.sort(sortedSpells, Comparator.comparingInt(a -> a[0]));
        Arrays.sort(potions);
        
        int[] pairs = new int[n];
        int potionIndex = m - 1;
        
        for (int i = 0; i < n; i++) {
            int spell = sortedSpells[i][0];
            int originalIndex = sortedSpells[i][1];
            
            while (potionIndex >= 0 && (long) spell * potions[potionIndex] >= success) {
                potionIndex--;
            }
            
            pairs[originalIndex] = m - 1 - potionIndex;
        }
        
        return pairs;
    }
}

Complexity Analysis

Time Complexity: O(n log n + m log m). O(n log n) for sorting spells, O(m log m) for sorting potions. The two-pointer scan takes O(n + m). The dominant part is the sorting.Space Complexity: O(n) to store the `(spell, index)` pairs and the result array.

Pros and Cons

Pros:
  • Generally the most efficient approach, especially when n and m are of similar magnitude.
  • The two-pointer scan is very fast in practice due to linear memory access and fewer complex operations per step compared to binary search.
Cons:
  • Requires extra space to store spell-index pairs.
  • The logic is the most complex of the three approaches.

Code Solutions

Checking out 3 solutions in different languages for Successful Pairs of Spells and Potions. Click on different languages to view the code.


Video Solution

Watch the video walkthrough for Successful Pairs of Spells and Potions



Algorithms:

Binary SearchSorting

Patterns:

Two Pointers

Data Structures:

Array

Companies:

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.