Алгоритм Мо — различия между версиями
Noobgam (обсуждение | вклад) |
Noobgam (обсуждение | вклад) |
||
Строка 1: | Строка 1: | ||
− | '''Алгоритм Мо''' (англ. ''Mo's algorithm'') — применяется для решения задач, в которых требуется отвечать на запросы <tex> | + | '''Алгоритм Мо''' (англ. ''Mo's algorithm'') — применяется для решения задач, в которых требуется отвечать на запросы <tex>arr[l \ldots r]</tex> на массиве |
− | ''без'' изменения элементов в оффлайн за время <tex>O(Q \cdot \log{Q} + (N + Q) \cdot \sqrt{N})</tex>, где <tex>Q</tex> - количество запросов, | + | ''без'' изменения элементов в оффлайн за время <tex>O(Q \cdot \log{Q} + (N + Q) \cdot \sqrt{N})</tex>, где <tex>Q</tex> {{---}} количество запросов, |
− | а <tex>N</tex> - количество элементов в массиве. Характерными примерами задач на этот алгоритм являются: нахождение моды на отрезке (число, которое встречается больше всех остальных), | + | а <tex>N</tex> {{---}} количество элементов в массиве. Характерными примерами задач на этот алгоритм являются: нахождение моды на отрезке (число, которое встречается больше всех остальных), |
вычисление количества инверсий на отрезке. | вычисление количества инверсий на отрезке. | ||
==Алгоритм== | ==Алгоритм== | ||
− | В каждый момент времени поддерживаем структуру данных, в которой хранится | + | В каждый момент времени поддерживаем структуру данных, в которой хранится непрерывный отрезок <tex>[a \ldots b]</tex> исходного массива (будем называть его рабочим отрезком), |
которая поддерживает следующие операции: | которая поддерживает следующие операции: | ||
− | * <tex> | + | * <tex>\mathtt{addLeft(a-1)}</tex>, <tex>\mathtt{addRight(b+1)}</tex> {{---}} операции, которые позволяют добавить элемент слева и справа соответственно. |
− | * <tex> | + | * <tex>\mathtt{delLeft(a)}</tex>, <tex>\mathtt{delRight(b)}</tex> {{---}} операции, которые позволяют удалить элемент слева и справа соответственно. |
− | * <tex> | + | * <tex>\mathtt{answer}</tex> {{---}} операция, которая позволяет получить ответ на запрос, если бы его границами был рабочий отрезок. |
− | Изначально в качестве рабочего отрезка можно взять любой отрезок, | + | Изначально в качестве рабочего отрезка можно взять любой отрезок. Для удобства чтения будем считать изначальным отрезок <tex>[1;1)</tex>, то есть <tex>a = 1</tex>, <tex>b = 0</tex>, фактически {{---}} пустой отрезок. |
− | Запишем все запросы в массив, некоторым образом их отсортируем и будем их обрабатывать в том порядке, в котором они будут лежать в массиве после сортировки. | + | Запишем все запросы в массив, некоторым образом их отсортируем и будем их обрабатывать в том порядке, в котором они будут лежать в массиве после [[Сортировки | сортировки]]. |
Допустим, что текущий рабочий отрезок — <tex>[a \ldots b]</tex>, а первый необработанный запрос — <tex>[l_i, r_i]</tex> тогда сначала расширим наш отрезок, | Допустим, что текущий рабочий отрезок — <tex>[a \ldots b]</tex>, а первый необработанный запрос — <tex>[l_i, r_i]</tex> тогда сначала расширим наш отрезок, | ||
− | используя только операции <tex> | + | используя только операции <tex>\mathtt{addLeft}</tex>, <tex>\mathtt{addRight}</tex> до отрезка <tex>[l \ldots r]</tex>, |
− | где <tex>l = \min(a, l_i)</tex>, а <tex>r = \max(b, r_i)</tex>, а затем удалим лишние элементы при помощи операций <tex> | + | где <tex>l = \min(a, l_i)</tex>, а <tex>r = \max(b, r_i)</tex>, а затем удалим лишние элементы при помощи операций <tex>\mathtt{delLeft}</tex>, <tex>\mathtt{delRight}</tex>, чтобы получить отрезок <tex>[l_i \ldots r_i]</tex>, после чего вызовем <tex>answer</tex> и запомним ответ для этого запроса. |
Теперь разберём поподробнее, как именно следует сортировать запросы для достижения вышеназванной асимптотики по времени. | Теперь разберём поподробнее, как именно следует сортировать запросы для достижения вышеназванной асимптотики по времени. | ||
− | + | Разделим все запросы на блоки размера <tex>K</tex> по левой границе: те запросы, для которых <tex>1 \leqslant l_i \leqslant K</tex> {{---}} попадают в первую группу, | |
− | те запросы, для которых <tex>K + 1 \leqslant l_i \leqslant 2 \cdot K</tex> - во вторую, <tex>2 \cdot K + 1 \leqslant l_i \leqslant 3 \cdot K</tex> - в третью, и так далее. Будем рассматривать все группы запросов независимо друг от друга. Если внутри каждой группы отсортировать запросы по правой границе, будет нетрудно заметить, что для всей группы суммарно будет выполнено не больше чем <tex>3 \cdot N + Q_i \cdot K</tex> операций <tex> | + | те запросы, для которых <tex>K + 1 \leqslant l_i \leqslant 2 \cdot K</tex> {{---}} во вторую, <tex>2 \cdot K + 1 \leqslant l_i \leqslant 3 \cdot K</tex> {{---}} в третью, и так далее. Будем рассматривать все группы запросов независимо друг от друга. Если внутри каждой группы отсортировать запросы по правой границе, будет нетрудно заметить, что для всей группы суммарно будет выполнено не больше чем <tex>3 \cdot N + Q_i \cdot K</tex> операций <tex>add</tex> и <tex>del</tex> где <tex>Q_i</tex> {{---}} количество запросов, принадлежащих группе под номером <tex>i</tex>. |
− | Для доказательства этого | + | Для доказательства этого рассмотрим отдельно количество сделанных операций каждого из четырёх типов: |
− | * Изначально, до обработки группы, рабочий отрезок был <tex>[a \ldots b]</tex>, для обработки первого запроса может потребоваться <tex>2 \cdot N</tex> операций <tex> | + | * Изначально, до обработки группы, рабочий отрезок был <tex>[a \ldots b]</tex>, для обработки первого запроса может потребоваться <tex>2 \cdot N</tex> операций <tex>add</tex>, <tex>del</tex> |
− | * <tex> | + | * <tex>\mathtt{delRight}</tex> не произойдёт ни разу, т.к. рабочий отрезок будет только расширяться в сторону правого конца |
− | * <tex> | + | * <tex>\mathtt{addRight}</tex> произойдёт суммарно не больше чем <tex>N</tex> раз, так как минимальная правая граница {{---}} <tex>1</tex>, а максимальная {{---}} <tex>N</tex> |
− | * Для оставшихся двух операций рассмотрим два последовательных запроса <tex>[l_i \ldots r_i]</tex>, <tex>[l_j \ldots r_j]</tex>. Нетрудно заметить, что так как отрезки принадлежат одной группе, то <tex>|l_i - l_j| < K</tex>, следовательно, количество операций <tex> | + | * Для оставшихся двух операций рассмотрим два последовательных запроса <tex>[l_i \ldots r_i]</tex>, <tex>[l_j \ldots r_j]</tex>. Нетрудно заметить, что так как отрезки принадлежат одной группе, то <tex>|l_i - l_j| < K</tex>, следовательно, количество операций <tex>\mathtt{addLeft}</tex> или <tex>delLeft</tex> также не будет превосходить <tex>K</tex> |
Таким образом, нетрудно видеть, все группы будут обработаны за время <tex>O(\dfrac{N^2}{K} + K \cdot Q)</tex>. | Таким образом, нетрудно видеть, все группы будут обработаны за время <tex>O(\dfrac{N^2}{K} + K \cdot Q)</tex>. | ||
− | При выборе <tex>K = \sqrt{N}</tex> с учётом сортировки по правой границе получается асимптотика времени <tex>O(Q \log Q + (N + Q) \cdot \sqrt N)</tex> | + | При выборе <tex>K = \sqrt{N}</tex> с учётом сортировки по правой границе получается асимптотика времени <tex>O(Q \cdot \log Q + (N + Q) \cdot \sqrt N)</tex> |
==Реализация== | ==Реализация== | ||
Строка 40: | Строка 40: | ||
'''int''' K = sqrt(N) | '''int''' K = sqrt(N) | ||
− | '''bool''' | + | '''bool''' isLess('''Query''' a, '''Query''' b): |
'''if''' (a.l / K != b.l / K): | '''if''' (a.l / K != b.l / K): | ||
'''return''' a.l < b.l | '''return''' a.l < b.l | ||
Строка 46: | Строка 46: | ||
'''function''' process('''Query'''[Q] q): | '''function''' process('''Query'''[Q] q): | ||
− | sort(q, | + | sort(q, isLess) <font color=green>// сортируем запросы, используя функцию '''isLess''' как оператор сравнения</font> |
− | '''int''' a = 1, b = 0 <font color=green>//создаём пустой рабочий отрезок</font> | + | '''int''' a = 1, b = 0 <font color=green>// создаём пустой рабочий отрезок</font> |
'''for''' i = 0 to Q - 1: | '''for''' i = 0 to Q - 1: | ||
− | '''while''' | + | '''while''' a > q[i].l: |
− | + | addLeft(a - 1) | |
a -= 1 | a -= 1 | ||
− | '''while''' | + | '''while''' b < q[i].r: |
− | + | addRight(b + 1) | |
b += 1 | b += 1 | ||
− | '''while''' | + | '''while''' a < q[i].l: |
− | + | delLeft(a) | |
a += 1 | a += 1 | ||
− | '''while''' | + | '''while''' b > q[i].r: |
− | + | delRight(b) | |
b -= 1 | b -= 1 | ||
− | result[q[i].id] = | + | result[q[i].id] = answer() |
Рассмотрим для наглядности решение задачи нахождения моды на отрезке: | Рассмотрим для наглядности решение задачи нахождения моды на отрезке: | ||
− | Будем использовать код описанный выше, осталось только описать операции <tex> | + | Будем использовать код описанный выше, осталось только описать операции <tex>\mathtt{addLeft}</tex>, <tex>\mathtt{addRight}</tex>, <tex>\mathtt{delLeft}</tex>, <tex>\mathtt{delRight}</tex>. |
Так как в данной задаче порядок чисел на отрезке не важен, важно лишь количество вхождений каждого, то реализация отдельных функций для добавления слева и справа нам не потребуется. | Так как в данной задаче порядок чисел на отрезке не важен, важно лишь количество вхождений каждого, то реализация отдельных функций для добавления слева и справа нам не потребуется. | ||
− | Для простоты будем считать, что все числа '''не превышают <tex>N</tex>''', тогда будем хранить массив <tex>cnt[N + 1]</tex>, где <tex>cnt[value]</tex> - количество вхождений числа <tex>value</tex> в рабочем отрезке. Будем помимо этого массива хранить отсортированное множество <tex>current</tex>, в котором будут содержаться все пары вида <tex>\langle cnt[value], value \rangle</tex>, для ненулевых <tex>cnt[value]</tex>. Тогда операции будут иметь следующий вид: | + | Для простоты будем считать, что все числа '''не превышают <tex>N</tex>''', тогда будем хранить массив <tex>cnt[N + 1]</tex>, где <tex>cnt[value]</tex> - количество вхождений числа <tex>value</tex> в рабочем отрезке. Будем помимо этого массива хранить отсортированное множество <tex>current</tex>, в котором будут содержаться все пары вида <tex>\langle cnt[value], value \rangle</tex>, для ненулевых <tex>cnt[value]</tex>. Реализовать его можно, например, используя [[Красно-черное_дерево | Красно-черное дерево]] Тогда операции будут иметь следующий вид: |
− | '''function''' | + | '''function''' add('''int''' index): |
− | '''int''' value = | + | '''int''' value = arr[index] |
'''if''' (cnt[value] != 0): | '''if''' (cnt[value] != 0): | ||
current.erase((cnt[value], value)) | current.erase((cnt[value], value)) | ||
Строка 76: | Строка 76: | ||
current.insert((cnt[value], value)) | current.insert((cnt[value], value)) | ||
− | + | '''function''' del('''int''' index): | |
− | '''int''' value = | + | '''int''' value = arr[index] |
current.erase((cnt[value], value)) | current.erase((cnt[value], value)) | ||
cnt[a[index]] -= 1 | cnt[a[index]] -= 1 | ||
'''if''' (cnt[value] != 0): | '''if''' (cnt[value] != 0): | ||
current.insert((cnt[value], value)) | current.insert((cnt[value], value)) | ||
− | '''function''' | + | |
− | return | + | '''function''' answer(): '''int''' |
+ | '''return''' current.max.second <font color=green>// находим максимальную пару в множестве</font> | ||
+ | |||
+ | Итоговая асимптотика решения: <tex>O(Q \cdot \log Q + (N + Q) \cdot \sqrt{N} \cdot \log N)</tex> | ||
+ | |||
+ | == См. также == | ||
+ | * [[Статистики_на_отрезках._Корневая_эвристика | Корневая эвристика]] | ||
+ | * [[Решение_RMQ_с_помощью_разреженной_таблицы#.D0.A0.D0.B0.D0.B7.D1.80.D0.B5.D0.B6.D0.B5.D0.BD.D0.BD.D0.B0.D1.8F_.D1.82.D0.B0.D0.B1.D0.BB.D0.B8.D1.86.D0.B0 | Разреженная таблица]] | ||
+ | |||
+ | ==Источники информации== | ||
+ | * [https://www.hackerearth.com/practice/notes/mos-algorithm/ Mo's algorithm] | ||
− | + | [[Категория:Сортировки]] |
Версия 22:03, 15 января 2017
Алгоритм Мо (англ. Mo's algorithm) — применяется для решения задач, в которых требуется отвечать на запросы
на массиве без изменения элементов в оффлайн за время , где — количество запросов, а — количество элементов в массиве. Характерными примерами задач на этот алгоритм являются: нахождение моды на отрезке (число, которое встречается больше всех остальных), вычисление количества инверсий на отрезке.Содержание
Алгоритм
В каждый момент времени поддерживаем структуру данных, в которой хранится непрерывный отрезок
исходного массива (будем называть его рабочим отрезком), которая поддерживает следующие операции:- , — операции, которые позволяют добавить элемент слева и справа соответственно.
- , — операции, которые позволяют удалить элемент слева и справа соответственно.
- — операция, которая позволяет получить ответ на запрос, если бы его границами был рабочий отрезок.
Изначально в качестве рабочего отрезка можно взять любой отрезок. Для удобства чтения будем считать изначальным отрезок
, то есть , , фактически — пустой отрезок.Запишем все запросы в массив, некоторым образом их отсортируем и будем их обрабатывать в том порядке, в котором они будут лежать в массиве после сортировки.
Допустим, что текущий рабочий отрезок —
, а первый необработанный запрос — тогда сначала расширим наш отрезок, используя только операции , до отрезка , где , а , а затем удалим лишние элементы при помощи операций , , чтобы получить отрезок , после чего вызовем и запомним ответ для этого запроса.Теперь разберём поподробнее, как именно следует сортировать запросы для достижения вышеназванной асимптотики по времени.
Разделим все запросы на блоки размера
по левой границе: те запросы, для которых — попадают в первую группу, те запросы, для которых — во вторую, — в третью, и так далее. Будем рассматривать все группы запросов независимо друг от друга. Если внутри каждой группы отсортировать запросы по правой границе, будет нетрудно заметить, что для всей группы суммарно будет выполнено не больше чем операций и где — количество запросов, принадлежащих группе под номером .Для доказательства этого рассмотрим отдельно количество сделанных операций каждого из четырёх типов:
- Изначально, до обработки группы, рабочий отрезок был , для обработки первого запроса может потребоваться операций ,
- не произойдёт ни разу, т.к. рабочий отрезок будет только расширяться в сторону правого конца
- произойдёт суммарно не больше чем раз, так как минимальная правая граница — , а максимальная —
- Для оставшихся двух операций рассмотрим два последовательных запроса , . Нетрудно заметить, что так как отрезки принадлежат одной группе, то , следовательно, количество операций или также не будет превосходить
Таким образом, нетрудно видеть, все группы будут обработаны за время
.При выборе
с учётом сортировки по правой границе получается асимптотика времениРеализация
struct Query: int l, r, index int K = sqrt(N) bool isLess(Query a, Query b): if (a.l / K != b.l / K): return a.l < b.l return a.r < b.r function process(Query[Q] q): sort(q, isLess) // сортируем запросы, используя функцию isLess как оператор сравнения int a = 1, b = 0 // создаём пустой рабочий отрезок for i = 0 to Q - 1: while a > q[i].l: addLeft(a - 1) a -= 1 while b < q[i].r: addRight(b + 1) b += 1 while a < q[i].l: delLeft(a) a += 1 while b > q[i].r: delRight(b) b -= 1 result[q[i].id] = answer()
Рассмотрим для наглядности решение задачи нахождения моды на отрезке:
Будем использовать код описанный выше, осталось только описать операции
, , , . Так как в данной задаче порядок чисел на отрезке не важен, важно лишь количество вхождений каждого, то реализация отдельных функций для добавления слева и справа нам не потребуется.Для простоты будем считать, что все числа не превышают Красно-черное дерево Тогда операции будут иметь следующий вид:
, тогда будем хранить массив , где - количество вхождений числа в рабочем отрезке. Будем помимо этого массива хранить отсортированное множество , в котором будут содержаться все пары вида , для ненулевых . Реализовать его можно, например, используяfunction add(int index): int value = arr[index] if (cnt[value] != 0): current.erase((cnt[value], value)) cnt[a[index]] += 1 current.insert((cnt[value], value)) function del(int index): int value = arr[index] current.erase((cnt[value], value)) cnt[a[index]] -= 1 if (cnt[value] != 0): current.insert((cnt[value], value)) function answer(): int return current.max.second // находим максимальную пару в множестве
Итоговая асимптотика решения: