Here's an algorithm in O(n lg k)
time, where n
is the length of the array and k
the length of the maxmimum subarray having 2 * min > max
.
Let A
the array. Let's start with the following invariant: for j
between 0
and length A
, SA(j)
is empty or 2 * min > max
. It is extremely easy to initialize: take the empty subarray from indices 0 to 0. (Note that SA(j)
may be empty because A[j]
may be zero or negative: you don't have 2 * min > max
because min >= 2 * min > max
is impossible.)
The algorithm is: for each j
, we set SA(j)
= SA(j-1)
+ A[j]
. But if A[j] >= 2 * min(SA(j-1))
, then the invariant is broken. To restore the invariant, we have to remove all the elements e
from SA(j) that meet A[j] >= 2 * e
. In the same way, the invariant is broken if 2 * A[j] <= max(SA(j-1))
. To restore the invariant, we have to remove all the elements e
from SA(j)
that meet 2 * A[j] <= e
.
On the fly, we keep a track of the longest SA(j)
found and return it.
Hence the algorithm:
SA(0) <- A[0..1] # 1 excluded -> empty subarray
ret <- SA(0)
for j in 1..length(A):
if A[j] >= 2 * min(SA(j-1)):
i <- the last index having A[j] >= 2 * A[i]
SA(j) <- A[i+1..j+1]
else if 2 * A[j] <= max(SA(j-1)):
i <- the last index having 2 * A[j] <= A[i]
SA(j) <- A[i+1..j+1]
if length(SA(j)) > length(ret):
ret <- SA(j)
return ret
The question is: how do we find the last index i
having A[j] >= 2 * A[i]
? If we iterate over SA(j-1)
, we need k
steps at most, and then the time complexity will be O(n k)
(we start with j-1
and look for the last value that keeps the invariant).
But there is a better solution. Imagine we have a min heap that stores elements of SA(j-1)
along with their positions. The first element is the minimum of SA(j-1)
, let i0
be its index. We can remove all elements from the start of SA(j-1)
to i0
included. Now, are we sure that A[j] >= 2 * A[i]
for all remaining i
s? No: there is maybe more elements that are to small. Hence we remove the elements one after the other until the invariant is restored.
We'll need a max heap to, to handle the other situation 2 * A[j] <= max(SA(j-1))
.
The easier is to create an ad hoc queue that has the following operations:
- add(v): add an element
v
to the queue
- remove_until_min_gt(v): remove elements from start of the queue until the minimum is greater than
v
- remove_until_max_lt(v): remove elements from start of the queue until the maximum is less than
v
- maximum: get the maximum of the queue
- minimum: get the minimum of the queue
With two heaps, maximum
and minimum
are O(1)
, but the other operations are O(lg k)
.
Here is a Python implementation that keep indices of the start and the en of the queue:
import heapq
class Queue:
def __init__(self):
self._i = 0 # start in A
self._j = 0 # end in A
self._minheap = []
self._maxheap = []
def add(self, value):
# store the value and the indices in both heaps
heapq.heappush(self._minheap, (value, self._j))
heapq.heappush(self._maxheap, (-value, self._j))
# update the index in A
self._j += 1
def remove_until_min_gt(self, v):
return self._remove_until(self._minheap, lambda x: x > v)
def remove_until_max_lt(self, v):
return self._remove_until(self._maxheap, lambda x: -x < v)
def _remove_until(self, heap, check):
while heap and not check(heap[0][0]):
j = heapq.heappop(heap)[1]
if self._i < j + 1:
self._i = j + 1 # update the start index
# remove front elements before the start index
# there may remain elements before the start index in the heaps,
# but the first element is after the start index.
while self._minheap and self._minheap[0][1] < self._i:
heapq.heappop(self._minheap)
while self._maxheap and self._maxheap[0][1] < self._i:
heapq.heappop(self._maxheap)
def minimum(self):
return self._minheap[0][0]
def maximum(self):
return -self._maxheap[0][0]
def __repr__(self):
ns = [v for v, _ in self._minheap]
return f"Queue({ns})"
def __len__(self):
return self._j - self._i
def from_to(self):
return self._i, self._j
def find_min_twice_max_subarray(A):
queue = Queue()
best_len = 0
best = (0, 0)
for v in A:
queue.add(v)
if 2 * v <= queue.maximum():
queue.remove_until_max_lt(v)
elif v >= 2 * queue.minimum():
queue.remove_until_min_gt(v/2)
if len(queue) > best_len:
best_len = len(queue)
best = queue.from_to()
return best
You can see that every element of A
except the last one may pass through this queue, thus the O(n lg k)
time complexity.
Here's a test.
import random
A = [random.randint(-10, 20) for _ in range(25)]
print(A)
# [18, -4, 14, -9, 8, -6, 12, 13, -7, 7, -2, 14, 7, 9, -9, 9, 20, 19, 14, 13, 14, 14, 2, -8, -2]
print(A[slice(*find_min_twice_max_subarray(A))])
# [20, 19, 14, 13, 14, 14]
Obviously, if there was a way to find the start index that restores the invariant in O(1)
, we would have a time complexity in O(1)
. (This reminds me how the KMP algorithm finds the best new start in a string matching problem, but I don't know if it is possible to create something similar here.)
findMin
andfindMax
, which would be linear unless you built something extra to make it faster. That said, it's not hard to improve your solution to be in O(n²), by incrementally adjustingmin
andmax
asj
iterates over the array rather than recomputing them from scratch on each iteration. – ruakhmin
andmax
, you can write a while-loop that incrementsj
whenever possible without violating the criterion, and incrementsi
whenever incrementingj
is not possible without violating the criterion. – ruakh@ruakh:
so it shows up in my inbox. (It's only by chance that I saw your reply here.) But to answer your question -- yes, you'd use a min-heap and a max-heap, and yes, you'd sometimes need to remove a whole bunch of elements at once. But any given element is only added at most once and removed at most once, so when you add up all the steps, they come out to O(n log n). (Note that log n + 0 + 0 + 0 = log n.) – ruakh