Rake-Compress деревья — различия между версиями
| Строка 38: | Строка 38: | ||
}}  | }}  | ||
| − | ==  | + | ==Реализация==  | 
| + | ===Хранение===  | ||
Для того, чтобы отвечать на запросы относительно структуры леса необходимо сохранить информацию о том, как происходил процесс сжатия леса. Для этого будем хранить таблицу, в каждой строке которой записана информация о структуре леса после выполнения операций. Каждая клетка таблицы будет хранить информацию о вершине после выполнения операций <tex>\mathrm{Rake}</tex> и <tex>\mathrm{Compress}</tex>. Из информации будем сохранять родителя вершины, множество детей и метку, сжата ли вершина.<br>  | Для того, чтобы отвечать на запросы относительно структуры леса необходимо сохранить информацию о том, как происходил процесс сжатия леса. Для этого будем хранить таблицу, в каждой строке которой записана информация о структуре леса после выполнения операций. Каждая клетка таблицы будет хранить информацию о вершине после выполнения операций <tex>\mathrm{Rake}</tex> и <tex>\mathrm{Compress}</tex>. Из информации будем сохранять родителя вершины, множество детей и метку, сжата ли вершина.<br>  | ||
Пример таблицы для следующей последовательности операций:  | Пример таблицы для следующей последовательности операций:  | ||
| Строка 116: | Строка 117: | ||
          cells[i] = []  |           cells[i] = []  | ||
| + | ===Построение===  | ||
Рассмотрим, как работает алгоритм построения Rake-Compress дерева. Будем строить таблицу по строкам. В каждый момент будем хранить множество вершин, которые еще не были сжаты, и перестраивать следующий слой. Также будем делать операции <tex>\mathrm{Rake}</tex> и <tex>\mathrm{Compress}</tex> одновременно. Чтобы определить, нужно ли сжимать вершину, воспользуемся следующим алгоритмом:  | Рассмотрим, как работает алгоритм построения Rake-Compress дерева. Будем строить таблицу по строкам. В каждый момент будем хранить множество вершин, которые еще не были сжаты, и перестраивать следующий слой. Также будем делать операции <tex>\mathrm{Rake}</tex> и <tex>\mathrm{Compress}</tex> одновременно. Чтобы определить, нужно ли сжимать вершину, воспользуемся следующим алгоритмом:  | ||
  '''bool''' shouldRemoveVertex(c: '''Cell''', rand, layer: '''int''')''':'''  |   '''bool''' shouldRemoveVertex(c: '''Cell''', rand, layer: '''int''')''':'''  | ||
| Строка 137: | Строка 139: | ||
          nextAlive = <tex>\emptyset</tex>  |           nextAlive = <tex>\emptyset</tex>  | ||
          '''for''' <tex>v \in \mathrm{alive}</tex>  |           '''for''' <tex>v \in \mathrm{alive}</tex>  | ||
| − |               c = getCellForVertex(v)  | + |               c = getCellForVertex(v)                        <font color="green">// получить клетку таблицы, соответствующую вершине</font>  | 
              '''if''' shouldRemoveVertex(c, rand, layer)  |               '''if''' shouldRemoveVertex(c, rand, layer)  | ||
                  '''if''' c.cntChild == 1  |                   '''if''' c.cntChild == 1  | ||
| Строка 151: | Строка 153: | ||
              cells[v].add(newCell)  |               cells[v].add(newCell)  | ||
          layer++  |           layer++  | ||
