1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 1 из 7

Частые элементы за один проход: большинство и сводка Мисры-Гриса

Коротко

Элемент, занимающий больше половины потока, находится одним счётчиком: чужой элемент уменьшает счётчик, свой увеличивает, и в конце в кандидатах остаётся большинство, если оно есть. Сводка Мисры-Гриса обобщает приём на k счётчиков: когда приходит новый ключ, а все счётчики заняты, все они уменьшаются на единицу. Каждое такое уменьшение «съедает» k + 1 элемент потока, поэтому их не больше N / (k + 1), и ни один счётчик не занижен сильнее. Всякий ключ, встретившийся чаще N / (k + 1) раз, гарантированно остаётся в сводке. Алгоритм без случайности, память - k пар, а ошибка только в одну сторону: вниз.

Если ты найдёшь элемент-большинство одним счётчиком, обобщишь приём на k счётчиков и докажешь, что ни один счётчик не занижен больше чем на N/(k + 1), - листай до «Теперь сам».

Как думаешь, можно ли найти большинство одной переменной

В потоке миллиард голосов. Известно, что один из кандидатов получил больше половины. Нужно назвать его, имея память на одно имя и одно число.

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

На что это похоже

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

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

Механизм: взаимное уничтожение и его обобщение

Алгоритм большинства. Хранятся кандидат и счётчик, сначала ноль.

пришёл элемент x:
    счётчик равен нулю:   кандидат = x, счётчик = 1
    x совпал с кандидатом:  счётчик + 1
    не совпал:              счётчик − 1

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

Сводка Мисры-Гриса. То же самое с k счётчиками вместо одного. Сводка - словарь не более чем из k пар «ключ, счётчик».

пришёл ключ x:
    x есть в сводке:           его счётчик + 1
    x нет, пар меньше k:       добавить пару (x, 1)
    x нет, пар уже k:          уменьшить все k счётчиков на 1, нулевые пары удалить;
                               x в сводку не попадает

Третий случай - уход группы: пришедший ключ и по одному голосу каждого из k хранимых ключей уничтожаются разом. Это k + 1 разный элемент потока.

Отсюда оценка. Каждое уменьшение убирает из учёта k + 1 элемент, а всего элементов N, значит уменьшений не больше N / (k + 1). Счётчик ключа отличается от его истинной частоты только на те уменьшения, в которых он участвовал, - либо как хранимый, либо как пришедший и отброшенный:

частота − N/(k + 1)  ≤  счётчик  ≤  частота

Ключ, которого в сводке нет, имеет «счётчик ноль», и для него неравенство тоже верно: его частота не больше N / (k + 1). Значит, всякий ключ с частотой больше N / (k + 1) в сводке есть.

При k = 1 это в точности алгоритм большинства: порог N / 2.

Правило

Сводка Мисры-Гриса с k счётчиками: свой ключ увеличивает счётчик, новый ключ занимает свободное место, а при заполненной сводке все счётчики уменьшаются на единицу и новый ключ отбрасывается. Счётчик ключа не больше его частоты и занижен не больше чем на N / (k + 1). Любой ключ с частотой больше N / (k + 1) присутствует в сводке; обратное неверно, и присутствие проверяют вторым проходом.

Разбор: большинство есть и большинства нет

def kandidat(potok):
    k = -1
    schet = 0
    for x in potok:
        if schet == 0:
            k = x
            schet = 1
        elif x == k:
            schet = schet + 1
        else:
            schet = schet - 1
    return [k, schet]

s_bolshinstvom = [7, 3, 7, 7, 2, 7, 5, 7, 7, 1, 7]
bez_bolshinstva = [7, 3, 7, 2, 7, 5, 1, 4, 6]
print(kandidat(s_bolshinstvom), s_bolshinstvom.count(7), len(s_bolshinstvom))
print(kandidat(bez_bolshinstva), bez_bolshinstva.count(6), len(bez_bolshinstva))
[7, 3] 7 11
[6, 1] 1 9

В первом потоке семёрка встречается 7 раз из 11 - больше половины; алгоритм назвал её, а счётчик 3 здесь равен перевесу: семь своих голосов минус четыре чужих. В общем случае счётчик - число голосов кандидата, оставшихся без пары: чужие голоса могут уничтожать и друг друга, и тогда он больше перевеса.

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

Разбор: девять счётчиков на шесть тысяч элементов

Поток из 6000 элементов: ключ 1 занимает около 20%, ключ 2 - 12%, ключ 3 - 8%, ключ 4 - 5%, остальное размазано по ключам от 5 до 2000. Сводка - 9 счётчиков. В программе уменьшение сделано пересборкой словаря: в новый попадают пары, у которых после вычитания остаётся больше нуля.

import random

def potok_s_tyazhelymi(n):
    potok = []
    for i in range(n):
        u = random.random()
        if u < 0.20:
            potok.append(1)
        elif u < 0.32:
            potok.append(2)
        elif u < 0.40:
            potok.append(3)
        elif u < 0.45:
            potok.append(4)
        else:
            potok.append(random.randint(5, 2000))
    return potok

