Участник:Artem.ustinov/НВП

Материал из Викиконспекты
Версия от 17:59, 31 декабря 2017; Artem.ustinov (обсуждение | вклад) (Доказательство корректности алгоритма)
Перейти к: навигация, поиск
Задача:
Дана перестановка π множества {1,2, , n}. Требуется найти НВП π за O(nloglogk), где k — длина НВП.


Task.jpg

Алгоритм за O(n log log n)

Нахождение длины НВП

Основная идея

Пусть {π1,π2, , πn} — входная перестановка.

Будем последовательно обрабатывать элементы в порядке π1,π2, , πn:

Для каждой длины l=1,2, , n предполагаемой НВП находим наименьший элемент, который может быть последним в возрастающей подпоследовательности длины l и запишем его в массив Bl. Будем называть его наилучшим элементом для длины l.

  • Если πi больше каждого элемента B, вычисленного для подпоследовательности π1,π2,  ,πi1, тогда с ним можно сделать возрастающую подпоследовательность максимальной длины из уже рассмотренных, в которой он будет последним элементом. Значит, записываем его в конец B.
  • Иначе πi будет наилучшим элементом для уже существующей длины, тогда мы находим наименьшее k:Bk>πi и заменяем Bk элементом πi.

Следует отметить, что полученный массив также образует возрастающую последовательность, на котором мы должны выполнять операции insert,next,delete, соответственно целесообразно использовать приоритетную очередь, реализованную через Дерево ван Эмде Боаса. Так как данная структура данных производит описанные операции за O(logk), где k — количество бит чисел, которые позволяет хранить дерево, то полученный алгоритм работает за O(nloglogn), потому что все элементы последовательности не превосходят n.

Пример

Типы операций
  • Добавление элемента, который больше всех предыдущих:

Operation1.jpg

  • Замещение элемента более подходящим, т.е. добавление немаксимального элемента:

Operation2 1.jpg Operation2 2.jpg

Пример последовательности
π1 π2 π3 π4 π5 π6 π7 π8 π9 π10 π11 π12
9 3 10 4 8 1 2 12 6 5 7 11
Состояние очереди при каждом добавлении
B1 B2 B3 B4 B5  πi 
9 9
3 3
3 10 10
3 4 4
3 4 8 8
1 4 8 1
1 2 8 2
1 2 8 12 12
1 2 6 12 6
1 2 5 12 5
1 2 5 7 7
1 2 5 7 11 11

Псевдокод

int LIS([math]\pi[/math][n])

   PriorityQueue B // рабочая приоритетная очередь
   int k = 0       // длина НВП
   for i = 1 to n
       x = [math]\pi[/math][i]
       // в любом случае добавляем в очередь очередной элемент
       // устаревшие будем удалять
       B.insert(x)
       if [math]\exists[/math] B.next(x)
           // добавленный элемент — не максимальный
           // удаляем следующее за x значение
           B.delete(B.next(x))
       else
           // добавленный элемент — максимальный
           // предыдущие значения не трогаем, очередь увеличилась
           k = k + 1           
   return k

Расширение алгоритма до нахождения НВП

Основная идея

Будем запоминать пары: для каждого элемента записываем его "предшественника".

Тогда, пройдя по предшественникам, начиная с последнего элемента очереди B, мы можем восстановить НВП.

Общий вид алгоритма

B1 B2 B3 B4 B5  πi 
9 9
3 3
3 10 10
3 4 4
3 4 8 8
1 4 8 1
1 2 8 2
1 2 8 12 12
1 2 6 12 6
1 2 5 12 5
1 2 5 7 7
1 2 5 7 11 11
predecessor
1 2 3 4 5 6 7 8 9 10 11 12
1 3 2 2 5 4 3 7 8

Псевдокод

int[] LIS([math]\pi[/math][n])

   PriorityQueue B
   int k = 0
   int predecessor[n] // резервируем [math]n[/math] позиций
   for i = 1 to n
       x = [math]\pi[/math][i]
       B.insert(x)
       predecessor[x] = B.prev(x)
       if [math]\exists[/math] B.next(x)
           B.delete(B.next(x))
       else
           k = k + 1
   // по цепочке от последнего элемента 
   // восстанавливаем НВП
   int result[k]
   int cur = B.max
   for i = k - 1 downto 0
       result[i] = cur
       cur = predecessor[cur]
   return result