| − | ==Операции удаления и добавления ребра==  | + | ===Операции удаления и добавления ребра===  | 
Как только некоторая вершина помечается как изменившаяся, отменим её действие на таблицу. А именно, найдем момент времени, когда вершина сжимается к родителю. Рассмотрим, какие вершины поменяются при сжатии данной. Это ее родитель (если он есть), а также сын (если он есть). Для каждой из этих вершин поменяем значения изменений, которые необходимо применить к состоянию. Также пометим, что эти вершины поменялись на этом слое. Для этого на каждом слое будем хранить список вершин, которые на нем поменялись. А перед тем как обрабатывать очередной слой будем добавлять в множество изменившихся вершин вершины из соответствующего списка. Кроме удаления эффекта от изменившихся вершин также необходимо и добавить правильный эффект. Для этого будем для каждой из из- менившихся вершин определять, как ее состояние меняется при переходе к следующему слою. Если вершина сжимается к ее родителю, то пометим родителя и ребенка (если он есть) и поменяем изменение, которое хранится в соответствующих клетках. А для пересчета состояния клеток воспользуемся значениями изменений, которые сохранены в клетках.<br>  | Как только некоторая вершина помечается как изменившаяся, отменим её действие на таблицу. А именно, найдем момент времени, когда вершина сжимается к родителю. Рассмотрим, какие вершины поменяются при сжатии данной. Это ее родитель (если он есть), а также сын (если он есть). Для каждой из этих вершин поменяем значения изменений, которые необходимо применить к состоянию. Также пометим, что эти вершины поменялись на этом слое. Для этого на каждом слое будем хранить список вершин, которые на нем поменялись. А перед тем как обрабатывать очередной слой будем добавлять в множество изменившихся вершин вершины из соответствующего списка. Кроме удаления эффекта от изменившихся вершин также необходимо и добавить правильный эффект. Для этого будем для каждой из из- менившихся вершин определять, как ее состояние меняется при переходе к следующему слою. Если вершина сжимается к ее родителю, то пометим родителя и ребенка (если он есть) и поменяем изменение, которое хранится в соответствующих клетках. А для пересчета состояния клеток воспользуемся значениями изменений, которые сохранены в клетках.<br>  | ||
Рассмотрим, что происходит с таблицей при изменении одного ребра. Основная идея заключается в том, чтобы научится пересчитывать все изменения таблицы за время пропорциональное их количеству. Для этого будем эффективно поддерживать множество изменившихся клеток. В момент, когда вершина помечается, как изменившаяся, найдем, как она влияет на таблицу и отменим это влияние. <br>Для начала необходимо найти момент времени, когда вершина сжимается. В этот момент она влияет на не более чем две вершины. Изменим значения <tex>\mathrm{cntChild}</tex>, <tex>\mathrm{sumChild}</tex> и <tex>\mathrm{newParent}</tex> нужным образом. Также необходимо добавить эти вершины в множество изменившихся (в момент, когда будет обработан соответствующий слой). Поэтому, для каждого слоя еще будем хранить список вершин, которые должны быть помечены перед обработкой слоя.<br>Алгоритм обновления дерева:  | Рассмотрим, что происходит с таблицей при изменении одного ребра. Основная идея заключается в том, чтобы научится пересчитывать все изменения таблицы за время пропорциональное их количеству. Для этого будем эффективно поддерживать множество изменившихся клеток. В момент, когда вершина помечается, как изменившаяся, найдем, как она влияет на таблицу и отменим это влияние. <br>Для начала необходимо найти момент времени, когда вершина сжимается. В этот момент она влияет на не более чем две вершины. Изменим значения <tex>\mathrm{cntChild}</tex>, <tex>\mathrm{sumChild}</tex> и <tex>\mathrm{newParent}</tex> нужным образом. Также необходимо добавить эти вершины в множество изменившихся (в момент, когда будет обработан соответствующий слой). Поэтому, для каждого слоя еще будем хранить список вершин, которые должны быть помечены перед обработкой слоя.<br>Алгоритм обновления дерева:  | ||
Версия 16:04, 1 мая 2016
Задача, которая решается с помощью динамических деревьев (англ. dynamic tree), формулируется следующим образом. Необходимо поддерживать лес деревьев и выполнять на нем следующие операции:
- добавить ребро . Вершина должна быть корнем некоторого дерева. Вершины и должны находиться в разных деревьях,
 - удалить ребро . Ребро должно присутствовать в графе,
 - некоторый запрос относительно структуры дерева.
 
