Блог пользователя ifsmirnov

Автор ifsmirnov, история, 9 лет назад, перевод, По-русски

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. Не надо писать про хеви-лайт и линк-кат. Я знаю, что это и какие задачи ими можно решать, но хочется научиться выжимать максимум из эйлерова обхода.

  • Проголосовать: нравится
  • +97
  • Проголосовать: не нравится

»
8 лет назад, # |
  Проголосовать: нравится +16 Проголосовать: не нравится

I think what you called "binary accent" is referenced as "binary lifting". Great article by the way! Thanks

»
8 лет назад, # |
  Проголосовать: нравится +5 Проголосовать: не нравится

What is the value of rerooting? What can we calculate with rerooting that we can't do otherwise?

»
8 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Can anyone suggest nice problems relating to this topic ??

»
8 лет назад, # |
  Проголосовать: нравится -16 Проголосовать: не нравится

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 )

»
8 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Did you reach a conclusion regarding this, or is it still an interest?

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

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

Пусть дерево записано как скобочная последовательность (отличие от версии поста: каждая вершина пишется только дважды, причем информация типа "число в вершине дерева" пишется только для входа!). Запишем эту скобочную последовательность в неявную дерамиду. Перевести вершину дерева в отрезок на дерамиде мы можем за счет записывания предка вершины в дерамиде (наивный LCA по дерамиде для открывающейся и закрывающейся скобки вершины). Баланс скобочной последовательности на отрезке и его минимум на отрезке могут быть записаны как функции левый-корень-правый и, как следствие, могут быть вычислены на дерамиде. Таким образом мы должны автоматически из такого представления уметь вычислять LCA (отличие от версии поста: запрос RMQ дает нам некоторого потомка вершины-LCA!) и переподвешивание. Все запросы модификации поддерева могут быть записаны как запросы на отрезке и аналогично они должны автоматически вычисляться в таком представлении. Персистентность, видимо, возможна (как минимум, неприятная редакция: нам нужно знать предков в дерамиде для определения отрезка вершины; кажется, что этого можно достичь, если хранить отдельный персистентный мап вершина дерамиды => предок в дерамиде).

При этом остаются вопросы, можно ли перенести полностью фишки Heavy/Light. Если нет переподвешивания, мы, видимо, можем изначально выписать тяжелого потомка первым и теперь любой путь по дереву разобьется в логарифм отрезков. В случае переподвешивания — ???

»
6 лет назад, # |
  Проголосовать: нравится +3 Проголосовать: не нравится

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.

»
3 года назад, # |
  Проголосовать: нравится +14 Проголосовать: не нравится

Very Informative, thanks!