7

I was wondering, given a list of integers, say l, and if we are allowed to select 3 integers from this list, say left, middle, right, where middle > left, right and left, middle, right appear in that order in the list (ie. index(left)<index(middle)<index(right)), does there exist an O(n) solution for finding the maximum of middle - left + middle - right? You may suppose that lists that do not satisfy these conditions do not appear (eg. [5, 0, 5] as pointed out by Eric Duminil)

Currently, I am able to come up with what I believe is (roughly) an O(n^2) solution (correct me if I am wrong).

Essentially, my current idea is to do:

maximum = 0
for idx in range(1, N - 1):
    left = min(l[0: idx])
    right = min(l[idx + 1:])
    middle = l[idx]

    if left < middle and right < middle:
        new_max = middle - left + middle - right
        maximum = max(new_max, maximum)

Help/hints would be greatly appreciated.

Thanks!

Russell Ng
  • 163
  • 8

6 Answers6

5

You can run through your numbers once, keeping a running minimum value, and storing it at each step, so that at the end you know what the minimum value is to the left of each index. That's O(n).

Similarly, you can run through all your numbers once from right to left, and work out what the minimum value is to the right of each index. That is O(n).

Then you can run through each possible middle value, and take the left and right values from your earlier computations. That is O(n).

O(n) + O(n) + O(n) = O(n).

khelwood
  • 55,782
  • 14
  • 81
  • 108
3

The trick is that minimum value of the list is always the part of solution (either left, or right).

  1. Find minimum of the list, which is O(n). Now this minimum element will be either left or right.
  2. Find maximum of (2x-y), where idx(x) > idx(y), and idx(x) < idx(min), that is check the left part of the list
  3. Find max(2x-y), where idx(x) < idx(y), and idx(x) > idx(min), that is check the right part of the list
  4. Now take maximum of the steps 2 and 3, which is your left/middle (or right/middle).
Matt
  • 13,674
  • 1
  • 18
  • 27
  • oh I see...didn't realise that the global minimum of the list has to be in the solution (facepalm), thanks for pointing it out! – Russell Ng Aug 30 '17 at 13:45
  • @EricDuminil however the middle element of (left, middle, right) is always greater than the other two (ie. 5 0 5 is not a valid sequence, lemme just edit that in the original question to make it clearer) – Russell Ng Aug 30 '17 at 13:54
  • hi I just thought of this, but what if there are multiple values of the minimum? Eg. [10, 20, 10, 30, 10] – Russell Ng Aug 30 '17 at 15:48
  • @RussellNg Well, then there are several solutions. Here it's {10(1), 30, 10(3)}; and {10(2), 30, 10(3)}. You can start with an arbitrary minimum value and still find one of the solutions. – Matt Aug 30 '17 at 16:02
  • Taking the maximum x-y doesn't necessarily produce an optimal solution. For example, with an input of `[1, 1000, 0, 1000000, 999999]`, the optimal solution is `0, 1000000, 999999`, but taking the maximum x-y would produce `1, 1000, 0`. – user2357112 Aug 30 '17 at 18:15
  • @user2357112 (1, 1000, 0) is the optimum computed in the step 2; while (0, 1000000, 999999) is the optimum in the step 3. Now, we take the best of two in the step 4, so the algorithm is perfectly OK. – Matt Aug 31 '17 at 08:59
  • @Matt: You describe the algorithm as taking the maximum x-y, not the maximum of the value we're trying to optimize. Even if you take the maximum of the value we're trying to optimize in step 4, `[999999, 1000000, 1, 1000, 0]` finds `1, 1000` in step 2 and nothing in step 3, so step 4 takes `1, 1000, 0` instead of `999999, 1000000, 0`. – user2357112 Aug 31 '17 at 14:27
  • @user2357112 1) max (x-y) gives the same pair x,y, as max (2x-y-global_min), this should be obvious; 2) it does, because we check indexes which are greater than index of zero element (min); try to re-read both conditions carefully. – Matt Aug 31 '17 at 15:42
  • @Matt: No it doesn't. max(x-y) gives less weight to x than max(2x-y-global_min) does. – user2357112 Aug 31 '17 at 16:05
  • Also, you haven't described how to take the max x-y in linear time. – user2357112 Aug 31 '17 at 16:05
  • @user2357112 1) It gives **the same elements** x,y,z; if you need the value of max(2x-y-z) you have to compute it afterwards; btw. calculating a shorter expression in the loop is always a good idea; 2) This is trivial, isn't it? – Matt Aug 31 '17 at 17:04
  • The maximum x-y value in the `[999999, 1000000, 1, 1000, 0]` example is 999, from taking x=1000 and y=1, but the maximum 2x-y-global_min value comes from taking x=1000000 and y=999999, even though that gives x-y=1. – user2357112 Aug 31 '17 at 17:40
  • @user2357112 Nah, I was too careless and, probably, sleepy. OK, max(2x-y) in both cases. – Matt Aug 31 '17 at 17:50