Примером последней операции может быть запрос "достижима ли вершина из ?", "сколько ребер на кратчайшем пути из в ?" или "какова сумма номеров вершин, которые находятся в поддереве вершины ?". Можно легко реализовать структуру данных, которая будет выполнять данные операции за время , где — количество вершин в графе. Динамические деревья нужны для того, чтобы обрабатывать запросы более эффективно. В частности, все предложенные операции возможно выполнять за время .
Содержание
Описание
Rake-Compress Tree — структура данных, которая хранит лес деревьев и поддерживает следующие операции:
- — все листья дерева сжимаются к своим родителям,
 - — выбирается и объединяется некоторое множество несмежных друг с другом вершин, имеющих ровно одного сына.
 
Входными данными для алгоритма Rake-Compress является лес корневых деревьев. К нему поочередно применяются операции  и  до тех пор, пока существует хотя бы одна живая вершина.
Во время каждой из этих операций выбирается некоторое множество попарно несмежных вершин, которое сжимается к своим родителям. После каждой операции лес сохраняется в специальном виде, что в дальнейшем дает возможность отвечать на запросы о структуре леса.
Современная реализация Rake-Compress деревьев была предложена Р. А. Тарьяном и Р. Ф. Вернеком.
Рассмотрим, как изменяется количество вершин в дереве после применения к нему операций и . Разобьём все вершины дерева на три группы: входящая степень которых равна нулю, одному и больше одного. Обозначим их количество за и соответственно.
| Лемма (1): | 
.  | 
| Доказательство: | 
| 
 Докажем по индукции по высоте дерева.  | 
| Лемма (2): | 
После применения операций  и  к лесу, математическое ожидание количества вершин в нём не превосходит  от их исходного числа.  | 
| Доказательство: | 
| 
 Математическое ожидание количества удаленных вершин  (так как все листья будут удалены после операции , а каждая вершина, у которой ровно один сын, будет удалена с вероятностью  после операции ). Из предыдущей леммы получаем:  | 
| Теорема: | 
Математическое ожидание количества операций  и , которые будут выполнены до полного сжатия дерева, равно , где  — общее количество вершин.  | 
| Доказательство: | 
| Из леммы 2 известно, что после каждой итерации применения операций и число вершин в среднем уменьшается в константное число раз. Значит, количество итераций в среднем ограничено . | 
Реализация
Хранение
Для того, чтобы отвечать на запросы относительно структуры леса необходимо сохранить информацию о том, как происходил процесс сжатия леса. Для этого будем хранить таблицу, в каждой строке которой записана информация о структуре леса после выполнения операций. Каждая клетка таблицы будет хранить информацию о вершине после выполнения операций  и . Из информации будем сохранять родителя вершины, множество детей и метку, сжата ли вершина.
Пример таблицы для следующей последовательности операций:
| Шаг | Операция | ||||
|---|---|---|---|---|---|
| 4 |  Родитель: — Дети:  | 
— | — | — | |
| 3 |  Родитель: — Дети:  | 
— |  Родитель:  Дети:  | 
— | |
| 2 |  Родитель: — Дети:  | 
 Родитель:  Дети:  | 
 Родитель:  Дети:  | 
— | |
| 1 |  Родитель: — Дети:  | 
 Родитель:  Дети:  | 
 Родитель:  Дети:  | 
 Родитель:  Дети:  | 
