Finding the longest interval with a decrease in value faster than quadratic time

Question:

I have a list of values for some metric, e.g.:

# 0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16
[50, 52, 58, 54, 57, 51, 55, 60, 62, 65, 68, 72, 62, 61, 59, 63, 72]

I need to find the longest interval over which the value has decreased. For the above list such interval is from index 7 to 14 (and it’s length is 8). An O(n²) solution to this is simple:

def get_longest_len(values: list[int]) -> int:
    longest = 0

    for i in range(len(values)-1):
        for j in range(len(values)-1, i, -1):
            if values[i] > values[j] and j - i > longest:
                longest = j - i
                break
    return longest + 1

Is there any way to improve it’s time complexity?

Asked By: Eugene Yarmash

||

Answers:

O(n log n):

from itertools import accumulate
from bisect import bisect

def get_longest_len(values: list[int]) -> int:
    maxi = list(accumulate(values, max))
    return max(
        i - bisect(maxi, value) + 1
        for i, value in enumerate(values)
    )

First I compute the prefix maxima. For your example:

#          0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16
values = [50, 52, 58, 54, 57, 51, 55, 60, 62, 65, 68, 72, 62, 61, 59, 63, 72]
maxi   = [50, 52, 58, 58, 58, 58, 58, 60, 62, 65, 68, 72, 72, 72, 72, 72, 72]

Then for each value, I can use binary search in these maxima to find the earliest larger value. For example the 59 at index 14 in values: We find that the earliest number in maxi larger than 59 is the 60 at index 7.

Correctness testing with 100 lists of 1000 randomized ascending values (the two numbers for each test case are your result and mine, and the boolean says whether they match):

True 461 461
True 360 360
True 909 909
    ...
True 576 576
True 312 312
True 810 810
100 out of 100 correct

Code:

from itertools import accumulate
from bisect import bisect
from random import randint, sample

def get_longest_len0(values: list[int]) -> int:
    longest = 0

    for i in range(len(values)-1):
        for j in range(len(values)-1, i, -1):
            if values[i] > values[j] and j - i > longest:
                longest = j - i
                break
    return longest + 1

def get_longest_len(values: list[int]) -> int:
    maxi = list(accumulate(values, max))
    return max(
        i - bisect(maxi, value) + 1
        for i, value in enumerate(values)
    )

cases = 100
correct = 0
for _ in range(cases):
    values = [i + randint(-10, 10) for i in range(1000)]
    for _ in range(5):
        i, j = sample(range(1000), 2)
        values[i], values[j] = values[j], values[i]
    expect = get_longest_len0(values)
    result = get_longest_len(values)
    correct += result == expect
    print(result == expect, expect, result)
print(correct, 'out of', cases, 'correct')

Attempt This Online!

Answered By: Kelly Bundy
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.