Найти условие максимальной длины подмассива 2 * min> max - PullRequest
2 голосов
/ 16 марта 2020

Это был вопрос интервью, который мне недавно задали в Adobe:

В массиве найдите подмассив максимальной длины с условием 2 * min > max, где min - минимальный элемент subarray, а max - максимальный элемент подмассива.

У кого-нибудь есть подход лучше, чем O (n ^ 2)?
Конечно, мы не можем отсортировать, так как требуется подмассив.

Ниже мой подход O (n ^ 2):

max=Integer.MIN_VALUE;
for (int i=0; i<A.length-1;i++)
  for(j=i+1;j<A.length;j++)
  {
    int min =findMin(A,i,j);
    int max =findMAx(A,i,j);
    if(2*min<=max) {
      if(j-i+1>max) 
        max = j-i+1
    }
  }

Кто-нибудь знает решение O (n)?

Ответы [ 3 ]

2 голосов
/ 18 марта 2020

Пусть A [ i j ] - подрешетка, состоящая из A [ i ], A [ i + 1],… A [ j ].

Наблюдения:

  • Если A [ i j ] не удовлетворяет критерию, то и A [ я … ( j + 1)], потому что 2 · мин ( A [ i … ( j + 1) ]) ≤ 2 · мин ( A [ i j ]) ≤ max ( A [ i j ]) ≤ max ( A [ i … ( j + 1)]). Таким образом, вы можете прервать свой внутренний l oop, как только найдете j, для которого условие не выполняется.
  • Если мы уже нашли подмассив длины L , который соответствует критерию, то нет необходимости рассматривать любой подмассив длиной ≤ L . Таким образом, вы можете начать свой внутренний l oop с j = i + maxLength вместо j = i + 1. (Конечно, вам нужно инициализировать maxLength до 0, а не Integer.MIN_VALUE.)

Комбинируя вышесказанное, мы имеем:

int maxLength = 0;
for (int i = 0; i < A.length; ++i) {
    for (int j = i + maxLength; j < A.length; ++j) {
        if (findMin(A,i,j) * 2 > findMax(A,i,j)) {
            // success -- now let's look for a longer subarray:
            maxLength = j - i + 1;
        } else {
            // failure -- keep looking for a subarray this length:
            break;
        }
    }
}

It может показаться неочевидным на первый взгляд, но внутренняя l oop теперь проходит всего O ( n ) итераций, поскольку j может принимать только каждое значение в большинство раз (Например, если i равно 3, а maxLength равно 5, то j начинается с 8. Если мы A [3… 8] соответствуют критерию, мы увеличиваем maxLength до тех пор, пока найдите подмассив, который не не соответствует критерию. Как только это произойдет, мы перейдем от A [ i … ( i + maxLength )] до A [( i + 1)… (( i + 1) + maxLength ) ], что означает, что новый l oop начинается с большего значения j, чем предыдущий l oop остановился.)

Мы можем сделать это более явным путем рефакторинга бита для модели A [ i j ] как скользящее и потенциально расширяющееся окно: увеличение i удаляет элемент с левого края окна, увеличивая j добавляет элемент к правому краю окна, и нет необходимости увеличивать i без увеличения j:

int maxLength = 0;
int i = 0, j = 0;
while (j < A.length) {
    if (findMin(A,i,j) * 2 > findMax(A,i,j)) {
        // success -- now let's look for a longer subarray:
        maxLength = j - i + 1;
        ++j;
    } else {
        // failure -- keep looking for a subarray this length:
        ++i;
        ++j;
    }
}

или, если вы предпочитаете:

int maxLength = 0;
int i = 0;
for (int j = 0; j < A.length; ++j) {
    if (findMin(A,i,j) * 2 > findMax(A,i,j)) {
        // success -- now let's look for a longer subarray:
        maxLength = j - i + 1;
    } else {
        // failure -- keep looking for a subarray this length:
        ++i;
    }
}

Так как в вашем решении внутренний l oop повторяет всего * 112 6 * O ( n 2 ), и вы заявили, что ваше решение работает в O ( n 2 ) времени, мы могли бы утверждать, что, поскольку вышеприведенное имеет внутреннюю l oop итерацию только O ( n ) раз, вышеприведенное должно выполняться в O ( n ) время.

Проблема в том, что предпосылка действительно очень сомнительна; вы не указали, как бы вы реализовали findMin и findMax, но простая реализация потребует O ( j - i ), например что ваше решение на самом деле работает в O ( n 3 ), а не O ( n 2 ). Поэтому, если мы уменьшим количество внутренних итераций l oop с O ( n 2 ) до O ( n ), что просто снижает общую сложность времени с O ( n 3 ) до O ( n 2 ).