3

Here's a way to calculate the minimums, left and right of every index, in O(n):

import random

N = 10
l = [random.randrange(N) for _ in range(N)]

print(l)
# => [9, 9, 3, 4, 6, 7, 0, 0, 7, 6]

min_lefts = []
min_left = float("inf")
min_rights = [None for _ in range(N)]
min_right = float("inf")

for i in range(N):
    e = l[i]
    if e < min_left:
        min_left = e
    min_lefts.append(min_left)

print(min_lefts)
# => [9, 9, 3, 3, 3, 3, 0, 0, 0, 0]

for i in range(N-1,-1,-1):
    e = l[i]
    if e < min_right:
        min_right = e
    min_rights[i] = min_right

print(min_rights)
# => [0, 0, 0, 0, 0, 0, 0, 0, 6, 6]

You can now iterate over every middle element in l (idx between 1 and N-2), and find the minimum of 2 * l[idx] - min_rights[idx] - min_lefts[idx]. This operation is also O(n):

print(max(2 * l[i] - min_rights[i] - min_lefts[i] for i in range(1, N-2)))

It outputs :

11

which is 2 * 7 - 0 - 3.

Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
1

Here are some timings! Feel free to edit the code that does the timing and\add new entries.

from timeit import timeit


setup10 = '''
import numpy.random as nprnd
lst = list(nprnd.randint(1000, size=10))
'''

setup100 = '''
import numpy.random as nprnd
lst = list(nprnd.randint(1000, size=100))
'''

setup1000 = '''
import numpy.random as nprnd
lst = list(nprnd.randint(1000, size=1000))
'''

fsetup = '''

import sys

def f2(lst):
    N = len(lst)
    maximum = 0
    for idx in range(1, N - 1):
        left = min(lst[0: idx])
        right = min(lst[idx + 1:])
        middle = lst[idx]

        if left < middle and right < middle:
            new_max = middle - left + middle - right
            maximum = max(new_max, maximum)
    return maximum


def eric(lst):
    N = len(lst)
    min_lefts = []
    min_left = float("inf")
    min_rights = [None for _ in range(N)]
    min_right = float("inf")

    for i in range(N):
        e = lst[i]
        if e < min_left:
            min_left = e
        min_lefts.append(min_left)

    for i in range(N-1,-1,-1):
        e = lst[i]
        if e < min_right:
            min_right = e
        min_rights[i] = min_right

    return max(2 * lst[i] - min_rights[i] - min_lefts[i] for i in range(1, N-2))


def bpl(lst):
    res = -sys.maxsize
    a = sys.maxsize
    b = -sys.maxsize
    c = sys.maxsize

    for i, v in enumerate(lst[1:-1]):
        a = min(lst[i], a)
        c = min(lst[i + 2], c)
        b = max(lst[i], b)
        res = max(2 * b - a - c, res)
    return res


def meow(l):
    N = len(l)
    right_min = (N - 2) * [sys.maxsize]
    right_min[0] = l[N - 1]
    for i in range(3, N):
       right_min[i - 2] = min(right_min[i - 2], l[N - i + 1])
    left = l[2]
    maximum = 2*l[1] - left - right_min[N - 3]

    for idx in range(2, N - 1):
        left = min(left, l[idx-1])
        right = right_min[N - idx - 2]
        middle = l[idx]

        if left < middle and right < middle:
            new_max = middle - left + middle - right
            maximum = max(new_max, maximum)
    return maximum

'''


