Алгоритм определения индексов i..j массива A, содержащего все элементы другого массива B - PullRequest
9 голосов
/ 29 мая 2009

Я сталкивался с этим вопросом в ветке вопросов интервью. Вот вопрос:

С учетом двух целочисленных массивов A [1..n] и B [1..m], найдите самое маленькое окно 1005 * в A, который содержит все элементов B. Другими словами, найдите пару такой, что A [i..j] содержит B [1..m].

Если A не содержит все элементы B, тогда i, j можно вернуть как -1. Целые числа в A не обязательно должны быть в том же порядке, что и в B. Если имеется более одного наименьшего окна (различного, но одинакового размера), то достаточно вернуть одно из них.

Пример: A [1,2,5,11,2,6,8,24,101,17,8] и B [5,2,11,8,17]. Алгоритм должен возвращать i = 2 (индекс 5 в A) и j = 9 (индекс 17 в A).

Теперь я могу вспомнить два варианта.

Предположим, что у B есть дубликаты.

  1. Эта вариация не учитывает количество раз, когда каждый элемент встречается в B. Она просто проверяет все уникальные элементы, которые встречаются в B, и находит самое маленькое соответствующее окно в A, которое удовлетворяет вышеуказанной проблеме. Например, если A [1,2,4,5,7] и B [2,2,5], это изменение не беспокоит наличие двух 2 в B, а просто проверяет A на уникальные целые числа в B, а именно 2 и 5 и, следовательно, возвращает i = 1, j = 3.

  2. Эта вариация учитывает дубликаты в B. Если в B есть два 2, то ожидается, что в A также будут видны как минимум два 2. Если нет, он возвращает -1, -1.

Когда вы отвечаете, пожалуйста, дайте мне знать, на какой вариант вы отвечаете. Псевдокод должен делать. Пожалуйста, укажите сложность пространства и времени, если это сложно вычислить. Укажите, если ваше решение предполагает, что индексы массива также начинаются с 1 или 0.

Заранее спасибо.

Ответы [ 4 ]

6 голосов
/ 29 мая 2009

Сложность

Время: O ((m + n) log m)

Пространство: O (м)

Следующее является доказуемо оптимальным с точностью до логарифмического фактора. (Я считаю, что от логарифмического фактора невозможно избавиться, и поэтому он оптимален.)

Вариант 1 - это особый случай варианта 2 со всеми кратностями 1, после удаления дубликатов из B. Так что достаточно обработать последний вариант; если вы хотите вариант 1, просто удалите дубликаты за O(m log m) раз. В дальнейшем пусть m обозначает количество различных элементов в B. Мы предполагаем m < n, потому что в противном случае мы можем просто вернуть -1 в постоянное время.

Для каждого индекса i в A мы найдем наименьший индекс s[i] такой, что A[i..s[i]] содержит B[1..m], с правильными кратностями. Важное наблюдение состоит в том, что s[i] не уменьшается , и это позволяет нам делать это в линейном амортизированном времени.

Начните с i=j=1. Мы будем хранить кортеж (c[1], c[2], ... c[m]) от числа повторений каждого элемента B в текущем окне A[i..j]. Мы также сохраним набор S индексов (подмножество 1..m), для которых счет «правильный» (т. Е. k для которого c[k]=1 в варианте 1 или c[k] = <the right number> в варианте 2) .

Итак, для i=1, начиная с j=1, увеличивайте каждый c[A[j]] (если A[j] был элементом B), проверьте, является ли c[A[j]] «правильным», и добавьте или удалите j с S соответственно. Остановитесь, когда S имеет размер m. Вы нашли s[1], самое большее O(n log m) время. (Есть O(n) j, и каждая операция установки занимает O(log m) раз.)

Теперь для вычисления последовательных s[i] с сделайте следующее. Увеличивайте i, уменьшайте c[A[i]], обновляйте S соответственно и, при необходимости, увеличивайте j до тех пор, пока S не будет иметь размер m снова. Это дает вам s[i] за каждый i. В конце укажите (i,s[i]), для которого s[i]-i было наименьшим.

Обратите внимание, что, хотя может показаться, что вы выполняете до O(n) шагов (с шагом j) для каждого i, второй указатель j перемещается только вправо, поэтому общее количество раз вы может увеличивать j не более n. (Это амортизированный анализ .) Каждый раз, когда вы увеличиваете j, вы можете выполнить операцию установки, которая занимает O(log m) времени, поэтому общее время составляет O(n log m). Требуемое пространство было для хранения кортежа счетчиков, набора элементов B, набора S и некоторого постоянного числа других переменных, поэтому O(m) всего.

Существует очевидная O(m+n) нижняя граница , потому что вам нужно изучить все элементы. Таким образом, единственный вопрос заключается в том, можем ли мы доказать необходимость фактора log; Я верю, что это так.