Но, как это бывает, можно можно рассчитать минимальное и максимальное значения этих подмассивов в амортизированных O (1) время и O ( n ) дополнительное пространство, используя «Метод 3» при https://www.geeksforgeeks.org/sliding-window-maximum-maximum-of-all-subarrays-of-size-k/. (Подсказка к גלעד ברקן для указания на это.) Как это работает, вы сохраняете два запроса: minseq для расчета min и maxseq для расчета max. (Я объясню только minseq; maxseq аналогично.) В любой момент времени первый элемент (голова) minseq является индексом элемента min в A [ я ... J ]; второй элемент minseq является индексом элемента min после первого элемента; и так далее. (Так, например, если подмассив [80,10,30,60,50] начинается с индекса # 2, тогда minseq будет [3,4,6], то есть индексами подпоследовательности [10 , 30,50].) Всякий раз, когда вы увеличиваете i, вы проверяете, является ли старое значение i заголовком minseq (это означает, что это текущий минимум); если это так, вы удалите голову. Всякий раз, когда вы увеличиваете j, вы неоднократно проверяете, является ли хвост minseq индексом элемента, который больше или равен элементу в j; если это так, вы удалите хвост и повторите. После удаления всех таких элементов хвоста вы добавляете j к хвосту. Поскольку каждый индекс добавляется и удаляется из очереди не более одного раза, эта бухгалтерия имеет общую стоимость O ( n ).

Это дает вам в целом O ( n ) время, по желанию.

0 голосов
/ 22 марта 2020

Вот алгоритм во O(n lg k) времени, где n - длина массива, а k - длина максимального подмассива, имеющего 2 * min > max.

Пусть A массив. Давайте начнем со следующего инварианта: для j между 0 и length A, SA(j) пусто или 2 * min > max. Инициализировать чрезвычайно легко: возьмите пустой подмассив из индексов от 0 до 0. (Обратите внимание, что SA(j) может быть пустым, потому что A[j] может быть нулевым или отрицательным: у вас нет 2 * min > max, потому что min >= 2 * min > max невозможно .)

Алгоритм: для каждого j мы устанавливаем SA(j) = SA(j-1) + A[j]. Но если A[j] >= 2 * min(SA(j-1)), то инвариант нарушается. Чтобы восстановить инвариант, мы должны удалить все элементы e из SA (j), которые встречают A[j] >= 2 * e. Таким же образом инвариант нарушается, если 2 * A[j] <= max(SA(j-1)). Чтобы восстановить инвариант, мы должны удалить все элементы e из SA(j), которые встречают 2 * A[j] <= e.

. На лету мы отслеживаем самый длинный найденный SA(j) и возвращаем его.

Отсюда и алгоритм:

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

Вопрос: как нам найти последний индекс i, имеющий A[j] >= 2 * A[i]? Если мы перебираем SA(j-1), нам нужно не более k шагов, и тогда сложность времени будет O(n k) (мы начнем с j-1 и ищем последнее значение, которое сохраняет инвариант).

Но есть лучшее решение. Представьте, что у нас есть минимальная куча, в которой хранятся элементы SA(j-1) вместе с их позициями. Первый элемент - это минимум SA(j-1), пусть i0 будет его индексом. Мы можем удалить все элементы с начала SA(j-1) до i0 включительно. Теперь мы уверены, что A[j] >= 2 * A[i] для всех оставшихся i с? Нет: может быть, есть еще элементы, которые являются маленькими. Следовательно, мы удаляем элементы один за другим, пока не будет восстановлен инвариант.

Нам потребуется максимальная куча, чтобы справиться с другой ситуацией 2 * A[j] <= max(SA(j-1)).

Чем проще создать ad ho c очередь, которая имеет следующие операции:

  • add (v): добавить элемент v в очередь
  • remove_until_min_gt ( v): удалять элементы из начала очереди до тех пор, пока минимум не станет больше v
  • remove_until_max_lt (v): удалить элементы из начала очереди до тех пор, пока максимум не станет меньше v
  • максимум: получить максимум из очереди
  • минимум: получить минимум из очереди

С двумя кучами maximum и minimum равны O(1), но другие операции: O(lg k).

Вот реализация Python, в которой хранятся индексы начала и конца очереди:

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

Вы можете видеть, что каждый элемент A кроме последнего может проходить через эту очередь, таким образом, O(n lg k) сложность времени. * 1 090 *

Вот тест.

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]

Очевидно, что если бы был способ найти начальный индекс, который восстанавливает инвариант в O(1), мы бы имели временную сложность в O(1). (Это напоминает мне, как алгоритм KMP находит лучший новый старт в проблеме сопоставления строк, но я не знаю, возможно ли создать что-то подобное здесь.)

0 голосов
/ 18 марта 2020

Существует простое O(n log n) время и O(n) пространственное решение, поскольку мы знаем, что длина окна ограничена, то есть для двоичного поиска размера окна. Для каждого выбранного размера окна мы перебираем массив один раз и делаем O(log n) таких обходов. Если окно слишком большое, мы не найдем решение и попробуем окно наполовину меньше; в противном случае мы пробуем окно на полпути между этим и последним успешным размером окна. (Чтобы обновить минимальное и максимальное значения в скользящем окне, мы можем использовать метод 3, описанный здесь .)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...