Оптимизация до O(n log log k)

Чтобы Дерево ван Эмде Боаса выполняло операции за O(loglogk), необходимо алфавит обрабатываемых значений уменьшить до O(k).

Предположим, мы знаем такое приближение числа k числом m:mk. Мы обсудим, как найти такое m позже.

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

Деление на блоки

Последовательность S делится на блоки Cj, j=1, , nm элементов: Cj=(π(j1)m+1,π(j1)m+2,  ,π(j1)m+m))

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

Цифровая сортировка каждых блоков отдельно будет давать нам время рваботы O(nmn)=O(n2m). Чтобы отсортировать их за линейное время, дополним каждый элемент номером его блока и получим пары i/m,πi. Цифровая сортировка этих пар, если принимать за старший разряд номер блока, а за младший значение элемента, будет работать за O(n), потому что значения элементов и номера блоков не превосходят n.

Обработка блока

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

Каждому элементу x взаимно однозначно сопоставим ключ y=key(x); x=elt(y). Все значения ключей расположим в промежутке {1,2,,2m}, и в очереди B будем работать со значениями ключей элементов.

Чтобы определить ключи элементов так, чтобы их значения были в представленном промежутке, будем, работая с блоком Cj, сливать элементы, ключи которых находятся в очереди B, с Csj в список merged. Получим ключи, сопоставив каждому элементу в merged его позицию в этом списке. Как было замечено ранее, элементы, чьи ключи находятся в B располагаются в возрастающем порядке, поэтому возможно производить тривиальную операцию слияния. Поскольку мы предположили, что mk, то количество ключей в B не больше m, тогда длина merged не больше 2m, что позволяет однозначно определить ключи на множестве {1,2,,2m}.

После того, как мы определили новые ключи для элементов, обновим ключи в очереди B.

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

В итоге, обработка блока делится на следующие этапы:

  • Достаем из очереди B ключи x, конвертируем их в элементы elt(x) и кладём в список elems.
  • Сливаем элементы в elems со следующим отсортированным блоком в список merged.
  • Присваеваем новые ключи элементам в порядке списка merged.
  • Вставляем в B новые ключи элементов списка elems.
  • Обрабатываем ключи элементов блока в порядке исходной последовательности с помощью алгоритма LIS. Для восстановления НВП также используем массив "предшественников", который будет работать с соответствующими ключами элементов elt(x).

Доказательство оптимальности

Утверждение:
Пусть имеется последовательность S={π1, , πn}, разбитая на n/m блоков Ci длины m. В результате описанного выше алгоритма получается очередь B, размер которой равен длине НВП последовательности S.

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

  • Пусть перед обработкой блока Ci в очереди B хранятся ключи наилучших элементов, вычисленные для подпоследовательности S(i1)m={π1, , π(i1)m}.
  • После слияния элементов очереди B и блока Csi получаем отсортированный список merged. Cопоставив ключи элементам в списке, как их позиции в нём, выполняется условие (πuj<πukkey(πuj)<key(πuk)), где πuj,πukmerged. Тогда справедливо утверждение, что любая возрастающая последовательность ключей элементов будет соответствовать возрастающей последовательности элементов.
  • Во время обработки ключей элементов алгоритм LIS работает только с очередью B и не зависит от предыдущих элементов последовательности, ключи которых не находятся в очереди. Так как на каждой итерации алгоритма LIS сохраняется описанный инвариант для наилучших значений ключей элементов каждой длины возрастающих подпоследовательностей обработанной последовательности Sj, которые соответствуют ключам наилучших элементов Sj. то по завершению работы алгоритма LIS на блоке Ci результатом его работы будет очередь, в которой хранятся ключи, соответствующие наилучшим элементам каждой длины возрастающих подпоследовательностей, входящих в Sim.
  • Таким образом, после обработки последнего блока, в очереди B будут храниться ключи наилучших элементов для каждой длины возрастающих подпоследовательностей последовательности Sn=S. Тогда последний элемент в очереди B соответствует наилучшему элементу длины НВП последовательности S, а так как в очереди B хранятся наилучшие элементы длин всех возрастающих подпоследовательностей S, то размер очереди B равен длине НВП последовательности S.

