2

MaxProductOfThree lesson : https://app.codility.com/programmers/lessons/6-sorting/max_product_of_three/

For solving this Codility lesson I've coded this function like below;

import itertools

def solution(A):
    combs=[]

    for pivot_element in A:
        to_comb=A[A.index(pivot_element):]

        for comb in itertools.combinations(to_comb, 3):
           mul_comb= comb[0] * comb[1] * comb[2]
           combs.append(mul_comb) 

    return max(combs)

My results were,


result_of_algorithm={
                    "correctness":100,
                    "performance":0,
                    "overall":44,
                    "time complexity": "O(N^3)"
                    }

How can I increase its performance to O(n) time complexity? Can you please explain?

Berke Şentürk
  • 119
  • 1
  • 12

5 Answers5

4

O(n) version of schwobaseggl's solution, also got 100%:

from heapq import nsmallest, nlargest

def solution(A):
    a, b = nsmallest(2, A)
    z, y, x = nlargest(3, A)
    return max(a*b*z, x*y*z)

Benchmarks with the largest allowed case (100,000 values from -1000 to 1000):

204 ms  206 ms  208 ms  210 ms  212 ms  sort
 76 ms   77 ms   78 ms   79 ms   81 ms  heapq
134 ms  135 ms  135 ms  135 ms  136 ms  oneloop
144 ms  146 ms  147 ms  149 ms  151 ms  twoloops
  3 ms    3 ms    3 ms    3 ms    4 ms  baseline

Benchmark code (Try it online!):

from timeit import repeat
from random import choices
from heapq import nsmallest, nlargest
import sys

def sort(A):
    A.sort()
    p1 = A[0] * A[1] * A[-1]
    p2 = A[-3] * A[-2] * A[-1]
    return max(p1, p2)

def heapq(A):
    a, b = nsmallest(2, A)
    z, y, x = nlargest(3, A)
    return max(a*b*z, x*y*z)

def oneloop(A):
    min1 = sys.maxsize * 2
    min2 = sys.maxsize * 2
    max1 = -sys.maxsize * 2
    max2 = -sys.maxsize * 2
    max3 = -sys.maxsize * 2
    for Ai in A:
        if Ai <= min1:
            min2 = min1
            min1 = Ai
        elif Ai <= min2:
            min2 = Ai
        if Ai >= max1:
            max3 = max2
            max2 = max1
            max1 = Ai
        elif Ai >= max2:
            max3 = max2
            max2 = Ai
        elif Ai >= max3:
            max3 = Ai
    return max([min1 * min2 * max1, max1 * max2 * max3])

def twoloops(A):
    min1 = sys.maxsize * 2
    min2 = sys.maxsize * 2
    max1 = -sys.maxsize * 2
    max2 = -sys.maxsize * 2
    max3 = -sys.maxsize * 2
    for Ai in A:
        if Ai <= min1:
            min2 = min1
            min1 = Ai
        elif Ai <= min2:
            min2 = Ai
    for Ai in A:
        if Ai >= max1:
            max3 = max2
            max2 = max1
            max1 = Ai
        elif Ai >= max2:
            max3 = max2
            max2 = Ai
        elif Ai >= max3:
            max3 = Ai
    return max([min1 * min2 * max1, max1 * max2 * max3])

def baseline(A):
    pass

funcs = sort, heapq, oneloop, twoloops, baseline

for _ in range(3):
    A = choices(range(-1000, 1001), k=100_000)
    for func in funcs:
        times = sorted(repeat(lambda: func(A[:]), number=10))
        print(*('%3d ms ' % (t * 1e3) for t in times), func.__name__)
    print()