Для того, чтобы выбирать множество вершин для применения операции  будем использовать следующий метод: для каждой вершины с помощью генератора псевдослучайных чисел выберем случайный бит. Вершина добавляется в множество, если у нее ровно один ребенок, она не является корнем и биты, которые были сгенерированы для нее, ребенка и родителя равны 0, 1 и 1 соответственно.
Рассмотрим более подробно, как необходимо хранить клетки таблицы Rake-Compress дерева. Для вершины необходимо сохранить ее родителя, а также множество детей. Для того, чтобы обрабатывать каждую клетку таблицы за , нужно производить операции с множеством детей за . 
На самом деле, множество детей хранится для того, чтобы определять, можно ли сжать вершину. Если детей у вершины больше одного, то ее точно нельзя сжать. Если у неё нет детей, то ее можно сжать только во время операции . Чтобы определить можно ли применить операцию  к вершине, в том случае, когда у нее один ребёнок, нужно узнать, как бит был сгенерирован на текущей итерации для ребёнка. Для этого необходимо знать номер вершины-ребёнка. Значит, необходимо уметь определять, кто находится в множестве только в том случае, если в нём не более одного элемента. Поэтому, всю информацию о множестве можно хранить с помощью двух величин — хранить количество элементов в множестве и сумму их номеров.
Если вершина является корнем, то в качестве ее родителя будем хранить ее номер. Кроме того необходимо хранить изменения, которые произойдут с клеткой при переходе к следующему слою: будем хранить, кто должен стать новым родителем, на сколько изменится количество детей, а также как изменится сумма их номеров. Все это необходимо для того, чтобы обрабатывать каждую изменившуюся клетку за .
Псевдокод хранения клетки таблицы:
struct Cell:
    int id, parent, cntChild, sumChild
    int newParent, diffCntChild, diffSumChild
    
    func applyChanges():
        parent = newParent
        sumChild += diffSumChild
        cntChild += diffCntChild
        diffCntChild = diffSumChild = 0
    
    func addChild(v):
        diffCntChild++
        diffSumChild += v
 
    func removeChild(v):
        diffCntChild--
        diffSumChild -= v
Для хранения Rake-Compress дерева будем использовать следующие данные:
- — список клеток, которые ей соответствуют, для каждой вершины,
 - — генератор псевдослучайных чисел,
 - — счётчик количества примененных операций по изменению структуры леса,
 - — массив, в котором для каждой вершины запишем номер последней операции, при обработки которой была изменена хотя бы одна клетка, которая соответствуют вершине: это позволит эффективно узнавать, была ли вершина уже помечена как поменявшаяся или нет.
 
struct RCTree(n: int):
    rand = RandBitsGenerator()
    time = 0
    for i = 0 to n
        lastUpdateTime[i] = 0
        cells[i] = []
Построение
Рассмотрим, как работает алгоритм построения Rake-Compress дерева. Будем строить таблицу по строкам. В каждый момент будем хранить множество вершин, которые еще не были сжаты, и перестраивать следующий слой. Также будем делать операции и одновременно. Чтобы определить, нужно ли сжимать вершину, воспользуемся следующим алгоритмом:
bool shouldRemoveVertex(c: Cell, rand, layer: int):
    if c.cntChild == 0
        return true
    if c.cntChild > 1 or c.parent == c.id
        return false
    if getCellForVertex(c.sumChild).cntChild == 0
        return false
    if rand.getBit(c.id, layer) == 0 and rand.getBit(c.sumChild, layer) == 1 and rand.getBit(c.parent, layer) == 1:
        return true
    return false
Таким образом, алгоритм построения дерева выглядит следующим образом:
func build(parent: int[]):
    alive = 
    layer = 0
    for i = 0 to n
        cells[i].add(Cell(parent[i]))
    while 
        nextAlive = 
        for 
            c = getCellForVertex(v)                        // получить клетку таблицы, соответствующую вершине
            if shouldRemoveVertex(c, rand, layer)
                if c.cntChild == 1
                    getCellForVertex(c.sumChild).newParent = c.parent
                    getCellForVertex(c.parent).addChild(c.sumChild)
                if c.parent  v
                    getCellForVertex(c.parent).remove(v)
            else
                nextAlive.add(v)
        alive = nextAlive
        for 
            newCell = getCellForVertex(v).clone().appleChanges()
            cells[v].add(newCell)
        layer++
