Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

e-maxx_algo

.pdf
Скачиваний:
121
Добавлен:
03.06.2015
Размер:
6.19 Mб
Скачать

Рандомизированная куча

Рандомизированная куча (randomized heap) — это куча, которая за счёт применения генератора случайных чисел позволяет выполнять все необходимые операции за логарифмическое ожидаемое время.

Кучей называется бинарное дерево, для любой вершины которого справедливо, что значение в этой вершине меньше либо равно значений во всех её потомках (это куча для минимума; разумеется, симметрично можно определить кучу для максимума). Таким образом, в корне кучи всегда находится минимум.

Стандартный набор операций, определяемый для куч, следующий:

Добавление элемента

Нахождение минимума

Извлечение минимума (удаление его из дерева и возврат его значения)

Слияние двух куч (возвращается куча, содержащая элементы обеих куч; дубликаты не удаляются)

Удаление произвольного элемента (при известной позиции в дереве)

Рандомизированная куча позволяет выполнять все эти операции за ожидаемое время

при очень

простой реализации.

 

Структура данных

Сразу опишем структуру данных, описывающую бинарную кучу:

struct tree {

T value;

tree * l, * r;

};

В вершине дерева хранится значение

некоторого типа , для которого определён оператор

сравнения (

). Кроме того, хранятся указатели на левого и правого сыновей (которые равны 0,

если соответствующий сын отсутствует).

 

Выполнение операций

Нетрудно понять, что все операции над кучей сводятся к одной операции: слиянию двух куч в одну. Действительно, добавление элемента в кучу равносильно слиянию этой кучи с кучей, состоящей из единственного добавляемого элемента. Нахождение минимума вообще не требует никаких действий — минимумом просто является корень кучи. Извлечение минимума эквивалентно тому, что куча заменяется результатом слияния левого и правого поддерева корня. Наконец, удаление произвольного элемента аналогично удалению минимума:

всё поддерево с корнем в этой вершине заменяется результатом слияния двух поддеревьев-сыновей этой вершины.

Итак, нам фактически надо реализовать только операцию слияния двух куч, все остальные операции тривиально сводятся к этой операции.

Пусть даны две кучи

и

, требуется вернуть их объединение. Понятно, что в корне каждой из этих куч находятся

их минимумы, поэтому в корне результирующей кучи будет находиться минимум из этих двух значений. Итак, мы сравниваем, в корне какой из куч находится меньшее значение, его помещаем в корень результата, а теперь

мы должны объединить сыновей выбранной вершины с оставшейся кучей. Если мы по какому-то признаку выберем одного из двух сыновей, то тогда нам надо будет просто объединить поддерево в корне с этим сыном с кучей. Таким образом, мы снова пришли к операции слияния. Рано или поздно этот процесс остановится (на это понадобится, понятно, не более чем сумма высот куч).

Таким образом, чтобы достичь логарифмической асимптотики в среднем, нам надо указать способ выбора одного из двух сыновей с тем, чтобы в среднем длина проходимого пути получалась бы порядка логарифма от количества элементов в куче. Нетрудно догадаться, что производить этот выбор мы будем случайно, таким образом, реализация операции слияния получается такой:

tree * merge (tree * t1, tree * t2) { if (!t1 || !t2)

return t1 ? t1 : t2; if (t2->value < t1->value)

swap (t1, t2); if (rand() & 1)

swap (t1->l, t1->r);

t1->l = merge (t1->l, t2); return t1;

}

Здесь сначала проверяется, если хотя бы одна из куч пуста, то никаких действий по слиянию производить не надо. Иначе, мы делаем, чтобы куча была кучей с меньшим значением в корне (для чего обмениваем и , если надо). Наконец, мы считаем, что вторую кучу будем сливать с левым сыном корня кучи , поэтому мы случайным образом обмениваем левого и правого сыновей, а затем выполняем слияние левого сына и второй кучи.

Асимптотика

Введём случайную величину , обозначающую длину случайного пути от корня до листа (длина в

числе рёбер). Понятно, что алгоритм

выполняется за

операций. Поэтому

для исследования асимптотики алгоритма надо исследовать случайную величину .

Математическое ожидание

