Мажорирующий элемент — различия между версиями
Никита (обсуждение | вклад) (→Доказательство) |
м (rollbackEdits.php mass rollback) |
||
(не показано 18 промежуточных версий 5 участников) | |||
Строка 1: | Строка 1: | ||
− | = | + | {{Задача |
− | + | |definition = Требуется в массиве длиной <tex>N</tex> найти элемент, встречающийся более <tex>N/2</tex> раз. Гарантируется, что такой элемент существует. | |
− | Требуется в массиве длиной <tex>N</tex> найти элемент, встречающийся более <tex>N/2</tex> раз. Гарантируется, что такой элемент существует. | + | }} |
== Решение за O(N) == | == Решение за O(N) == | ||
Строка 7: | Строка 7: | ||
Алгоритм можно представить следующим образом: пусть на вечеринке собрались <tex>N</tex> людей, и каждому из них соответствует один элемент из массива. Когда встречаются двое с разными элементами, то они образуют пару и садятся. В конце концов останутся стоять только люди с одинаковыми элементами. Это и есть искомый элемент. | Алгоритм можно представить следующим образом: пусть на вечеринке собрались <tex>N</tex> людей, и каждому из них соответствует один элемент из массива. Когда встречаются двое с разными элементами, то они образуют пару и садятся. В конце концов останутся стоять только люди с одинаковыми элементами. Это и есть искомый элемент. | ||
− | Будем идти по массиву и запоминать элемент, для которого еще не нашлось пары. При встрече такого же элемента увеличиваем счетчик "без пары", иначе | + | Будем идти по массиву и запоминать элемент, для которого еще не нашлось пары. При встрече такого же элемента увеличиваем счетчик "без пары", иначе — уменьшаем. Если все элементы уже имеют пару, то говорим, что у текущего элемента пары нет. |
=== Псевдокод === | === Псевдокод === | ||
− | findMajorityElement(a | + | '''function''' findMajorityElement(a: '''int[N]'''): '''int''' |
− | count = 0 // количество людей, оставшихся стоять | + | count = 0 ''<font color="green">// количество людей, оставшихся стоять</font>'' |
candidate = <tex>\varnothing</tex> | candidate = <tex>\varnothing</tex> | ||
− | |||
'''for''' i = 0 '''to''' N - 1 | '''for''' i = 0 '''to''' N - 1 | ||
− | '''if''' count == 0 // никто не стоит | + | '''if''' count == 0 ''<font color="green">// никто не стоит</font>'' |
− | candidate = a[i] // встанет текущий элемент | + | candidate = a[i] ''<font color="green">// встанет текущий элемент</font>'' |
− | count++ // увеличим количество стоящих | + | count++ ''<font color="green">// увеличим количество стоящих</font>'' |
− | '''else''' // кто-то стоит | + | '''else''' ''<font color="green">// кто-то стоит</font>'' |
− | '''if''' a[i] == candidate // стоит такой же элемент | + | '''if''' a[i] == candidate ''<font color="green">// стоит такой же элемент</font>'' |
− | count++ // увеличим количество стоящих | + | count++ ''<font color="green">// увеличим количество стоящих</font>'' |
− | '''else''' // стоит другой элемент => подобрали пару | + | '''else''' ''<font color="green"> // стоит другой элемент => подобрали пару</font>'' |
− | count-- // уменьшим количество стоящих | + | count-- ''<font color="green">// уменьшим количество стоящих</font>'' |
− | + | '''return''' candidate | |
− | |||
=== Доказательство === | === Доказательство === | ||
− | На <tex>i | + | На <tex>i</tex>''-ом'' шаге выполняется следующий инвариант: если <tex>\mathtt{count} > 0</tex>, то <tex>\mathtt{candidate}</tex> — мажорирующий элемент на подмассиве <tex>a[0\ldots i]</tex>, либо мажорирующего элемента на данном подмассиве не существует; если <tex>\mathtt{count} = 0</tex>, то мажорирующего элемента на данном подмассиве не существует. Нам гарантируется существование мажорирующего элемента, значит на <tex>N</tex>''-ом'' шаге <tex>\mathtt{count} \ne 0</tex> и <tex>\mathtt{candidate}</tex> содержит мажорирующий элемент. Покажем, что данный инвариант всегда выполняется. |
− | Пусть данный инвариант выполняется на <tex>k | + | Пусть данный инвариант выполняется на <tex>k</tex>''-ом'' шаге. Тогда на <tex>(k+1)</tex>''-ом'' шаге возможны 3 варианта: |
− | # <tex>count = 0</tex><p>Очевидно, что на подмассиве <tex>a[0 | + | # <tex>\mathtt{count} = 0</tex><p>Очевидно, что на подмассиве <tex>a[0\ldots k]</tex> мажорирующего элемента не существует, так как все элементы разбились на пары. Тогда только <tex>a[k+1]</tex> может быть мажорирующим элементом.</p> |
− | # <tex>count > 0</tex> и <tex>a[k+1] = candidate</tex><p>Если на подмассиве <tex>a[0 | + | # <tex>\mathtt{count} > 0</tex> и <tex>a[k+1] = \mathtt{candidate}</tex><p>Если на подмассиве <tex>a[0\ldots k]</tex> существует мажорирующий элемент, то он находится в <tex>\mathtt{candidate}</tex>. Тогда, в силу равенства <tex>a[k+1]</tex> и <tex>\mathtt{candidate}</tex>, если на подмассиве <tex>a[0\ldots k+1]</tex> существует мажорирующий элемент, то он тоже будет равен <tex>\mathtt{candidate}</tex>.</p> |
− | # <tex>count > 0</tex> и <tex>a[k+1] | + | # <tex>\mathtt{count} > 0</tex> и <tex>a[k+1] \ne \mathtt{candidate}</tex><p>Если на подмассиве <tex>a[0\ldots k]</tex> существует мажорирующий элемент, то он находится в <tex>\mathtt{candidate}</tex>. Тогда, в силу неравенства <tex>a[k+1]</tex> и <tex>\mathtt{candidate}</tex>, образовалась новая пара. Если <tex>\mathtt{count}</tex> не станет равным нулю, то, опять же, мажорирующим элементом может быть только <tex>\mathtt{candidate}</tex>, так как для всех остальных мы нашли пару, а, значит, встречаются они не более <tex>N/2</tex> раз.</p> |
Строка 40: | Строка 38: | ||
== Обобщение на случай поиска элемента, встречающегося N/K раз == | == Обобщение на случай поиска элемента, встречающегося N/K раз == | ||
− | Будем пользоваться той же идеей, что и в предыдущем пункте. До этого мы | + | Будем пользоваться той же идеей, что и в предыдущем пункте. До этого мы сажали людей парами, а теперь будем сажать группами из <tex>K</tex> человек. В итоге, если искомые нами элементы есть, то они останутся стоять. |
− | Будем идти по массиву и хранить элементы, которые еще не сели. При встрече элемента, который уже есть среди стоящих, увеличиваем счетчик данного элемента на <tex>1</tex>. В противном случае смотрим, можем ли мы посадить группу и, либо ее | + | Будем идти по массиву и хранить элементы, которые еще не сели. При встрече элемента, который уже есть среди стоящих, увеличиваем счетчик данного элемента на <tex>1</tex>. В противном случае смотрим, можем ли мы посадить группу и, либо ее сажаем, либо добавляем текущий элемент к стоящим. В конце требуется сделать проверку, что оставшиеся стоять элементы встречаются <tex>N/K</tex> раз. |
=== Псевдокод === | === Псевдокод === | ||
− | findMajorityElement(a | + | '''function''' findMajorityElement(a: '''int[N]''', K: '''int'''): '''int[N]''' |
− | // candidates | + | ''<font color="green">//candidates — словарь, где ключ — стоящий элемент,</font>'' |
− | // значение | + | ''<font color="green">//значение — количество таких стоящих элементов</font>'' |
'''for''' i = 0 '''to''' N - 1 | '''for''' i = 0 '''to''' N - 1 | ||
− | '''if''' candidates.containsKey(a[i]) // нашли стоящий элемент | + | '''if''' candidates.containsKey(a[i]) ''<font color="green">// нашли стоящий элемент</font>'' |
− | candidates[a[i]]++ // увеличим счетчик | + | candidates[a[i]]++ ''<font color="green">// увеличим счетчик</font>'' |
'''else''' | '''else''' | ||
− | '''if''' candidates.size | + | '''if''' candidates.size < K - 1 ''<font color="green">// полная группа не образована</font>'' |
− | candidates[a[i]] = 1 // добавим элемент в группу | + | candidates[a[i]] = 1 ''<font color="green">// добавим элемент в группу</font>'' |
− | '''else''' // образовалась полная группа | + | '''else''' ''<font color="green">// образовалась полная группа</font>'' |
− | '''for''' element '''in''' candidates // посадим группу | + | '''for''' element '''in''' candidates ''<font color="green">// посадим группу</font>'' |
candidates[element]-- | candidates[element]-- | ||
− | '''if''' candidates[element] == 0 // если никто с таким элементом не стоит | + | '''if''' candidates[element] == 0 ''<font color="green">// если никто с таким элементом не стоит</font>'' |
− | candidates.remove(element) // удалим этот элемент | + | candidates.remove(element) ''<font color="green">// удалим этот элемент</font>'' |
− | '''for''' candidate '''in''' candidates // обнулим счетчик | + | '''for''' candidate '''in''' candidates ''<font color="green">// обнулим счетчик</font>'' |
candidates[candidate] = 0 | candidates[candidate] = 0 | ||
− | '''for''' i = 0 '''to''' N - 1 // посчитаем, сколько раз встречаются элементы | + | '''for''' i = 0 '''to''' N - 1 ''<font color="green">// посчитаем, сколько раз встречаются элементы</font>'' |
'''if''' candidates.containsKey(a[i]) | '''if''' candidates.containsKey(a[i]) | ||
candidates[a[i]]++ | candidates[a[i]]++ | ||
− | '''for''' candidate '''in''' candidates // проверим, встречается ли элемент N / K раз | + | '''for''' candidate '''in''' candidates ''<font color="green">// проверим, встречается ли элемент N / K раз</font>'' |
'''if''' candidates[candidate] > N / K | '''if''' candidates[candidate] > N / K | ||
elements.add(candidate) | elements.add(candidate) | ||
Строка 75: | Строка 73: | ||
=== Доказательство === | === Доказательство === | ||
− | На <tex>i</tex>''-ом'' шаге поддерживается следующий инвариант: если на подмассиве <tex>a[0 | + | На <tex>i</tex>''-ом'' шаге поддерживается следующий инвариант: если на подмассиве <tex>a[0\ldots i]</tex> существуют элементы, которые встречаются больше, чем <tex>i / K</tex> раз, то все они содержатся в <tex>\mathtt{candidates}</tex> и размер <tex>\mathtt{candidates}</tex> не превышает <tex>K</tex>. Тогда на <tex>N</tex>''-ом'' шаге <tex>\mathtt{candidates}</tex> будет содержать все возможные элементы, встречающиеся более, чем <tex>N / K</tex> раз, остается только выполнить проверку. Покажем, что данный инвариант всегда выполняется: |
Пусть данный инвариант выполняется на <tex>k</tex>''-ом'' шаге. Тогда на <tex>(k+1)</tex>''-ом'' шаге возможны <tex>3</tex> варианта: | Пусть данный инвариант выполняется на <tex>k</tex>''-ом'' шаге. Тогда на <tex>(k+1)</tex>''-ом'' шаге возможны <tex>3</tex> варианта: | ||
− | #<tex>a[k + 1] \in candidates</tex><p>Размер <tex>candidates</tex> не увеличен, текущий элемент останется стоять. Инвариант выполняется.</p> | + | #<tex>a[k + 1] \in \mathtt{candidates}</tex><p>Размер <tex>\mathtt{candidates}</tex> не увеличен, текущий элемент останется стоять. Инвариант выполняется.</p> |
− | #<tex>a[k + 1] \notin candidates</tex> и <tex>candidates | + | #<tex>a[k + 1] \notin \mathtt{candidates}</tex> и <tex>|\mathtt{candidates}| < K - 1</tex><p>Размер <tex>\mathtt{candidates}</tex> останется меньше <tex>K</tex>, текущей элемент останется стоять. Инвариант выполняется.</p> |
− | #<tex>a[k + 1] \notin candidates</tex> и <tex>candidates | + | #<tex>a[k + 1] \notin \mathtt{candidates}</tex> и <tex>|\mathtt{candidates}| = K - 1</tex><p>Размер <tex>\mathtt{candidates}</tex> на данном шаге может только уменьшиться. Сажаем группу. Если какой-то элемент был удален на текущем шаге, то, значит, он встречался не более, чем <tex>(k + 1) / K</tex> раз, т.к. мы могли успеть посадить максимум <tex>(k + 1) / K</tex> групп. Тогда удаление данного элемента из <tex>\mathtt{candidates}</tex> ни к чему плохому не приведет. Инвариант выполняется.</p> |
Рассмотрим среднее количество обращений к словарю за одну итерацию. На каждой итерации происходит не более <tex>1</tex> увеличения счетчика. Тогда, за все время, происходит не более <tex>N</tex> увеличений. Так как количество уменьшений счетчика не может превышать количество увеличений, то всего происходит не более <tex>2N</tex> обращений к словарю. Следовательно, сложность одной итерации составляет <tex>O(1)</tex>. | Рассмотрим среднее количество обращений к словарю за одну итерацию. На каждой итерации происходит не более <tex>1</tex> увеличения счетчика. Тогда, за все время, происходит не более <tex>N</tex> увеличений. Так как количество уменьшений счетчика не может превышать количество увеличений, то всего происходит не более <tex>2N</tex> обращений к словарю. Следовательно, сложность одной итерации составляет <tex>O(1)</tex>. | ||
Строка 93: | Строка 91: | ||
− | findMajorityElement(a | + | '''function''' findMajorityElement(a: '''int[N]''', K: '''int'''): '''int''' |
− | '''while''' true | + | '''while''' ''true'' |
candidate = a[random(N)] | candidate = a[random(N)] | ||
count = 0 | count = 0 | ||
Строка 107: | Строка 105: | ||
На каждом шаге мы берем случайный элемент. Проверка на мажорируемость выполняется за <tex>O(N)</tex>. Вероятность, что мы выбрали элемент "удачно" составляет <tex>1 / K</tex>. Тогда, в среднем, мы будем делать <tex>K</tex> проверок. Итоговая сложность <tex>O(N \cdot K)</tex>. | На каждом шаге мы берем случайный элемент. Проверка на мажорируемость выполняется за <tex>O(N)</tex>. Вероятность, что мы выбрали элемент "удачно" составляет <tex>1 / K</tex>. Тогда, в среднем, мы будем делать <tex>K</tex> проверок. Итоговая сложность <tex>O(N \cdot K)</tex>. | ||
− | == Источники == | + | == Источники информации == |
− | * [http://habrahabr.ru/post/167177/ Habrahabr | + | * [http://habrahabr.ru/post/167177/ Habrahabr — Поиск часто встречающихся элементов в массиве] |
− | * [http://algolist.ncstu.ru/olimp/poi_sol.php#a15 Algolist | + | * [http://algolist.ncstu.ru/olimp/poi_sol.php#a15 Algolist — Решение задачи 15] |
[[Категория: Дискретная математика и алгоритмы]] | [[Категория: Дискретная математика и алгоритмы]] | ||
[[Категория: Амортизационный анализ]] | [[Категория: Амортизационный анализ]] |
Текущая версия на 19:05, 4 сентября 2022
Задача: |
Требуется в массиве длиной | найти элемент, встречающийся более раз. Гарантируется, что такой элемент существует.
Содержание
Решение за O(N)
Алгоритм можно представить следующим образом: пусть на вечеринке собрались
людей, и каждому из них соответствует один элемент из массива. Когда встречаются двое с разными элементами, то они образуют пару и садятся. В конце концов останутся стоять только люди с одинаковыми элементами. Это и есть искомый элемент.Будем идти по массиву и запоминать элемент, для которого еще не нашлось пары. При встрече такого же элемента увеличиваем счетчик "без пары", иначе — уменьшаем. Если все элементы уже имеют пару, то говорим, что у текущего элемента пары нет.
Псевдокод
function findMajorityElement(a: int[N]): int
count = 0 // количество людей, оставшихся стоять
candidate =
for i = 0 to N - 1
if count == 0 // никто не стоит
candidate = a[i] // встанет текущий элемент
count++ // увеличим количество стоящих
else // кто-то стоит
if a[i] == candidate // стоит такой же элемент
count++ // увеличим количество стоящих
else // стоит другой элемент => подобрали пару
count-- // уменьшим количество стоящих
return candidate
Доказательство
На
-ом шаге выполняется следующий инвариант: если , то — мажорирующий элемент на подмассиве , либо мажорирующего элемента на данном подмассиве не существует; если , то мажорирующего элемента на данном подмассиве не существует. Нам гарантируется существование мажорирующего элемента, значит на -ом шаге и содержит мажорирующий элемент. Покажем, что данный инвариант всегда выполняется.Пусть данный инвариант выполняется на
-ом шаге. Тогда на -ом шаге возможны 3 варианта:-
Очевидно, что на подмассиве
мажорирующего элемента не существует, так как все элементы разбились на пары. Тогда только может быть мажорирующим элементом. -
Если на подмассиве
существует мажорирующий элемент, то он находится в . Тогда, в силу равенства и , если на подмассиве существует мажорирующий элемент, то он тоже будет равен .
и -
Если на подмассиве
существует мажорирующий элемент, то он находится в . Тогда, в силу неравенства и , образовалась новая пара. Если не станет равным нулю, то, опять же, мажорирующим элементом может быть только , так как для всех остальных мы нашли пару, а, значит, встречаются они не более раз. и
Всего происходит итераций, каждая из которых обрабатывается за . Итоговая асимптотика .
Обобщение на случай поиска элемента, встречающегося N/K раз
Будем пользоваться той же идеей, что и в предыдущем пункте. До этого мы сажали людей парами, а теперь будем сажать группами из
человек. В итоге, если искомые нами элементы есть, то они останутся стоять.Будем идти по массиву и хранить элементы, которые еще не сели. При встрече элемента, который уже есть среди стоящих, увеличиваем счетчик данного элемента на
. В противном случае смотрим, можем ли мы посадить группу и, либо ее сажаем, либо добавляем текущий элемент к стоящим. В конце требуется сделать проверку, что оставшиеся стоять элементы встречаются раз.Псевдокод
function findMajorityElement(a: int[N], K: int): int[N] //candidates — словарь, где ключ — стоящий элемент, //значение — количество таких стоящих элементов for i = 0 to N - 1 if candidates.containsKey(a[i]) // нашли стоящий элемент candidates[a[i]]++ // увеличим счетчик else if candidates.size < K - 1 // полная группа не образована candidates[a[i]] = 1 // добавим элемент в группу else // образовалась полная группа for element in candidates // посадим группу candidates[element]-- if candidates[element] == 0 // если никто с таким элементом не стоит candidates.remove(element) // удалим этот элемент for candidate in candidates // обнулим счетчик candidates[candidate] = 0 for i = 0 to N - 1 // посчитаем, сколько раз встречаются элементы if candidates.containsKey(a[i]) candidates[a[i]]++ for candidate in candidates // проверим, встречается ли элемент N / K раз if candidates[candidate] > N / K elements.add(candidate) return elements
Доказательство
На
-ом шаге поддерживается следующий инвариант: если на подмассиве существуют элементы, которые встречаются больше, чем раз, то все они содержатся в и размер не превышает . Тогда на -ом шаге будет содержать все возможные элементы, встречающиеся более, чем раз, остается только выполнить проверку. Покажем, что данный инвариант всегда выполняется:Пусть данный инвариант выполняется на
-ом шаге. Тогда на -ом шаге возможны варианта:Размер
не увеличен, текущий элемент останется стоять. Инвариант выполняется.Размер
останется меньше , текущей элемент останется стоять. Инвариант выполняется.
и Размер
на данном шаге может только уменьшиться. Сажаем группу. Если какой-то элемент был удален на текущем шаге, то, значит, он встречался не более, чем раз, т.к. мы могли успеть посадить максимум групп. Тогда удаление данного элемента из ни к чему плохому не приведет. Инвариант выполняется. и
Рассмотрим среднее количество обращений к словарю за одну итерацию. На каждой итерации происходит не более
увеличения счетчика. Тогда, за все время, происходит не более увеличений. Так как количество уменьшений счетчика не может превышать количество увеличений, то всего происходит не более обращений к словарю. Следовательно, сложность одной итерации составляет .Получается, при реализации словаря на основе хэш-таблиц, каждая итерация в среднем выполняется за
. Итоговая сложность с дополнительной памятью .Альтернативное решение
Выберем случайный элемент в массиве и проверим, встречается ли он больше, чем
раз. Будем делать так, пока не найдем подходящий элемент. Утверждается, что данный алгоритм в среднем работает заПсевдокод
function findMajorityElement(a: int[N], K: int): int while true candidate = a[random(N)] count = 0 for i = 0 to N - 1 if a[i] == candidate count++ if count > N / K return candidate
Доказательство
На каждом шаге мы берем случайный элемент. Проверка на мажорируемость выполняется за
. Вероятность, что мы выбрали элемент "удачно" составляет . Тогда, в среднем, мы будем делать проверок. Итоговая сложность .