Операции удаления и добавления ребра
Как только некоторая вершина помечается как изменившаяся, отменим её действие на таблицу. А именно, найдем момент времени, когда вершина сжимается к родителю. Рассмотрим, какие вершины поменяются при сжатии данной. Это ее родитель (если он есть), а также сын (если он есть). Для каждой из этих вершин поменяем значения изменений, которые необходимо применить к состоянию. Также пометим, что эти вершины поменялись на этом слое. Для этого на каждом слое будем хранить список вершин, которые на нем поменялись. А перед тем как обрабатывать очередной слой будем добавлять в множество изменившихся вершин вершины из соответствующего списка. Кроме удаления эффекта от изменившихся вершин также необходимо и добавить правильный эффект. Для этого будем для каждой из из- менившихся вершин определять, как ее состояние меняется при переходе к следующему слою. Если вершина сжимается к ее родителю, то пометим родителя и ребенка (если он есть) и поменяем изменение, которое хранится в соответствующих клетках. А для пересчета состояния клеток воспользуемся значениями изменений, которые сохранены в клетках.
Рассмотрим, что происходит с таблицей при изменении одного ребра. Основная идея заключается в том, чтобы научится пересчитывать все изменения таблицы за время пропорциональное их количеству. Для этого будем эффективно поддерживать множество изменившихся клеток. В момент, когда вершина помечается, как изменившаяся, найдем, как она влияет на таблицу и отменим это влияние. 
Для начала необходимо найти момент времени, когда вершина сжимается. В этот момент она влияет на не более чем две вершины. Изменим значения ,  и  нужным образом. Также необходимо добавить эти вершины в множество изменившихся (в момент, когда будет обработан соответствующий слой). Поэтому, для каждого слоя еще будем хранить список вершин, которые должны быть помечены перед обработкой слоя.
Алгоритм обновления дерева:
func changeTree(: Edge): time = time + 1 affected = markAffected(u) // пусть из дерева было удалено ребро markAffected(v) cells[u].parent = u cells[v].cntChild-- cells[v].sumChild -= u layer = 0 while for markAffected(v) for c = getCellForVertex(v) if shouldRemoveVertex(v) cells[v].size = layer + 1 if c.cntChild == 1 getCellForVertex(c.sumChild).newParent = c.parent getCellForVertex(c.parent).addChild(c.sumChild) markAffected(c.sumChild) if c.parent v getCellForVertex(c.parent).removeChild(v) markAffected(c.parent) affected.remove(v) for newCell = getCellForVertex(v).clone().applyChanges() cells[v][layer + 1] = newCell layer++ func markAffected(v: int): if lastUpdateTime[v] == time return // вершина уже помечена lastUpdateTime[v] = time affected.add(v) removeEffectOfVertex(v) func removeEffectOfVertex(v: int): layer = cells[v].size c = cells[v][layer] if c.parent == v return cells[c].parent.removeChild(v) if c.cntChild == 1 cells[c.parent].addChild(c.sumChild) cells[c.sumChild].newParent = v
Возможность параллельного построения
Операции и можно выполнять параллельно для всех вершин. Если предположить, что множество детей можно пересчитывать за , то Rake-Compress дерево можно построить за в модели PRAM в случае наличия процессоров.
Пример выполнения операций
Применения
- Задача MST online: дан граф из вершин, в который добавляются новые рёбра. Необходимо поддерживать минимальный остовный лес для данного графа,
 - Задача о максимальном потоке: в помощью динамических деревьев можно улучшить ассимптотику алгоритма Диница с до .
 
См. также
Источники информации
- Wikipedia — Parallel Tree Contraction
 - Б. Ю. Минаев — Реализация динамических Rake-Compress деревьев в случае отсутствия ограничения на степени вершин
 - G. L. Miller, J. H. Reif — Parallel Tree Contraction
 - R. Werneck — Design and Analysis of Data Structures for Dynamic Trees