Утверждается, что математическое ожидание оценивается сверху логарифмом от числа вершин в этой куче:

 

 

 

Доказывается это легко по индукции. Пусть и

— соответственно левое и правое поддеревья корня кучи , а

и

— количества вершин в них (понятно, что

).

Тогда справедливо:

что и требовалось доказать.

Превышение ожидаемой оценки

Докажем, что вероятность превышения полученной выше оценки мала:

для любой положительной константы .

Обозначим через множество путей от корня кучи до листьев, длина которых превосходит

.

Заметим, что для любого пути длины

вероятность того, в качестве случайного пути будет выбран именно он,

равна

. Тогда получаем:

 

 

 

 

 

 

 

 

 

 

что и требовалось доказать.

Асимптотика алгоритма

Таким образом, алгоритм , а, значит, и все остальные выраженные через него операции, выполняется за в среднем.

Более того, для любой положительной константы найдётся такая положительная константа , что вероятность того, что операция потребует больше чем операций, меньше (это в некотором смысле описывает

худшее поведение алгоритма).

Задача RMQ (Range Minimum Query - минимум на отрезке)

Дан массив A[1..N]. Поступают запросы вида (L, R), на каждый запрос требуется найти минимум в массиве A, начиная с позиции L и заканчивая позицией R.

Приложения

Помимо непосредственного применения в самых разных задачах, можно отметить следующие: Задача LCA (наименьший общий предок)

Решение

Задача RMQ решается с помощью структур данных.

Из описанных на сайте структур данных можно выбрать:

Sqrt-декомпозиция - отвечает на запрос за O (sqrt (N)), препроцессинг за O (N). Преимущество в том, что это очень простая структура данных. Недостаток - асимптотика.

Дерево отрезков - отвечает на запрос за O (log N), препроцессинг за O (N).

Преимущество - хорошая асимптотика. Недостаток - бОльший объём кода по сравнению с другими структурами данных.

Дерево Фенвика - отвечает на запрос за O (log N), препроцессинг за O (N log N)

Преимущество - очень быстро пишется и работает тоже очень быстро. Но значительный недостаток - дерево Фенвика может отвечать только на запросы с L = 1, что для многих приложений неприменимо.

Примечание. "Препроцессинг" - это предварительная обработка массива A, фактически это построение структуры данных для данного массива.

Теперь предположим, что массив A может изменяться в процессе работы (т.е. также будут поступать запросы об изменении значения в некотором отрезке [L;R]). Тогда полученную задачу можно решить с помощью

Sqrt-декомпозиции и Дерева отрезков.

Нахождение наидлиннейшей возрастающей подпоследовательности

Условие задачи следующее. Дан массив из чисел:

. Требуется найти в

этой последовательности строго возрастающую подпоследовательность наибольшей длины.

Формально это выглядит следующим образом: требуется найти такую последовательность индексов , что:

В данной статье рассматриваются различные алгоритмы решения данной задачи, а также некоторые задачи, которые можно свести к данной задаче.

Решение за : метод динамического программирования

Динамическое программирование — это весьма общая методика, позволяющая решать огромный класс задач. Здесь мы рассмотрим эту методику применительно к нашей конкретной задаче.

Научимся сначала искать длину наидлиннейшей возрастающей подпоследовательности, а восстановлением самой подпоследовательности займёмся чуть позже.

Динамическое программирование для поиска длины ответа

Для этого давайте научимся считать массив , где — это длина наидлиннейшей возрастающей подпоследовательности, оканчивающейся именно в элементе с индексом . Массив этот (он и есть —

сама динамика) будем считать постепенно: сначала

, затем

и т.д. В конце, когда этот массив будет

подсчитан нами, ответ на задачу будет равен максимуму в массиве

.

Итак, пусть текущий индекс — , т.е. мы хотим посчитать значение

, а все предыдущие значения

уже подсчитаны. Тогда заметим, что у нас есть два варианта:

либо

, т.е. искомая подпоследовательность состоит только из числа

.

 

либо

. Тогда перед числом

в искомой подпоследовательности стоит какое-то другое число.

 

Давайте переберём это число: это может быть любой элемент

 

 

, но такой, что

 

 

. Пусть мы рассматриваем какой-то текущий индекс