1 голос
/ 07 декабря 2010

Мое решение:

а. Создайте хэш-таблицу с m ключами, по одному для каждого значения в B. Каждый ключ в H отображается в динамический массив отсортированных индексов, содержащих индексы в A, равные B [i]. Это занимает O (N) время. Мы просматриваем каждый индекс j в A. Если ключ A [i] существует в H (O (1) время), то добавляем значение, содержащее индекс j of A, в список индексов, в которые отображается H [A [i]] .

На данный момент мы «сгруппировали» n элементов в m корзин. Тем не менее, общий объем памяти просто O (n).

б. Вторая часть алгоритма включает в себя поддержание «левого» и «правого» индекса для каждого списка в H. Давайте создадим два массива размера m, называемых L и R, которые содержат эти значения. Изначально в нашем примере

Мы также отслеживаем «лучшее» минимальное окно.

Затем мы повторяем следующие действия на L и R, которые по своей природе жадные: я. На каждой итерации мы вычисляем минимальное и максимальное значения в L и R. Для L, Lmax - Lmin - окно, а для R, Rmax - Rmin - окно. Мы обновляем лучшее окно, если одно из этих окон лучше текущего лучшего окна. Мы используем минимальную кучу, чтобы отслеживать минимальный элемент в L, и максимальную кучу, чтобы отслеживать самый большой элемент в R. Для сборки требуется O (m * log (m)). II. С «жадной» точки зрения, мы хотим предпринять действие, которое минимизирует размер окна в каждом L и R. Для L это интуитивно имеет смысл увеличить минимальный индекс, а для R имеет смысл уменьшить максимальный индекс.

Мы хотим увеличивать позицию массива для минимального значения, пока он не станет больше, чем 2-й наименьший элемент в L, и аналогичным образом мы хотим уменьшить позицию массива для наибольшего значения в R, пока он не станет меньше, чем 2-й по величине элемент в R.

Далее сделаем ключевое наблюдение:

Если L [i] является минимальным значением в L, а R [i] меньше 2-го наименьшего элемента в L, т. Е. Если R [i] все еще будет минимальным значением в L, если L [i] были заменены на R [i], тогда мы сделали. Теперь у нас есть «лучший» индекс в списке i, который может внести вклад в минимальное окно. Кроме того, все другие элементы в R не могут вносить вклад в лучшее окно, так как все их значения L больше, чем L [i]. Точно так же, если R [j] является максимальным элементом в R, а L [j] больше 2-го по величине значения в R, мы также делаем это путем установки R [j] = L [j]. Любой другой индекс в массиве i слева от L [j] уже учтен, как и все индексы справа от R [j], и все индексы между L [j] и R [j] будут работать хуже, чем L [J].

В противном случае мы просто увеличиваем позицию массива L [i] до тех пор, пока она не станет больше 2-го наименьшего элемента в L, и уменьшаем позицию массива R [j] (где R [j] - максимум в R), пока она не станет меньше чем второй по величине элемент в R. Мы вычисляем окна и обновляем лучшее окно, если одно из окон L или R меньше лучшего окна. Мы можем выполнить поиск Фибоначчи, чтобы оптимально сделать приращение / уменьшение. Мы продолжаем увеличивать L [i], используя приращения Фибоначчи, пока не будем больше, чем 2-й по величине элемент в L. Затем мы можем выполнить бинарный поиск, чтобы получить наименьший элемент L [i], который больше, чем 2-й по величине элемент в L, аналогично для набор R. После увеличения / уменьшения мы извлекаем самый большой элемент из максимальной кучи для R и минимальный элемент для минимальной кучи для L и вставляем новые значения L [i] и R [j] в кучи. Это операция O (log (m)).

Шаг ii. завершится, когда Lmin не сможет больше двигаться вправо или Rmax не сможет больше двигаться влево (так как значения R / L одинаковы). Обратите внимание, что у нас могут быть сценарии, в которых L [i] = R [i], но если это не минимальный элемент в L или максимальный элемент в R, алгоритм все равно будет продолжаться.

Анализ времени выполнения: а. Создание хеш-таблицы занимает O (n) времени и O (n) пространства. б. Создание куч: O (m * log (m)) время и O (m) пространство. с. Жадный итеративныйалгоритм немного сложнее анализировать. Его время выполнения действительно ограничено распределением элементов. В худшем случае мы покрываем все элементы каждого массива в хэш-таблице. Для каждого элемента мы выполняем обновление кучи O (log (m)).

Время выполнения в худшем случае, следовательно, O (n * log (m)) для итеративного жадного алгоритма. В лучшем случае мы очень быстро обнаружим, что L [i] = R [i] для минимального элемента в L или максимального элемента в R… время выполнения O (1) * log (m) для жадного алгоритма!

