TL;DR: можно ли одновременно поддерживать эйлеровым обходом LCA, сумму в поддереве и переподвешивание?
Дерево эйлерова обхода -- это способ представлять подвешенное неориентированное дерево массивом чисел. Построить это представление можно несколькими способами. У каждого есть свои плюсы и минусы. Здесь я хочу обсудить, можно ли придумать алгоритм, объединяющий плюсы всех описанных. Может быть, новичкам пост будет полезен как туториал. Первый способ -- выписать все рёбра дерева (ориентированные) в порядке обхода DFS'а. Так эйлеров обход определяется в Википедии.
1
/ \
2 5
/ \
3 4
[1-2] [2-3] [3-2] [2-4] [4-2] [2-1] [1-5] [5-1]
Второй способ -- хранить вершины. Каждая вершина добавляется в массив дважды: когда мы в неё спускаемся и когда выходим. Каждому листу (кроме, возможно, корня) соответствует два последовательных вхождения.
1
/ \
2 5
/ \
3 4
1 2 3 3 4 4 2 5 5 1
Третий способ -- также храним вершины, но каждая вершина добавляется в массив всякий раз, когда мы её посещаем (когда спускаемся из предка и поднимаемся из потомка).
1
/ \
2 5
/ \
3 4
1 2 3 2 4 2 1 5 1
Далее, если явно не сказано иное, обход будет храниться в декартовом дереве по неявному ключу. first(v) и last(v) -- первое и последнее вхождение вершины в массив.
Итак, как же этим пользоваться?
Во-первых, заметим, что поддереву всегда соответствует отрезок. Значит, можно сводить запросы на поддеревьям к запросам на отрезках. Например, если мы хотим прибавлять к вершине и считать сумму в поддереве, можно построить дерево Фенвика на обходе 2-го типа. add(v, x) становится add(first(v), x) в Фенвике, а get_sum(v) становится get_sum(first(v), last(v)). С помощью небольшого трюка можно считать сумму на пути до корня: add(v, x) превращается add(first(v), x), add(last(v), - x) в Фенвике, а get_sum_on_path_from_root(v) -- в get_sum(0, first(v)) (можете сами проверить на бумажке).
Эйлеровым обходом можно искать LCA. Используем третий тип. Будем хранить массив высот h, где h[i] = height(v[i]). Тогда lca(u, v) = v[argmin(first(u), first(v)) в h]. Почему? Потому что LCA(u, v) -- это самая высокая вершина между u и v в порядке обхода поиска в глубину. Я часто пишу этот метод вместо двоичного подъема -- быстрее и требует линию памяти.
Из того, что поддереву соответствует отрезок, можно извлечь ещё плюшек. В первом и третьем подходе можно переподвешивать дерево, просто перекинув кусок массива из начала в конец. Чтобы избежать некоторого геморроя, в третьем подходе можно не хранить последнюю вершину для корня (тогда у каждой вершины будет столько вхождений, чему равна её степень). Ещё можно удалять и добавлять рёбра (link-cut), вырезая отрезок из массива и вставляя его обратно. В третьем подходе надо аккуратно обрабатывать дублирующееся вхождение бывшего предка поддерева, которое появляется после его вырезания.
В общем, штука мощная и достаточно универсальная, но у меня есть несколько вопросов. Можно ли одновременно делать переподвешивание и LCA? сумму в поддереве и LCA? и так далее. Да, при всём при этом не хочется терять возможность вырезать и вставлять обратно поддеревья.
Вот небольшая табличка.
Хранится | LCA | Сумма в поддереве | Сумма на пути | Переподвешивание | |
---|---|---|---|---|---|
1 | ребра | — | + (значения на рёбрах) | + | + (не уверен, что можно с суммой) |
2 | вершины | — | + (значения в вершинах) | + | — |
3 | вершины | + (без переподвешивания) | — | — | + (без LCA) |
Заключительный вопрос (и главная цель этого поста): можете добавить плюсиков в таблицу и рассказать ещё что-нибудь, что можно делать эйлеровым обходом?
P.S. Не надо писать про хеви-лайт и линк-кат. Я знаю, что это и какие задачи ими можно решать, но хочется научиться выжимать максимум из эйлерова обхода.
I think what you called "binary accent" is referenced as "binary lifting". Great article by the way! Thanks
What is the value of rerooting? What can we calculate with rerooting that we can't do otherwise?
You can read it in the post 'There is also a trick to compute sum on path from root to a vertex'
Can you explain uses of rerooting.
Can anyone suggest nice problems relating to this topic ??
Recently appeared on codechef: CLOSEFAR
This one is nice 620E. New Year Tree
You can also pick any problem about LCA to practice this topic, like SPOJ LCA
Open-ended question Can we reroot a tree and be able to answer the LCA as well?
Answer is yes. This is exactly what this question asks ( https://www.codechef.com/problems/TALCA )
Here is a link to my implementation using the above technique ( https://www.codechef.com/viewsolution/10863340 )
Your implementation has nothing to do with Euler-Tour trees.
Answering your own question , never seen that before xD
Did you reach a conclusion regarding this, or is it still an interest?
Я немного думал над вопросами, только немного потому, что задач в сторону переподвешиваний почти никогда не было :) Все утверждения этого комментария есть лишь идеи и наработки, никогда не проверялись и могут быть просто ошибочны.
Пусть дерево записано как скобочная последовательность (отличие от версии поста: каждая вершина пишется только дважды, причем информация типа "число в вершине дерева" пишется только для входа!). Запишем эту скобочную последовательность в неявную дерамиду. Перевести вершину дерева в отрезок на дерамиде мы можем за счет записывания предка вершины в дерамиде (наивный LCA по дерамиде для открывающейся и закрывающейся скобки вершины). Баланс скобочной последовательности на отрезке и его минимум на отрезке могут быть записаны как функции левый-корень-правый и, как следствие, могут быть вычислены на дерамиде. Таким образом мы должны автоматически из такого представления уметь вычислять LCA (отличие от версии поста: запрос RMQ дает нам некоторого потомка вершины-LCA!) и переподвешивание. Все запросы модификации поддерева могут быть записаны как запросы на отрезке и аналогично они должны автоматически вычисляться в таком представлении. Персистентность, видимо, возможна (как минимум, неприятная редакция: нам нужно знать предков в дерамиде для определения отрезка вершины; кажется, что этого можно достичь, если хранить отдельный персистентный мап вершина дерамиды => предок в дерамиде).
При этом остаются вопросы, можно ли перенести полностью фишки Heavy/Light. Если нет переподвешивания, мы, видимо, можем изначально выписать тяжелого потомка первым и теперь любой путь по дереву разобьется в логарифм отрезков. В случае переподвешивания — ???
Approach 2 is described in this paper (https://arxiv.org/pdf/1502.05292.pdf) as "depth-first-tour trees". It describes how you can do LCA in O(log n) with that approach, and you can reroot the tree to a neighbor of the root in O(log n) (described as the "evert" operation).
Something that should be noted is that there's a typo in this paper: Lemma 17 should say s1+s2 in both cases, instead of saying s1+s1 in the second case.
Very Informative, thanks!