. Поскольку динамика

для него уже

 

подсчитана, получается, что это число

вместе с числом

даёт ответ

. Таким образом,

 

можно считать по такой формуле:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Объединяя эти два варианта в один, получаем окончательный алгоритм для вычисления :

Этот алгоритм — и есть сама динамика.

Реализация

Приведём реализацию описанного выше алгоритма, которая находит и выводит длину наидлиннейшей возрастающей подпоследовательности:

int d[MAXN]; // константа MAXN равна наибольшему возможному значению n

for (int i=0; i<n; ++i) { d[i] = 1;

for (int j=0; j<i; ++j) if (a[j] < a[i])

d[i] = max (d[i], 1 + d[j]);

}

int ans = d[0];

for (int i=0; i<n; ++i)

ans = max (ans, d[i]); cout << ans << endl;

Восстановление ответа

Пока мы лишь научились искать длину ответа, но саму наидлиннейшую подпоследовательность мы вывести не можем, т. к. не сохраняем никакой дополнительной информации о том, где достигаются максимумы.

Чтобы суметь восстановить ответ, помимо динамики

надо также хранить вспомогательный

 

массив

— то, в каком месте достигся максимум для каждого значения

. Иными словами, индекс

будет обозначать тот самый индекс , при котором получилось наибольшее значение

. (Этот массив

в динамическом программировании часто называют "массивом предков".)

 

 

Тогда, чтобы вывести ответ, надо просто идти от элемента с максимальным значением

по его предкам до тех

пор, пока мы не выведем всю подпоследовательность, т.е. пока не дойдём до элемента со значением

.

Реализация восстановления ответа

Итак, у нас изменится и код самой динамики, и добавится код, производящий вывод наидлиннейшей подпоследовательности (выводятся индексы элементов подпоследовательности, в 0-индексации).

Для удобства мы изначально положили индексы : для элементов, у которых динамика получилась равной единице, это значение предка так и останется минус единицей, что чуть-чуть удобнее при восстановлении ответа.

int d[MAXN], p[MAXN]; // константа MAXN равна наибольшему возможному значению n

for (int i=0; i<n; ++i) { d[i] = 1;

p[i] = -1;

for (int j=0; j<i; ++j) if (a[j] < a[i])

if (1 + d[j] > d[i]) { d[i] = 1 + d[j]; p[i] = j;

}

}

int ans = d[0], pos = 0; for (int i=0; i<n; ++i)

if (d[i] > ans) { ans = d[i]; pos = i;

}

cout << ans << endl;

vector<int> path; while (pos != -1) {

path.push_back (pos); pos = p[pos];

}

reverse (path.begin(), path.end()); for (int i=0; i<(int)path.size(); ++i)

cout << path[i] << ' ';

Альтернативный способ восстановления ответа

Впрочем, как почти всегда в случае динамического программирования, для восстановления ответа можно не хранить дополнительный массив предков , а просто заново пересчитывая текущий элемент динамики и ища, на

каком же индексе был достигнут максимум.

Этот способ при реализации приводит к чуть более длинному коду, однако взамен получаем экономию памяти и абсолютное совпадение логики программы в процессе подсчёта динамики и в процессе восстановления.

Решение за : динамическое программирование

с двоичным поиском

Чтобы получить более быстрое решение задачи, построим другой вариант динамического программирования за

,

а затем поймём, как можно этот вариант ускорить до

.

 

Динамика теперь будет такой: пусть

— это число, на которое оканчивается

 

возрастающая подпоследовательность длины (а если таких чисел несколько — то наименьшее из них). Изначально мы полагаем , а все остальные элементы .

Считать эту динамику мы будем постепенно, обработав число , затем , и т.д. Приведём реализацию этой динамики за :

int d[MAXN]; d[0] = -INF;

for (int i=1; i<=n; ++i) d[i] = INF;

for (int i=0; i<n; i++)

for (int j=1; j<=n; j++)

if (d[j-1] < a[i] && a[i] < d[j]) d[j] = a[i];

Заметим теперь, что у этой динамики есть одно очень важное свойство:

 

для

всех

. Другое свойство — что каждый элемент

обновляет максимум одну ячейку

.

