Задача
В черный ящик кладутся листки с написанными на них числами. На каждом листке — ровно одно целое число. Иногда некоторые листки исчезают из ящика. После каждого события (когда в ящик положили листок, или когда из ящика исчез листок), нужно вывести число, которое встречается чаще всего на листках, находящихся в данный момент в ящике. Если таких чисел несколько, выведите наименьшее.
Входные данные
Первая строка содержит количество событий [latex]n[/latex] [latex]\left(1 \le n \le 2 \times 10^{5} \right)[/latex]. Каждая из следующих n строк содержит описание одного события:
- [latex]+ x[/latex] — положен листок с числом [latex]x[/latex] [latex]\left(1 \le x \le 10^{6} \right)[/latex];
- [latex]- x[/latex] — исчез листок с числом [latex]x[/latex] (гарантируется, что в ящике был хотя бы один листок с числом [latex]x[/latex]).
Выходные данные
Вывести в точности [latex]n[/latex] строк — по одной для каждого события. Каждая строка должна содержать одно число — ответ к задаче. Если после какого-то события ящик оказался пуст, следует вывести [latex]0[/latex].
Тесты
Входные данные | Выходные данные |
---|---|
3 + 1 — 1 + 2 |
1 0 2 |
6 + 1 + 1000000 — 1 + 4 + 4 — 1000000 |
1 1 1000000 4 4 4 |
8 + 71 + 91 + 99 + 71 — 71 — 91 — 71 — 99 |
71 71 71 71 71 71 99 0 |
Код программы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
#include <iostream> using namespace std; template <class POINTER_TYPE, class TYPE> class segment_tree { TYPE *SegmentTree; POINTER_TYPE base_capacity = 0; TYPE (*g)(TYPE, TYPE); TYPE neutral; TYPE result_on_segment_in ( POINTER_TYPE v, POINTER_TYPE tl, POINTER_TYPE tr, POINTER_TYPE l, POINTER_TYPE r ) { if (l > r) return neutral; else if (l == tl && r == tr) return SegmentTree[v]; else { POINTER_TYPE tm = (tl + tr) / 2; return g(result_on_segment_in(v*2, tl, tm, l, min(r,tm)), result_on_segment_in(v*2+1, tm+1, tr, max(l,tm+1), r)); } } void make_monoid_and_fill_rest(TYPE *NewTree, const POINTER_TYPE &n, TYPE f(TYPE, TYPE), const TYPE &neutr) { g = f; neutral = neutr; for(POINTER_TYPE i = base_capacity+n; i < 2*base_capacity; ++i) { NewTree[i] = neutral; } for(POINTER_TYPE i = base_capacity-1; i > 0; --i) { NewTree[i] = g(NewTree[2*i], NewTree[2*i+1]); } SegmentTree = NewTree; } void fix_capacity(const POINTER_TYPE &base_array_size) { for (base_capacity = 1; base_capacity < base_array_size; base_capacity <<= 1); } public: void read_and_construct(const POINTER_TYPE amount_of_elements, TYPE preprocessing_function(const POINTER_TYPE&), TYPE f(TYPE, TYPE), const TYPE neutr) { fix_capacity(amount_of_elements); TYPE *NewTree = new TYPE[base_capacity*2]; for(POINTER_TYPE i = 0; i < amount_of_elements; ++i) { NewTree[base_capacity+i] = preprocessing_function(i); } make_monoid_and_fill_rest(NewTree, amount_of_elements, f, neutr); } void assign(const POINTER_TYPE index, const TYPE new_value) { SegmentTree[base_capacity+index] = new_value; for (POINTER_TYPE i = (base_capacity+index)/2; i > 0; i /= 2) { SegmentTree[i] = g(SegmentTree[2*i], SegmentTree[2*i+1]); } } TYPE result_on_segment (POINTER_TYPE l, POINTER_TYPE r) { return result_on_segment_in(1, 0, base_capacity-1, l, r); } }; struct leaf { unsigned int number, amount; leaf() { number = 2000000; amount = 0; } leaf(unsigned int a) { number = a, amount = 0; } }; leaf max_leafs(leaf a, leaf b) { return (a.amount == b.amount) ? (a.number < b.number) ? a : b : (a.amount > b.amount) ? a : b; } leaf constructor(const unsigned int &i) { return leaf(i); } int main() { segment_tree <unsigned int, leaf> box; box.read_and_construct(1000001, constructor, max_leafs, leaf()); unsigned int n, index; char operation; leaf tmp; scanf("%u\n", &n); ++n; while (--n) { scanf("%c %u\n", &operation, &index); tmp = box.result_on_segment(index, index); if (operation == '+') ++tmp.amount; else --tmp.amount; box.assign(index, tmp); printf("%u\n", (box.result_on_segment(0, 1000000)).number); } return 0; } |
Решение задачи
Проанализируем задачу: так как требуется вывести число, которое встречается чаще всего на листках, находящихся в ящике после запроса удаления/добавления, а если таких чисел несколько, то вывести наименьшее, то задачу можно переформулировать следующим образом:
«Даётся последовательность (массив) объектов leaf [latex]x_{1}[/latex], [latex]x_{2}[/latex], [latex]x_{3}[/latex], [latex]\ldots[/latex], [latex]x_{999999}[/latex], [latex]x_{1000000}[/latex], представляющих из себя пару (number, amount)[latex]=x_{i}=\left(i, a_{i}\right) \in {\mathbb{N}_{0}}^{2}[/latex], где первые элементы пар [latex]i[/latex] представляет из себя число/номер листка, а вторые элементы [latex]a_{i}[/latex] — число листков с этим номером. Изначально все элементы пар [latex]a_{i}[/latex] равны нулю (так как изначально ящик пуст). Для запросов первого типа [latex]+ x[/latex] необходимо увеличивать на единицу число [latex]a_{i}[/latex] объекта, у которого номер [latex]i[/latex] равен [latex]x[/latex], а для запросов второго типа — уменьшать. Для каждого запроса необходимо вывести число [latex]j[/latex], удовлетворяющее условию [latex]j = \min\limits_{i \in \mathbb{K}}{i}[/latex], где [latex]\mathbb{K} = \{i \mid a_{i} = \max\limits_{k \in \{1, 2, \ldots, 1000000\}}{a_{k}} \}[/latex]».
Иными словами, число [latex]i[/latex] соответствует некоторому элементу [latex]x_{i} = \left(i, a_{i}\right)[/latex], который в свою очередь определяется операцией такой, что [latex]i[/latex] и [latex]a_{i}[/latex] удовлетворяют приведённым выше условиям. Очевидно, что данная операция является ассоциативной (как объединение минимума и максимума на заданных множествах), а потому для решения задачи воспользуемся универсальным деревом отрезков.
Создадим дерево отрезков box методом read_and_construct из объектов leaf. Так как нумерация листков начинается с единицы, а их число не превышает [latex]10^{6}[/latex], зададим размер базы дерева отрезков [latex]10^{6}+1[/latex], добавив неё элемент с индексом [latex]0[/latex]. Модифицируем метод read_and_construct таким образом, чтобы в функцию-препроцессор передавался номер элемента [latex]i[/latex], дабы была возможность задавать элементам [latex]x_{i}[/latex] их первоначальные значения [latex]\left(i, 0\right)[/latex]. Вышеупомянутую операцию назовём max_leafs и определим таким образом, чтобы она принимала два аргумента [latex]x_{i} = \left(i, a_{i}\right)[/latex] и [latex]x_{j} = \left(j, a_{j}\right)[/latex] и возвращала тот из них, у которого значение [latex]a[/latex] является большим, а в случае равенства этих значений — аргумент с меньшим индексом. Нейтральным элементом относительно данной операции будет, очевидно, пара [latex]\left(+\infty, 0\right)[/latex], но в силу того, что номера элементов не превышают [latex]10^6[/latex], вместо неё мы будем пользоваться парой [latex]\left(2 \times 10^{6}, 0\right)[/latex].
Далее при запросах вида [latex]+ x[/latex] будем увеличивать соответствующее значение [latex]a_{x}[/latex] пары [latex]\left(x, a_{x}\right)[/latex] на единицу, а при запросах вида [latex]- x[/latex] — уменьшать. Для обоих запросов будем выводить номер заданного листка, который удовлетворяет приведённым в задаче условиям, с использованием метода result_on_segment на всём отрезке [latex]\left[0, 10^{6}\right][/latex]. Ответом для каждого запроса будет значение number пары, которую вернёт метод.
Примечание: ситуация когда ящик становится пустым нигде не обрабатывается, но в силу того, что мы определили массив на отрезке [latex]\left[0, 10^{6}\right][/latex] вместо [latex]\left[1, 10^{6}\right][/latex] в нём всегда есть пара [latex]\left(0, 0\right)[/latex] (листки с номером [latex]0[/latex], число (значение [latex]b[/latex]) которых всегда равно [latex]0[/latex] в силу того, что листки с номером [latex]0[/latex] в ящик не добавляются). Так как определённая нами операция всегда возвращает минимальный номер листка, число которого максимально, то в случае, когда ящик пуст (т.е. значения всех [latex]a_{i} = 0, i = 0, 1, \ldots, 10^{6}[/latex]) будет выводится номер листка [latex]0[/latex]. Этот побочный эффект данного нами определения массива решает эту ситуацию и завершает решение задачи.
Ссылки
- Универсальное дерево отрезков;
- Условие задачи на e-olymp.com;
- Оригинал кода программы на ideone.com.
Молодец! Принято.
Правда Кодрат Воронов (он учится на пару курсов старше вас) держит рекорд по этой задаче — в два раза быстрее и в 12 раз меньше памяти.
Надо попросить его прокомментировать код.
Добрый день, Игорь Евгеньевич!
Я посмотрел решение Вадима и могу рассказать, в чем его неэффективность.
1. Слишком универсальный (обобщённый) код. Реализовано универсальное дерево отрезков. Используется класс с методами, что теоретически может создавать оверхед(хотя и не должно, т.к. класс только один, наследования нет, а значит, оверхеду от позднего связывания неоткуда взяться). В любом случае, на ACM писать классы если можно обойтись функциями как-то не принято.
2. Медленный ввод. В решении Вадима используются scanf и printf, но ввиду тривиальности ввода в этой задаче, его можно ускорить с помощью посимвольного ввода (что и делается в моем решении).
4. Решение задачи в в общем случае без учета особенностей конкретной задачи. Нет смысла считать то, что уже посчитано. У Вадима в коде, вычисляющем ответ на запрос, есть такая строка:
printf(«%u\n», (box.result_on_segment(0, 1000000)).number);
Но ведь, поскольку нас интересует ответ на запрос на отрезке, покрывающем всё дерево, мы можем просто напрямую обратиться к нулевому (или первому, в зависимости от реализации) элементу дерева. К тому же, из-за того, что размер дерева не является степенью двойки, то это ещё и работает за O(log n). А ведь на запросы чтения можно отвечать за O(1).
3. Использование неправильной структуры данных для решения задачи. В ACM всегда надо смотреть на ограничения. В данной задаче числа могут быть до 10^6, из-за чего при использовании обычного дерева отрезков нам приходится работать с деревом отрезков на 10^6 элементов. В частности, уходит время на процедуру построения дерева. А ведь запросов всего 10^5. То есть мы строим дерево размером 10^6 из нейтральных элементов независимо от размера ввода. И используем в конечном счете не более 1/10 элементов из этого дерева. Вместо этого предлагается использовать неявное дерево отрезков. В таком случае будут создаваться только те элементы дерева, которые реально используются (плюс будет ускорение на тестах маленького размера). А процедура построения дерева вообще не требуется.
Прикинем асимптотику обоих решений (не исключительно в терминах big-O-notation, а учитывая все компоненты):
n — количество элементов дерева (1000000)
q — количество запросов
Решение Вадима:
Построение дерева — O(n)
Обновление дерева — O(log n) на запрос
Получение ответа — O(log n) на запрос
Итого время работы: C1*O(n)+C2*O(q*log n)+C3*O(q*log n),
где C1, C2, C3 — константы
Моё решение:
Обновление дерева — O(log n) на запрос
Получение ответа — O(1) на запрос
Итого время работы: C1*O(q*log n)+C2*O(1),
где C1, C2 — константы