Leetcode.com problem 2300: "Successful Pairs of Spells and Potions"

James S. Plank

Tue Apr 4 15:49:01 EDT 2023

In Case Leetcode's Servers are Down

You are given three inputs to the method successfulPairs. A combination of a spell and a potion is "successful" if their product is greater than or equal to success. Your method should return a vector rv of ints, where rv[i] is the number of potions in potions that are successful with spell[i].

Examples

The driver program in Skeleton.cpp works in one of two ways. The first is to have spells on the first line of standard input, potions on the second line, and success on the third line. Here's the first example:

UNIX> cat input-1.txt
5 1 3
1 2 3 4 5
7
UNIX> ./a.out < input-1.txt
4 0 3 
UNIX> 

To explain, spell[0] is 5. When it is combined with potion[0] (which is 1), its product is too small. So it is unsuccessful. However, its product with all of the other potions is greater than 7, so there are 4 successful potions. There are no successful potions with spell[1], and three successful potions with spell[2] (those potions are 3, 4 and 5). Hence the return value of [ 4 0 3 ].

Here is the other Leetcode example, plus two of mine:

UNIX> cat input-2.txt
3 1 2
8 5 8
16
UNIX> ./a.out < input-2.txt
2 0 2 
UNIX> cat input-3.txt
4 24 10 19 22
25 14 20 15 7 14 10 6 17 5 13 5 7 27 11 10 13 1 25 16
80
UNIX> ./a.out < input-3.txt
4 19 14 19 19 
UNIX> cat input-4.txt
6 24 13 17 7 27 30 29 42 39 27 29 4 9 25 39 6 14 38 47
16 29 21 42 5 48 14 14 35 16 15 15 43 2 10 47 18 17 34 1
1000
UNIX> ./a.out < input-4.txt
0 4 0 0 0 4 6 5 7 7 4 5 0 0 4 7 0 0 7 7 
UNIX> 

Now, if you enter a single negative number to the program, it will generate a random, typically large example within the constraints. It runs the method and then prints the last element of rv. For example:

UNIX> echo -1 | ./a.out
rv[4163] = 12183
UNIX> echo -2 | ./a.out
rv[91243] = 4321
UNIX> 


The testing script

The testing script runs the five examples in input-1.txt, input-2.txt, input-3.txt, input-4.txt, and input-5.txt. Then it runs 50 random examples with seeds between -1 and -50.

Your program's output should match answers.txt, and it should run quickly. Let's say under 4 seconds total.


The straightforward solution is too slow

The easiest solution is to use a nested for loop:
For every spell i,
  For every potion j,
    Test to see if i*j ≥ success.
Unfortunately, the running time of that is O(n2), where n is the size of spells and potions (just assume they are roughly the same size). That means the running time can be roughly 1010, which is way too slow. So we have to try something else.

A better approach

Suppose we sort the potions. Let's use input-3.txt as an example. Below, we sort the potions and show the index of each potion in the sorted vector:

 Index   Value
     0       1
     1       5
     2       5
     3       6
     4       7
     5       7
     6       10
     7       10
     8       11
     9       13
    10       13
    11       14
    12       14
    13       15
    14       16
    15       17
    16       20
    17       25
    18       25
    19       27

To remind you, the spells are

4 24 10 19 22

And success is 80. So consider the first spell: 4. By division, we know that successful potions will have values ≥ 20. We can find the value 20 in the sorted array, and see that there are four values ≥ 20. So rv[0] is 4.

Similarly, the second spell is 24. Successful potions will have values ≥ 4. The smallest value ≥ 4 in the sorted array is 5, with an index of 1. Therefore, there are 19 successful potions, and rv[1] is 19.

And so on.


So how to implement this? #1 - Use a map.

My first solution was to use a map. Its keys were potion values, and its vals were the number of potions whose values are ≥ that value. So, for input-3.txt, the map is:

   Key     Val
     1      20
     5      19
     6      17
     7      16
    10      14
    11      12
    13      11
    14       9
    15       7
    16       6
    17       5
    20       4
    25       3
    27       1

Make sure you see how you can make that from the sorted vector above (you can make it more simply, but it may be easier to see how it is derived from the vector).

Now, when you consider a spell, you calculate the smallest successful potion value, and use the lower bound method of map to find the smallest value in the map greater than or equal to that value. That determination is done in O(log n) time, so you have:

Here it is on tests.sh. It was fast enough to pass leetcode's tests, but it was one of the slowest legal solutions:

UNIX> time sh tests.sh > /dev/null

real	0m3.111s
user	0m2.958s
sys	0m0.129s
UNIX> 


#2 - Use binary search

Instead of using a map, you can simply use the sorted potions. You have to write a binary search to find the smallest potion whose value is big enough. Let's go back to the spell of 4 in input-3.txt. Here are the sorted potions again:

 Index   Value
     0       1
     1       5
     2       5
     3       6
     4       7
     5       7
     6       10
     7       10
     8       11
     9       13
    10       13
    11       14
    12       14
    13       15
    14       16
    15       17
    16       20
    17       25
    18       25
    19       27

You want to find the smallest potion whose value is ≥ 20. I'm not fond of my binary search, but I never am. I kept track of two values:

My binary search went as follows:
b   sz   middle-element   action
0   20   Index 10: 13     Too small, set b to 11 and size to 9.
11   9   Index 15: 17     Too small, set b to 16 and size to 4
16   4   Index 18: 25     >= 0 20, keep b at 16 and set size to 2
16   2   Index 17: 25     >= 0 20, keep b at 16 and set size to 1
16   1   Index 16: 20     >= 0 20, keep b at 16 and we're done
Since there are 4 elements >= potions[16], the answer is 4.