print('OP with 10\t:{}'.format(timeit(stmt="f2(lst)", setup=setup10 + fsetup, number=100)))
print('eric with 10\t:{}'.format(timeit(stmt="eric(lst)", setup=setup10 + fsetup, number=100)))
print('bpl with 10\t:{}'.format(timeit(stmt="bpl(lst)", setup=setup10 + fsetup, number=100)))
print('meow with 10\t:{}'.format(timeit(stmt="meow(lst)", setup=setup10 + fsetup, number=100)))
print()
print('OP with 100\t:{}'.format(timeit(stmt="f2(lst)", setup=setup100 + fsetup, number=100)))
print('eric with 100\t:{}'.format(timeit(stmt="eric(lst)", setup=setup100 + fsetup, number=100)))
print('bpl with 100\t:{}'.format(timeit(stmt="bpl(lst)", setup=setup100 + fsetup, number=100)))
print('meow with 10\t:{}'.format(timeit(stmt="meow(lst)", setup=setup100 + fsetup, number=100)))
print()
print('OP with 1000\t:{}'.format(timeit(stmt="f2(lst)", setup=setup1000 + fsetup, number=100)))
print('eric with 1000\t:{}'.format(timeit(stmt="eric(lst)", setup=setup1000 + fsetup, number=100)))
print('bpl with 1000\t:{}'.format(timeit(stmt="bpl(lst)", setup=setup1000 + fsetup, number=100)))
print('meow with 10\t:{}'.format(timeit(stmt="meow(lst)", setup=setup1000 + fsetup, number=100)))

10 elements on the list, 100 repetitions
OP      :0.00102
eric    :0.00117
bpl     :0.00141
meow    :0.00159

100 elements on the list, 100 repetitions
OP      :0.03200
eric    :0.00654
bpl     :0.01023
meow    :0.02011

1000 elements on the list, 100 repetitions
OP      :2.34821
eric    :0.06086
bpl     :0.10305
meow    :0.21190

And as a bonus an inefficient one-liner:

maximum = max(2*z -sum(x) for x, z in zip([[min(lst[:i+1]), min(lst[i+2:])] for i, _ in enumerate(lst[:-2])], lst[1:-1]))
Ma0
  • 15,057
  • 4
  • 35
  • 65
0

possible solution:

import sys
import random

random.seed(1)

l = [random.randint(0, 100) for i in range(10)]
print(l)

res = -sys.maxsize
a = sys.maxsize
b = -sys.maxsize
c = sys.maxsize

for i, v in enumerate(l[1:-1]):
    a = min(l[i], a)
    c = min(l[i + 2], c)
    b = max(l[i], b)
    res = max(2 * b - a - c, res)

print(res)

output:

[13, 85, 77, 25, 50, 45, 65, 79, 9, 2]
155
BPL
  • 9,632
  • 9
  • 59
  • 117
  • hmm for this, might there possibly be an error? With regards to the given example list, the maximum sum should be 155 (by selecting 13, 85, 2) – Russell Ng Aug 30 '17 at 13:48
-1

You are definitely on the right track, you just have to get rid of those min operations. So my hint for you is that you can pre-compute them (in linear time) beforehand, then look up the min in the loop, as you are already doing.

To clarify: you have to pre-compute min(list[0:i]) and min(list[i:n]) for all i's, before the part you already have. The idea is to store them in two arrays, say m1 and m2, such that m1[i] = min(list[0:i]) and m2[i] = min(list[i:n]). Then, use m1 and m2 in your loop

The challenge now is to compute m1 and m2 in linear time, meaning that you are not allowed to use the min function to compute them. If you have m1[i], how can you compute m1[i+1] using list[i+1]?

BlackBear
  • 22,411
  • 10
  • 48
  • 86
  • hmm im not too sure what that means exactly...could you clarify? Especially the part of "pre-computing the minimum" since the minimum changes depending on the current number we are iterating, and "look up the min in the loop", which I'm not too sure what that means. Thanks! – Russell Ng Aug 30 '17 at 13:23
  • 1
    with regards to the edit: however if we pre-compute the minimum wouldn't that still take O(n^2) time? like we have to iterate through all i's (O(n)) and then do the minimum function (O(n)) for a total time complexity of O(n^2) – Russell Ng Aug 30 '17 at 13:32
  • @RussellNg yes. The challenge is to do it in linear time, without using the built-in `min` function. If you have `m1[i]`, how can you compute `m1[i+1]`? – BlackBear Aug 30 '17 at 13:35