Bender, Algorithmic Complexity


The aim of this puzzle is to match a scatterplot with one of a few complexity functions. As these functions have a \(x \mapsto x^{\alpha} \beta^x \log{x} \) form, one of the most complete solution would be to use the method of least squares. But for this puzzle, you can settle for a lighter method, for example computing the scatterplots corresponding to the complexity functions, and find a formula to assess their likelihood to the original scatterplot.
Note that the \(\Theta (n^3)\) validation test can be tricky to pass with this method, as the data set is a \(\Theta(n^{2.8})\) which is closer to a \(\Theta (n^2 \log{n})\) on the interval studied. A hack around this is to add the \(\Theta(n^{2.5})\) complexity function, and output \(\Theta (n^3)\) when it is recognized.

Python

import math

# name, and function of the main complexities
names = ["1","log n","n","n log n", "n^2", "n^2 log n", "n^3", "2^n"]
complexities = [lambda x: 1, 
                lambda x: math.log(x), 
                lambda x: x, 
                lambda x: x*math.log(x), 
                lambda x: x**2, 
                lambda x: x**2*math.log(x),
                lambda x: x**3, 
                lambda x: math.pow(2,x)]
# maximum value which can be fed to the corresponding complexity function
# without overflow
upperLimit = [None, None, None, None, None, None, None, 500]

# Read input, storing data volumes in x, and execution times in perf
x, perf = [], []
for i in range(int(input())):
    a, b = [int(j) for j in input().split()]
    x.append(a)
    perf.append(b)

# for each complexity function f, computes a normalized variance of the ratio (perf/f)
# It it is not representative (variance over less than 5 values) stores -1 instead
# Then updates minVariance and result so that minVariance stays the minimal 
# representative variance found yet, and result is the number of the corresponding 
# function, which is the most probable complexity function found yet
variances, minVariance, result = [], sys.maxsize, -1

for i in range(len(complexities)):
    f = complexities[i]
    # takes upperLimit into account to avoid an overflow
    maxDataVolume = len(x)
    if upperLimit[i] != None:
        for j in range(len(x)):
            if x[j] > upperLimit[i]:
                maxDataVolume = j-1
                break
            
    ratio = [ perf[j]/f(x[j]) for j in range(maxDataVolume)]
    if (len(ratio) < 5):
        variances.append(-1)
    else:
        mean = sum(ratio) / len(ratio)
        variances.append(sum([(val-mean)**2 for val in ratio]) / mean**2)
        if (variances[-1] < minVariance):
            minVariance = variances[-1]
            result = i

print("O({})".format(names[result]))