Now, when you're done with your binary search, you need to consider that b may be the smallest index with a value ≥ 20, or the largest index with a value < 20. So you need to do a little work when your binary search is done to make sure you know what b is. This is one reason why I don't like writing binary searches...

Now, let's evaluate the running time:

So again, the total is O(n log n). However, you'll see that this solution is a lot faster, because maps use a lot of extra memory. Put another way, sorting is a lot faster than creating that map, even though they are both O(n log n).

UNIX> time sh tests.sh > /dev/null

real	0m1.085s
user	0m0.972s
sys	0m0.100s
UNIX> 


#3 - Sort spells and potions

My last solution doesn't do a binary search, but instead sorts both spells and potions. When it sorts spells, it keeps track of the original index of each vector. Here are the two sorted vectors:

    Spells
Value   Original-Index
  4          0
 10          2
 19          3
 22          4
 24          1
    Potions:
 Index   Value
     0       1
     1       5
     2       5
     3       6
     4       7
     5       7
     6       10
     7       10
     8       11
     9       13
    10       13
    11       14
    12       14
    13       15
    14       16
    15       17
    16       20
    17       25
    18       25
    19       27

Now, we run through both vectors at the same time. We'll traverse spells from low to high and potions from high to low. Here's our starting points:

         Spells
     Value   Original-Index
i -->  4          0
      10          2
      19          3
      22          4
      24          1
         Potions:
      Index   Value
          0       1
          1       5
          2       5
          3       6
          4       7
          5       7
          6       10
          7       10
          8       11
          9       13
         10       13
         11       14
         12       14
         13       15
         14       16
         15       17
         16       20
         17       25
         18       25
j -->    19       27

Now, we consider the value at index i, which is 4. We calculate that we need potions ≥ 20, so we decrement j until we have a value less than 20 (or we've run out of potions):

         Spells
     Value   Original-Index
i -->  4          0
      10          2
      19          3
      22          4
      24          1
         Potions:
      Index   Value
          0       1
          1       5
          2       5
          3       6
          4       7
          5       7
          6       10
          7       10
          8       11
          9       13
         10       13
         11       14
         12       14
         13       15
         14       16
j -->    15       17
         16       20
         17       25
         18       25
         19       27

We know there are 4 values greater than potions[j], and the spell at index i was originally spells[0], so rv[0] = 4.

Now, we increment i:

         Spells
     Value   Original-Index
       4          0
i --> 10          2
      19          3
      22          4
      24          1
         Potions:
      Index   Value
          0       1
          1       5
          2       5
          3       6
          4       7
          5       7
          6       10
          7       10
          8       11
          9       13
         10       13
         11       14
         12       14
         13       15
         14       16
j -->    15       17
         16       20
         17       25
         18       25
         19       27

We calculate that we need potions with values ≥ 10, so we decrement j until we get to a potion whose value is less than 10:

         Spells
     Value   Original-Index
       4          0
i --> 10          2
      19          3
      22          4
      24          1
         Potions:
      Index   Value
          0       1
          1       5
          2       5
          3       6
          4       7
j -->     5       7
          6       10
          7       10
          8       11
          9       13
         10       13
         11       14
         12       14
         13       15
         14       16
         15       17
         16       20
         17       25
         18       25
         19       27

There are 14 potions with values greater than potions[j], and the original index of spell i is 2, so rv[2] = 14.

We increment i again:

         Spells
     Value   Original-Index
       4          0
      10          2
i --> 19          3
      22          4
      24          1
         Potions:
      Index   Value
          0       1
          1       5
          2       5
          3       6
          4       7
j -->     5       7
          6       10
          7       10
          8       11
          9       13
         10       13
         11       14
         12       14
         13       15
         14       16
         15       17
         16       20
         17       25
         18       25
         19       27

We calculate that we need potions with values ≥ 5. So we decrement j until it is pointing to a value < 5:

         Spells
     Value   Original-Index
       4          0
      10          2
i --> 19          3
      22          4
      24          1
         Potions:
      Index   Value
j -->     0       1
          1       5
          2       5
          3       6
          4       7
          5       7
          6       10
          7       10
          8       11
          9       13
         10       13
         11       14
         12       14
         13       15
         14       16
         15       17
         16       20
         17       25
         18       25
         19       27

So now we set rv[3] = 19. See how that works? Since we only traverse spells and potions once, we have:

The overall time is still O(n log n), just like the others, but I suspect that this one will be the fastest.

Unfortunately, there is a subtlety here in that we have to sort spells, but we need to keep track of each value's original index. We could us a map for this, but then we're going to be as slow as solution 1, so that won't do. What I did was transform each value in spells to a long long. Call it x:

x = (value << 20) | index;

When I sort the x's, they'll be in the same order as sorting the spells. However, I can extract the values from each x with (x > > 20), and I can extract the indices with (x & 0xfffff). This is because we know the values and indices are capped at 100,000, which is less than 220. Let me use the spells in input-3.txt as an example:

     Value   Original-Index    x        x-in-hex     x>>20       x&0xfffff
       4          0         4194304     0x400000         4               0
      10          2        10485762     0xa00002        10               2
      19          3        19922947    0x1300003        19               3
      22          4        23068676    0x1600004        22               4
      24          1        25165825    0x1800001        24               1

As you can see, the values are sorted by the spell value, but I can extract both the spell value and the original index from the x value.

It's not a bad exercise to program this one up -- when I did it, it was the fastest of the three. It was faster than 86% of the leetcoders:

UNIX> time sh tests.sh > /dev/null

real	0m1.049s
user	0m0.940s
sys	0m0.091s
UNIX>