Таким образом, это означает, что обрабатывать очередное

мы можем за

, сделав двоичный поиск

по массиву . В самом деле, мы просто ищем в массиве

первое число, которое строго больше

, и

пытаемся произвести обновление этого элемента аналогично приведённой выше реализации.

 

Реализация за

Воспользовавшись стандартным в языке C++ алгоритмом двоичного поиска (который возвращает позицию первого элемента, строго большего данного), получаем такую простую реализацию:

int d[MAXN]; d[0] = -INF;

for (int i=1; i<=n; ++i) d[i] = INF;

for (int i=0; i<n; i++) {

int j = int (upper_bound (d.begin(), d.end(), a[i]) - d.begin()); if (d[j-1] < a[i] && a[i] < d[j])

d[j] = a[i];

}

Восстановление ответа

По такой динамике тоже можно восстановить ответ, для чего опять же помимо динамики также надо хранить массив "предков" — то, на элементе с каким индексом оканчивается оптимальная подпоследовательность длины

. Кроме того, для каждого элемента массива

надо будет хранить его "предка" — т.е. индекс того элемента,

который должен стоять перед в оптимальной подпоследовательности.

Поддерживая эти два массива по ходу вычисления динамики, в конце будет нетрудно восстановить искомую подпоследовательность.

(Интересно отметить, что применительно к данной динамике ответ можно восстанавливать только так, через массивы предков — а без них восстановить ответ после вычисления динамики будет невозможно. Это один из редких случаев, когда к динамике неприменим альтернативный способ восстановления — без массивов предков).

Решение за : структуры данных

Если приведённый выше способ за

весьма красив, однако не совсем тривиален идейно, то есть и

другой путь: воспользоваться одной из известных простых структур данных.

В самом деле, давайте вернёмся к самой первой динамике, где состоянием являлась просто текущая позиция.

Текущее значение динамики

вычисляется как максимум значений

среди всех таких элементов ,

что

.

 

 

Следовательно, если мы через обозначим такой массив, в который будем записывать значения динамики от чисел:

то получается, что всё, что нам надо уметь — это искать максимум на префиксе массива

: .

Задача поиска максимума на префиксах массива (с учётом того, что массив может меняться) решается многими стандартными структурами данных, например, деревом отрезков или деревом Фенвика.

Воспользовавшись любой такой структурой данных, мы получим решение за .

У этого способа решения есть явные недостатки: по длине и сложности реализации этот путь будет в любом случае хуже, чем описанная выше динамика за . Кроме того, если входные числа могут

быть достаточно большими, то скорее всего их придётся сжимать (т.е. перенумеровывать от до ) — без этого многие стандартные структуры данных работать не смогут из-за высокого потребления памяти.

С другой стороны, у данного пути есть и преимущества. Во-первых, при таком способе решения не придётся задумываться о хитрой динамике. Во-вторых, этот способ позволяет решать некоторые обобщения нашей задачи (о них см. ниже).

Смежные задачи

Приведём здесь несколько задач, тесно связанных с задачей поиска наидлиннейшей возрастающей подпоследовательности.

Наидлиннейшая неубывающая подпоследовательность

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

Решение этой задачи по сути ничем не отличается от нашей исходной задачи, просто при сравнениях изменятся знаки неравенств, а также надо будет немного изменить двоичный поиск.

Количество наидлиннейших возрастающих подпоследовательностей

Для решения этой задачи можно использовать самую первую динамику за

либо подход с помощью

структур данных для решения за

. И в том, и в том случае все изменения заключаются только в том,

что помимо значения динамики

надо также хранить, сколькими способами это значение могло быть получено.

По всей видимости, способ решения через динамику за к данной задаче применить невозможно.

Наименьшее число невозрастающих подпоследовательностей, покрывающих данную последовательность

Условие таково. Дан массив из чисел

. Требуется раскрасить его числа в наименьшее

число цветов так, чтобы по каждому цвету получалась бы невозрастающая подпоследовательность.

Решение. Утверждается, что минимальное количество необходимых цветов равно длине наидлиннейшей возрастающей подпоследовательности.

Доказательство. Фактически, нам надо доказать двойственность этой задачи и задачи поиска наидлиннейшей возрастающей подпоследовательности.