Пример

Предположим, что m=5. Исходно получаем:

Блок 1 1 1 1 1 2 2 2 2 2 3 3
π 9 3 10 4 8 1 2 12 6 5 7 11

После сортировки:

Блок 1 1 1 1 1 2 2 2 2 2 3 3
π 3 4 8 9 10 1 2 5 6 12 7 11

Первый блок

Первый блок
π 9 3 10 4 8
key 4 1 5 2 3
Cортированный
π 3 4 8 9 10
key 1 2 3 4 5

Обработка блока с помощью алгоритма LIS.

B1 B2 B3 key π
4 4 9
1 1 3
1 5 5 10
1 2 2 4
1 2 3 3 8

В результате получаем

B:{1,2,3}

merged:{3,4,8,9,10}


Второй блок

Восстанавливаем элементы B:{1,2,3} из merged:{3,4,8,9,10}: {3,4,8}.

Сливаем Cs2 и восстановленные элементы из B:

B
3 4 8
Cs2
1 2 5 6 12
merged
1 2 3 4 5 6 8 12
1 2 3 4 5 6 7 8
Второй блок
π 1 2 12 6 5
key 1 2 8 6 5
Cортированный
π 1 2 5 6 12
key 1 2 5 6 8

Обновляем ключи в очереди:

B1 B2 B3 π
3 3
3 4 4
3 4 7 7

запускаем LIS для блока:

B1 B2 B3 B4 key π
1 4 7 1 1
1 2 7 2 2
1 2 7 8 8 12
1 2 6 8 6 6
1 2 5 8 5 5

В результате получаем:

B:{1,2,5,8}

merged:{1,2,3,4,5,6,8,12}


Третий блок

Восстанавливаем элементы B:{1,2,5,8} из merged:{1,2,3,4,5,6,8,12}: {1,2,5,12}.

Сливаем Cs3 и восстановленные элементы из B:

B
1 2 5 12
Cs3
7 11
merged
1 2 5 7 11 12
1 2 3 4 5 6
третий блок
π 7 11
key 4 5
Cортированный
π 7 11
key 4 5

Обновление старых ключей:

B1 B2 B3 B4 π
1 1
1 2 2
1 2 3 3
1 2 3 6 6

запускаем LIS для блока:

B1 B2 B3 B4 B5 key π
1 2 3 4 4 7
1 2 3 4 5 5 11

Результат завершения алгоритма:

B:{1,2,3,4,5}

merged:{1,2,5,7,11,12}

Получаем, что длина НВП — 5, и НВП оканчивается на merged[5]=11.


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

predecessor
1 2 3 4 5 6 7 8 9 10 11 12
1 3 2 2 5 4 3 7 8

Начинаем восстановление с merged[5]=11:

обратный порядок 11 7 5 2 1
НВП 1 2 5 7 11

Оценка времени работы

Так как размер списка merged не больше 2m, а количество блоков всего n/m. То общее количество присваиваний новых ключей элементам последовательности, также как и количество операций слияния списков, не больше 2cmnm=O(n), где c — некоторая константа. Каждая операция с приоритетной очередью требует O(loglogm) времени, так как элементы в B не больше 2m.

Рассмотрим последовательность {m0, m1, m2, }, где mi+1=mlogmii=2log2mi, m0 — некоторое значение, меньшее k.

Будем по порядку для элементов этой последовательности запускать алгоритм, представленный выше. Если размер очереди B становится больше mi, то условие mk перестает выполняться, тогда останавливаем алгоритм и переходим к следующему значению mi+1. Когда найдётся первое mj:mjk, то алгоритм успешно завершится.

Таким образом, время работы запущенного алгоритма — O(nloglogmi) для 0ij.

Заметим, что

loglogmi+1=loglog2log2mi=loglog2mi=2loglogmi.

loglogmj=2jiloglogmi

Общее время работы алгоритма по всем miO(n(ji=02(i1))loglogmi).

Обратим внимание, что mi<klogk, так как в противном случае mi1>k, что противоречит тому, что mi — первый из тех, которые больше k. Следовательно, [math]\operatorname{log}\operatorname{log}m_i \lt 2\operatorname{log}\operatorname{log}k \[/math].

Тогда алгоритм также работает за время O(nloglogk).

См. также

Источники информации