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?
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')
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?
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')