Обозначим через длину наидлиннейшей возрастающей подпоследовательности, а через — искомое наименьшее число невозрастающих подпоследовательностей. Нам надо доказать, что .

С одной стороны, понятно, почему не может быть

: ведь если у нас есть строго возрастающих элементов,

то никакие два из них не могли попасть в одну невозрастающую подпоследовательность, а, значит,

.

Покажем теперь, что, наоборот, не может быть

. Докажем это от противного: предположим, что

.

Тогда рассмотрим любой оптимальный набор из

невозрастающих подпоследовательностей. Преобразуем этот

набор таким образом: пока есть две таких подпоследовательности, что первая начинается раньше второй, но при этом первая начинается с числа, больше либо равного чем начало второй — отцепим это стартовое число от первой подпоследовательности и прицепим в начало второй. Таким образом, через какое-то конечное число шагов у нас останется подпоследовательностей, причём их стартовые числа будут образовывать

возрастающую подпоследовательность длины . Но

, т.е. мы пришли к противоречию (ведь не может

быть возрастающих подпоследовательностей длиннее

).

Таким образом, в самом деле, , что и требовалось доказать.

Восстановление ответа. Утверждается, что само искомое разбиение на подпоследовательности можно искать жадно, т.е. идя слева направо и относя текущее число в ту подпоследовательность, которая сейчас заканчивается на минимальное число, больше либо равное текущему.

Задачи в online judges

Список задач, которые можно решить по данной тематике:

 

MCCME #1793 "Наибольшая возрастающая подпоследовательность за O(n*log(n))"

 

[сложность: низкая]

 

 

TopCoder SRM 278 "500 IntegerSequence"

[сложность: низкая]

TopCoder SRM 233 "DIV2 1000 AutoMarket"

[сложность: низкая]

Всеукраинская олимпиада школьников по информатике — задача F "Турист" [сложность: средняя]

Codeforces Beta Round #10 — задача D "НОВП"

[сложность: средняя]

ACM.TJU.EDU.CN 2707 "Greatest Common Increasing Subsequence" [сложность: средняя]

SPOJ #57 "SUPPER. Supernumbers in a permutation"

[сложность: средняя]

ACM.SGU.RU #521 "North-East" [сложность: высокая]

 

TopCoder Open 2004 — Round 4 — "1000. BridgeArrangement"

[сложность: высокая]

K-ая порядковая статистика за O (N)

Пусть дан массив A длиной N и пусть дано число K. Задача заключается в том, чтобы найти в этом массиве K-ое по величине число, т.е. K-ую порядковую статистику.

Основная идея - использовать идеи алгоритма быстрой сортировки. Собственно, алгоритм несложный, сложнее доказать, что он работает в среднем за O (N), в отличие от быстрой сортировки.

Реализация в виде нерекурсивной функции:

template <class T>

T order_statistics (std::vector<T> a, unsigned n, unsigned k)

{

using std::swap;

for (unsigned l=1, r=n; ; )

{

if (r <= l+1)

{

//текущая часть состоит из 1 или 2 элементов -

//легко можем найти ответ

if (r == l+1 && a[r] < a[l]) swap (a[l], a[r]);

return a[k];

}

//упорядочиваем a[l], a[l+1], a[r] unsigned mid = (l + r) >> 1;

swap (a[mid], a[l+1]); if (a[l] > a[r])

swap (a[l], a[r]); if (a[l+1] > a[r])

swap (a[l+1], a[r]); if (a[l] > a[l+1])

swap (a[l], a[l+1]);

//выполняем разделение

//барьером является a[l+1], т.е. медиана среди a[l], a[l+1],

a[r]

unsigned

i = l+1, j = r;

const T

cur = a[l+1];

for (;;)

{

while (a[++i] < cur) ; while (a[--j] > cur) ; if (i > j)

break; swap (a[i], a[j]);

}

//вставляем барьер a[l+1] = a[j];

a[j] = cur;

//продолжаем работать в той части,

//которая должна содержать искомый элемент if (j >= k)

r = j-1; if (j <= k)

l = i;

}

}

Следует заметить, что в стандартной библиотеке C++ этот алгоритм уже реализован - он называется nth_element.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]