def misra_gries(potok, k):
    schetchiki = {}
    for x in potok:
        if x in schetchiki:
            schetchiki[x] = schetchiki[x] + 1
        elif len(schetchiki) < k:
            schetchiki[x] = 1
        else:
            ostalis = {}
            for klyuch in schetchiki:
                if schetchiki[klyuch] > 1:
                    ostalis[klyuch] = schetchiki[klyuch] - 1
            schetchiki = ostalis
    return schetchiki

random.seed(61)
potok = potok_s_tyazhelymi(6000)
tochno = {}
for x in potok:
    tochno[x] = tochno.get(x, 0) + 1

k = 9
svodka = misra_gries(potok, k)
print(len(tochno), len(potok) // (k + 1))
for klyuch in sorted(svodka):
    print(klyuch, svodka[klyuch], tochno[klyuch], tochno[klyuch] - svodka[klyuch])
1622 600
1 699 1208 509
2 205 712 507
3 2 461 459
194 1 3 2
1354 1 3 2
1618 1 2 1
1661 1 2 1

Первая строка: в потоке 1622 разных ключа, порог N / (k + 1) = 600. Дальше - содержимое сводки: ключ, счётчик, истинная частота и занижение.

Ключи 1 и 2 с частотами 1208 и 712 выше порога и, как обещано, остались. Их счётчики занижены на 509 и 507 - меньше шестисот. Ключ 3 с частотой 461 ниже порога; он остался в сводке, но мог и не остаться, и счётчик у него всего 2. Ключ 4, встретившийся около трёхсот раз, в сводку не попал. Последние четыре строки - случайные ключи, пришедшие в самом конце потока.

Как читать такую сводку. Счётчик - нижняя граница частоты, а «счётчик плюс 600» - верхняя. Про ключ 1 известно: от 699 до 1299. Про ключ 194: от 1 до 601 - то есть ничего. Сводка сама не отличает тяжёлый ключ от только что пришедшего; отличает порог. Чтобы гарантированно получить все ключи с частотой больше 10% потока, берут k так, чтобы N / (k + 1) было не больше этой доли, а потом вторым проходом считают точные частоты кандидатов из сводки - их всего k.

Граница: сводка работает, когда тяжёлые ключи есть. Если поток ровный и ни один ключ не превышает порога, в сводке окажутся последние пришедшие ключи с малыми счётчиками - это не список «самых частых», и чтобы утверждать, что тяжёлых нет вовсе, кандидатов всё равно проверяют вторым проходом. И второе: удалений сводка не поддерживает - отменить событие нельзя.

Что ломается без этого

Кандидата в большинство не проверяют. Алгоритм назовёт кандидата всегда; он прав, только если большинство есть.

Счётчик читают как частоту. 699 в сводке - это «не меньше 699», а на деле 1208. Сравнивать по счётчикам два ключа можно, только если разница превышает допуск.

При переполнении выбрасывают ключ с наименьшим счётчиком, а остальные не трогают. Гарантия пропадает: тяжёлый ключ, пришедший поздно и вперемежку с новыми, каждый раз оказывается наименьшим и выбрасывается.

Порог не связан с k. Девять счётчиков гарантируют ключи с долей больше 10%. Искать по ним ключи с долей 1% - значит получить случайный набор.

Зачем это в работе

Поиск «горячих» ключей. В распределённом кеше или базе данных один ключ, на который приходится заметная доля запросов, перегружает свой узел; сводка на несколько десятков счётчиков на каждом узле находит такие ключи без учёта всех остальных.

Сетевая статистика: адреса и соединения, занимающие больше процента трафика. Счётчиков нужно около ста, а разных адресов в потоке - миллионы.

В отличие от count-min, эта сводка хранит сами ключи: на вопрос «кто самые частые» она отвечает списком, а таблице count-min пришлось бы задавать вопрос про каждый ключ отдельно. И её гарантия не опирается ни на случайность, ни на качество хеш-функций: она выполняется всегда, а не с вероятностью.

Теперь сам

  1. Пройди алгоритмом большинства поток A, B, A, C, A. Какими будут кандидат и счётчик в конце?
  2. В потоке 10 000 элементов, сводка на 4 счётчика. Насколько, самое большее, занижен счётчик?
  3. Сколько счётчиков нужно, чтобы гарантированно сохранить все ключи с долей больше 2% потока?
  4. Сводка на два счётчика содержит {A: 3, B: 1}. Пришёл ключ C. Что станет со сводкой?
  5. В сводке для потока из 6000 элементов с девятью счётчиками у ключа счётчик 150. В каких пределах лежит его частота?

Ответ. 1. A:1, затем B снимает до 0, A:1, C снимает до 0, A:1. Кандидат A, счётчик 1.

  1. На 10 000 / 5 = 2000.

  2. Нужно N / (k + 1) ≤ 0,02 · N, то есть k + 1 ≥ 50: 49 счётчиков.

  3. Оба счётчика уменьшаются: {A: 2}; пара B с нулём удалена, C в сводку не попал.

  4. От 150 до 150 + 600 = 750.

Главное

Частый элемент находят не подсчётом, а взаимным уничтожением: разные элементы гасят друг друга, и погасить всё может только то, чего мало. Сводка из k счётчиков гасит группами по k + 1, поэтому потери каждого ключа ограничены долей 1/(k + 1) потока, и всё, что встречается чаще, обязано уцелеть. Гарантия здесь не вероятностная: она выполняется на любом потоке.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

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