Средний случай кажется действительно сложным для анализа. Какова средняя «сходимость» этого алгоритма к минимальному окну. Если бы мы предположили, что инкременты Фибоначчи / бинарный поиск должны были помочь, мы могли бы сказать, что мы рассматриваем только элементы m * log (n / m) (каждый список имеет n / m элементов) в среднем случае. В этом случае время работы алгоритма жадности будет m * log (n / m) * log (m).

Общее время работы Наилучший случай: O (n + m * log (m) + log (m)) время = O (n) в предположении, что m << n Средний случай: O (n + m * log (m) + m * log (n / m) * log (m)) время = O (n) в предположении, что m << n. В худшем случае: O (n + n * log (m) + m * log (m)) = O (n * log (m)) в предположении m << n. </p>

Пробел: O (n + m) (хеш-таблица и куча) всегда.

Редактировать: Вот отработанный пример:

A [5, 1, 1, 5, 6, 1, 1, 5] Б [5, 6]

H: { 5 => {1, 4, 8} 6 => {5} }

Жадный алгоритм:

L => {1, 1} R => {3, 1}

Итерация 1: а. Lmin = 1 (так как H {5} [1]

L => {2, 1}

Rmin = H {6} [1] = 5, Rmax = H {5} [3] = 8. Окно = 8 - 5 + 1 = 4. Лучшее окно на данный момент = 4 (меньше 5, вычисленное выше) , Также отметим индексы в A (5, 8) для лучшего окна.

Уменьшите Rmax, теперь оно становится равным 2, а значение равно 4.

R => {2, 1}

б. Теперь Lmin = 4 (H {5} [2]), индекс i в L равен 1. Lmax = 5 (H {6} [1]), а индекс в L равен 2. Мы не можем увеличивать Lmin, так как L [1] = R [1] = 2. Таким образом, мы сейчас просто вычисляем окно.

Окно = Lmax - Lmin + 1 = 2, которое является лучшим окном на данный момент.

Таким образом, лучшее окно в A = (4, 5).

1 голос
/ 29 мая 2009

Вот решение, о котором я подумал (но оно не очень аккуратное).

Я собираюсь проиллюстрировать это на примере из вопроса.

Пусть A [1,2,5,11,2,6,8,24,101,17,8] и B [5,2,11,8,17]

  1. Сортировка B. (То есть B = [2,5,8,11,17]). Этот шаг занимает O (log m).

  2. Выделите массив C размера A. Итерируйте по элементам A, выполняйте двоичный поиск в отсортированном B, если он найден, введите его «index в отсортированном B + 1» в C. Если он не найден введите -1. После этого шага

A = [1, 2, 5, 11, 2, 6, 8, 24, 101, 17, 8] (без изменений, для простоты цитирования).

C = [-1, 1, 2, 4, 1, -1, 3, -1, -1, 5, 3]

Время: (n log m), пробел O (n).

  1. Найдите наименьшее окно в C, в котором есть все числа от 1 до m. Чтобы найти окно, я могу придумать два основных направления: а. Бит-ориентированный подход, в котором я устанавливаю бит, соответствующий каждой позиции, и, наконец, проверяю некоторый вид ANDing. б. Создайте другой массив D размером m, пройдите через C, и когда я столкнусь с p в C, увеличим D [p]. Используйте это для поиска окна.

Пожалуйста, оставляйте комментарии относительно общего подхода как такового, а также для 3a и 3b.

0 голосов
/ 21 мая 2012
struct Pair {
    int i;
    int j;
};

Pair
find_smallest_subarray_window(int *A, size_t n, int *B, size_t m)
{
    Pair p;

    p.i = -1;
    p.j = -1;

    // key is array value, value is array index
    std::map<int, int> map;
    size_t count = 0;

    int i;
    int j;
    for(i = 0; i < n, ++i) {
        for(j = 0; j < m; ++j) {
            if(A[i] == B[j]) {
                if(map.find(A[i]) == map.end()) {
                    map.insert(std::pair<int, int>(A[i], i));
                } else {
                    int start = findSmallestVal(map);
                    int end = findLargestVal(map);
                    int oldLength = end-start;
                    int oldIndex = map[A[i]];

                    map[A[i]] = i;
                    int _start = findSmallestVal(map);
                    int _end = findLargestVal(map);
                    int newLength = _end - _start;
                    if(newLength > oldLength) {
                        // revert back
                        map[A[i]] = oldIndex;
                    }
                }
            }
        }

        if(count == m) {
            break;
        }
    }

    p.i = findSmallestVal(map);
    p.j = findLargestVal(map);

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