no comment
  • 6,381
  • 4
  • 12
  • 30
  • This is a really short and clean solution and in `O(n)` although calling `nsmallest` and `nlargest` is iterating `A` twice. – Leonard Aug 31 '21 at 09:03
  • @Leonard It's a list, it doesn't mind being iterated twice :-) – no comment Aug 31 '21 at 09:04
  • Of course not but it's possible with one iteration. Weird thing is that Codility detects `O(n log(n))` for both mine and your solution which are both in `O(n)`. – Leonard Aug 31 '21 at 09:06
  • I tried quite the same as this, but it was detected as log-linear. According to the docs this should be linear, I agree. – user2390182 Aug 31 '21 at 09:08
  • @Leonard Yeah, but I'm sure most of the work is done by *handling* the values, not by iterating them. Your single iteration is slower than each of two separate iterations would be. It's probably not much faster than the two together would be. – no comment Aug 31 '21 at 09:09
  • @schwobaseggl Hard to distinguish automatically, I guess. It certainly is linear. But is that in the docs? I only know it from the source code. – no comment Aug 31 '21 at 09:10
  • Unfortunately, the `heapq` module is written in pure Python, and for most use cases, the algorithmic advantages of this approach are eaten up by the brute C-speed of just sorting. But this is the most elegant way, no doubt ;-) – user2390182 Aug 31 '21 at 09:15
  • 1
    @schwobaseggl There are also [705 lines of C](https://github.com/python/cpython/blob/main/Modules/_heapqmodule.c). – no comment Aug 31 '21 at 09:17
  • Ah, that's true. I just found https://github.com/python/cpython/blob/main/Lib/heapq.py – user2390182 Aug 31 '21 at 09:21
  • @schwobaseggl Always look for the imports :-). I this case [these](https://github.com/python/cpython/blob/793f55bde9b0299100c12ddb0e6949c6eb4d85e5/Lib/heapq.py#L578-L595). – no comment Aug 31 '21 at 09:28
  • How it is O(N * logN) I didn't figure it out? – Berke Şentürk Aug 31 '21 at 09:50
  • @BerkeŞentürk What do you mean? – no comment Aug 31 '21 at 09:51
  • I'm very amateur in Big O notation so I didn't get why it is hasn't O(N^3) like complexity. Doesn't nsmallest or nlargest contain O(n) complexity? – Berke Şentürk Aug 31 '21 at 10:04
  • @BerkeŞentürk nsmallest and nlargest are O(|A|) here. I have no idea why you're thinking of O(N^3). – no comment Aug 31 '21 at 10:10
2

Naively, you can do this in log-linear time:

def solution(A):
    A.sort()
    p1 = A[0] * A[1] * A[-1]
    p2 = A[-3] * A[-2] * A[-1]
    return max(p1, p2)

The sorting allows you to just try the numbers on both extremes of the sort order. The two options account for the possibility of including negative numbers. It achieves 100% on both correctness and performance.

user2390182
  • 72,016
  • 6
  • 67
  • 89
  • You don't "just try the numbers with the largest absolute values", though. Fortunately, you also try the numbers with the *smallest* absolute values when that's necessary, for example for `[-9, -8, -7, -6, -5, -4, -3, -2, -1]`. – no comment Aug 31 '21 at 08:25
  • True, one should say the ones on both extremes of the sort order. – user2390182 Aug 31 '21 at 08:27
  • This is possible in linear time, you don't need to sort the array - it doesn't achieve 100% in performance. – Leonard Aug 31 '21 at 08:29
  • @Leonard Even mine, which also sorted and then did *more* work, achieved 100%. – no comment Aug 31 '21 at 08:30
  • I misunderstood you. I thought you meant that this is the optimal solution in terms of time complexity which it isn't. – Leonard Aug 31 '21 at 08:32
  • Still not 100% sure it's right, but wrote a heapq version with imho cute variable names. Didn't know the term "log-linear", but [found it](https://en.wikipedia.org/wiki/Time_complexity#Quasilinear_time). Might want to use the k=1 name "linearithmic". – no comment Aug 31 '21 at 08:55
  • @schwobaseggl If `k` is the number of smallest/largest elements to detect and `n` the number of items in the list, they have complexity `O(n log(k))`. Here, `k` is a constant so it's `O(n)`. My solution is definitely `O(n)` (it's just one iteration and a constant number of comparisons) and it still detects `O(n log(n))`. – Leonard Aug 31 '21 at 09:09
  • @Leonard True. Yours is definitely linear, just doesn't look so elegant :-) – user2390182 Aug 31 '21 at 09:11
  • 1
    Added a benchmark to my answer. Also @Leonard – no comment Aug 31 '21 at 09:54
  • And now I **fixed** my answer's benchmark and the results are very different :-) Also @Leonard – no comment Aug 31 '21 at 10:07
1

From this source I've reached a very simple solution: https://youtu.be/qr3i9cXAjbc

The Algorithm is the same algorithm in the answer of @schwobaseggl and simply like below. Works %100 in both performance and correctness.

def solution(A):
    A.sort()
    N=len(A)

    P1 = A[N-1] * A[0] * A[1]
    P2 = A[N-1] * A[N-2] * A[N-3]

    return max(P1,P2)
Berke Şentürk
  • 119
  • 1
  • 12
0

The idea is described here: https://afteracademy.com/blog/maximum-product-of-three-numbers

There are only two possible options:

  1. You need the three biggest numbers (this is obviously the case if all numbers are positive)
  2. You need the biggest number and the two smallest numbers. This can only happen if the two smallest numbers are negative and if their product is larger than the product of the second and third largest numbers. Clearly, the product of the two smallest numbers then is positive and you then need to multiply it with the largest number.

You need to check both cases and return whatever is larger. I adapted the code from the link to Python. It has time complexity O(n) as we iterate through the list only one:

# solution.py
import sys


def solution(A):
    min1 = sys.maxsize * 2
    min2 = sys.maxsize * 2
    max1 = -sys.maxsize * 2
    max2 = -sys.maxsize * 2
    max3 = -sys.maxsize * 2

    for Ai in A:
        if Ai <= min1:
            min2 = min1
            min1 = Ai
        elif Ai <= min2:
            min2 = Ai
        if Ai >= max1:
            max3 = max2
            max2 = max1
            max1 = Ai
        elif Ai >= max2:
            max3 = max2
            max2 = Ai
        elif Ai >= max3:
            max3 = Ai
    return max([min1 * min2 * max1, max1 * max2 * max3])

For example, print(solution([-11, -10, 1, 2, 3, 4, 5, 6, 7, 8, 9])) gives you 990 (-11 * -10 * 9)

Leonard
  • 783
  • 3
  • 22
0

def maxproduct(a):
    i = 0
    max_three = [float('-inf')]
    idx = []
    while i < len(a) - 1:
        temp = max(a[i:i+2])
        if temp > max_three[-1]:
            max_three.append(temp)
        i +=1
        
    return [a.index(e) for e in (max_three[-3:])]
    
maxproduct(a)
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 15 '23 at 14:20