Added inplace merge
This commit is contained in:
parent
72f0941bb1
commit
0a66856500
3
pages/inplace_merge/Makefile
Normal file
3
pages/inplace_merge/Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
# Default makefile for algorithmica-like pages
|
||||
|
||||
include $(SCRIPT_DIR)/mk/algorithmica.mk
|
96
pages/inplace_merge/main.md
Normal file
96
pages/inplace_merge/main.md
Normal file
@ -0,0 +1,96 @@
|
||||
# Inplace merge
|
||||
|
||||
В данной заметке мы поговорим о задаче слияния двух отсортированных массивов
|
||||
с использованием $O(1)$ дополнительной памяти. Данная задача естественно возникает
|
||||
при попытке сделать сортировку слиянием так, чтобы она не делала никаких аллокаций.
|
||||
|
||||
Считается, что читатель знаком с сортировкой слиянием.
|
||||
|
||||
**Теорема.** Существует алгоритм, который сливает два подряд идущих отсортированных массива $A, B$,
|
||||
используя $O(|A| + |B|)$ времени без дополнительной памяти.
|
||||
|
||||
Более того, вся дополнительная память будет использована под _счетчики_, числа
|
||||
размера $O(\log n)$, которыми мы индексируем массив. Далее мы будем пренебрегать
|
||||
константным количеством оных, то есть фраза _без дополнительной памяти_ значит,
|
||||
что мы имеем право завести константное число _счетчиков_. Тем самым _дополнительная память_
|
||||
измеряется элементами массивов.
|
||||
|
||||
Мы также считаем, что своп двух элементов также делается _без дополнительной памяти_, в крайнем
|
||||
случае с помощью $1$, чем мы тоже пренебрегаем.
|
||||
|
||||
## Предварительные сведения
|
||||
|
||||
**Утверждение.** Циклический сдвиг массива $A$ на любую величину можно сделать за $O(|A|)$ времени и без дополнительной
|
||||
памяти.
|
||||
|
||||
Читателю предлагается попробовать сделать это разными способами, в качестве какого-то из них
|
||||
предлагается $\text{reverse}(A[0, n)) \circ \text{reverse}(A[k, n)) \circ \text{reverse}(A[0..k))$.
|
||||
|
||||
**Лемма 1.** Существует алгоритм, который сливает два подряд идущих массива $A, B$,
|
||||
используя $O(|A| + |B|)$ времени и $\min(|A|, |B|)$ дополнительной памяти.
|
||||
|
||||
**Доказательство.** Исходя из прошлого утверждения мы можем считать, что $A$ -- меньший из массивов
|
||||
лежит в начале общего буфера. Пусть `a, b` -- указатели на начало $A, B$ соответственно, `c` -- указатель
|
||||
на начало общего буфера(то есть исходно `c = a`). Скопируем `a` в отдельный буфер `a'`.
|
||||
Далее сольем `a', b` в `c` как будто бы они не пересекались вовсе.
|
||||
|
||||
Заметим, что в процессе работы алгоритма `b - c >= 0`. Действительно, если мы берем очередной элемент из `b`,
|
||||
то эта разница не меняется, а если из `a`, то уменьшается на $1$, и изначально `b - c = |A|`. Откуда
|
||||
алгоритм действительно отработает как будто бы буферы не пересекались.
|
||||
|
||||
## Основной алгоритм
|
||||
|
||||
**Утверждение**. Существует алгоритм, который выделяет $k$ максимальных элементов
|
||||
из двух подряд идущих отсортированных массивов $A, B$ и передвигает их в конец не меняя относительный порядок остальных,
|
||||
используя $O(|A| + |B|)$ времени без дополнительной памяти.
|
||||
|
||||
Это несложно сделать с помощью циклического сдвига.
|
||||
|
||||
**Лемма 2.** Существует алгоритм, который сливает два подряд идущих отсортированных массива $A, B$,
|
||||
используя $O(|A| + |B| + \min(|A|, |B|)^2)$ времени без дополнительной памяти.
|
||||
|
||||
**Доказательство**. Обозначим $k = \min(|A|, |B|)^2$. Выделим $k$ максимумов и переместим их в конец. Пусть
|
||||
`c = a, b` -- начало $A^\prime, B^\prime$(с вытащенными максимальными элементами) соответственно, `buf` -- указатель на начало максимумов
|
||||
соответственно. Попробуем слить `a, b` в `c` с помощью буфера `buf` как в Лемме 1. Только теперь нам нельзя присваивать во время слияния,
|
||||
так как иначе мы потеряем информацию, поэтому будем свапать. Из корректности алгоритма в Лемме 1, мы получаем, что
|
||||
в начале исходного буфера будет записан результат слияния $A^\prime, B^\prime$. Но так как мы нигде не теряли информацию, в конце также
|
||||
должны остаться все максимальные элементы, возможно, в каком-то другом порядке. Отсортируем их любой квадратичной сортировкой и получим
|
||||
требуемое.
|
||||
|
||||
Далее обозначим $n = |A| + |B|$ и будем решать исходную задачу.
|
||||
|
||||
**Следствие.** Мы научились решать исходную задачу если $\min(|A|, |B|) = O\left( \sqrt{n} \right)$.
|
||||
|
||||
Выберем $\beta = \Theta(\sqrt{n})$. Выделим $\beta$ максимальных элементов и переместим их в конец. Это пространство мы будем
|
||||
использовать как буфер. Далее разобъём $A, B$ на блоки размера $\beta$. В силу Леммы 2 можно считать, что блоки полные.
|
||||
|
||||
**Шаг 1.** Итого у нас получилось порядка $ \frac{n}{\beta} $ блоков размера $\beta$. Отсортируем их **выбором** по минимальному элементу(при
|
||||
равенстве сравним еще и по максимальному). Сортировка выбором делает порядка $ \left( \frac{n}{\beta} \right)^2 $ сравнений и $ \frac{n}{\beta}$ свопов.
|
||||
Стоимость одного сравнения $O(1)$, а свопа $O(\beta)$, поэтому суммарно этот шаг отработает за $O\left( \frac{n^2}{\beta^2} +
|
||||
\frac{n}{\beta} \beta \right) = O(n)$ в силу выбора $\beta$.
|
||||
|
||||
**Лемма 3.** Если отсортировать первые $(t + 1)\beta$ элементов, то на первых $t\beta$ местах будут стоять $t\beta$ минимумов всего
|
||||
массива.
|
||||
|
||||
**Следствие.** Если мы отсортировали первые $(t + 1)\beta$ элементов, то чтобы после этого отсортировать первые $(t + 2)\beta$ достаточно
|
||||
слить подотрезки $[t\beta, (t + 1)\beta), [(t + 1)\beta, (t + 2)\beta)$.
|
||||
|
||||
**Доказательство.** Выделим $t\beta$ минимумов. Все блоки делятся на два типа: из $A$ и из $B$. Рассмотрим последний блок из $A$, в
|
||||
котором что-то выделено. Утверждается, что в блоках из $A$, которые идут после него не выделено ничего(проверьте!), а если такого нет, то нигде не выделены элементы из $A$. Также выделенные блоки из $B$ после него сразу идут подряд и все, кроме, может быть последнего выделены полностью. Аналогично для блоков из
|
||||
$B$. Здесь мы как раз пользуемся отсортированностью по минимуму.
|
||||
|
||||
Тем самым выделенные элементы должны попасть в первые $(t + 1)$ блоков. Действительно, рассмотрим последний из 'крайних' блоков. Из
|
||||
структурного утверждения, в каждом блоке до него также что-то выделено.
|
||||
Пусть до него включительно есть $k$ блоков. Так как частичных блоков не более двух, то хотя бы $k - 2$ заполнены полностью, то есть
|
||||
выделено хотя бы $(k - 2)\beta + 1$ элементов. Откуда $k <= t + 1$, что и требовалось.
|
||||
|
||||
**Шаг 2.** Согласно следствию нам достаточно итеративно сливать соседние блоки. Каждое такое слияние можно выполнить без дополнительной
|
||||
памяти с помощью буфера из максимумов, которым мы запаслись в начале. Ясно, что суммарно такие слияния работают за $O(n)$.
|
||||
|
||||
**Шаг 3.** Квадратично отсортируем буфер из максимумов, и, если требуется, дольем неполные блоки с помощью Леммы 2. Ясно, что эта
|
||||
процедура работает за $O(n)$ и не требует дополнительной памяти.
|
||||
|
||||
Тем самым мы научились решать исходную задачу.
|
||||
|
||||
## Полезные ссылки и источники
|
||||
* [Лекция Ф.Д.Руховича по алгоритмам и структурам данных](https://www.youtube.com/watch?v=V-kWungDekU&t=4536s)
|
5
pages/inplace_merge/metadata.md
Normal file
5
pages/inplace_merge/metadata.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
credits:
|
||||
- Никифор Кузнецов
|
||||
title: Inplace merge
|
||||
---
|
Loading…
Reference in New Issue
Block a user