Я считаю, что логарифмическое время для всех запросов достижимо. Основная идея состоит в том, чтобы использовать дерево интервалов, где каждый узел в дереве соответствует интервалу индексов. Я разработаю ключевые идеи, начав с более простой версии структуры данных (которая может поддерживать get и set, но не другие операции), а затем добавлю функции для поддержки других функций.
Простая схема (поддерживает get и set, но не add или stab)
Скажем , что интервал является плоским , если функция F постоянна на [ с , Ь ] , то есть, если F ( ) = е ( + 1 ) = ⋯ = е ( б ) .[a,b]f[a,b]f(a)=f(a+1)=⋯=f(b)
Наша простая структура данных будет деревом интервалов. Другими словами, у нас есть двоичное дерево, где каждому узлу соответствует интервал (индексов). Мы будем хранить соответствующий интервал в каждом узле v дерева. Каждый лист будет соответствовать плоскому интервалу, и они будут расположены таким образом, чтобы считывание листьев слева направо давало нам последовательность последовательных плоских интервалов, которые не пересекаются и объединение которых равно [ 1 , n ] . Интервал для внутреннего узла будет объединением интервалов его двух дочерних узлов. Кроме того , в каждом узле листа л мы будем хранить значение V ( л )I(v)v[1,n]ℓV(ℓ)функции на интервале I ( ℓ ), соответствующем этому узлу (обратите внимание, что этот интервал плоский, поэтому f постоянна на интервале, поэтому мы просто сохраняем одно значение f в каждом листовом узле).fI(ℓ)ff
Эквивалентно, вы можете представить, что мы разбиваем на плоские интервалы, а затем структура данных представляет собой двоичное дерево поиска, где ключами являются левые конечные точки этих интервалов. Листья содержат значение f в некотором диапазоне индексов, где f постоянно.[1,n]ff
Используйте стандартные методы, чтобы гарантировать, что бинарное дерево остается сбалансированным, т. Е. Его глубина составляет (где m подсчитывает текущее количество листьев в дереве). Конечно, m ≤ n , поэтому глубина всегда не более O ( lg n ) . Это будет полезно ниже.O(lgm)mm≤nO(lgn)
Теперь мы можем поддерживать операции get и set следующим образом:
легко: мы проходим дерево, чтобы найти лист, интервал которого содержит i . Это в основном просто обход бинарного дерева поиска. Поскольку глубина составляет O ( LG N ) , время работы составляет O ( LG N ) .get(i)iO(lgn)O(lgn)
сложнее. Это работает так:set([a,b],y)
Сначала мы находим интервал листьев содержащий a ; если a 0 < a , то мы разделяем этот листовой интервал на два интервала [ a 0 , a - 1 ] и [ a , b 0 ] (таким образом превращая этот листовой узел во внутренний узел и вводя двух дочерних элементов).[a0,b0]aa0<a[a0,a−1][a,b0]
Далее мы находим интервал листьев содержащий b ; если b < b 1 , мы разделяем этот листовой интервал на два интервала [ a 1 , b ] и [ b + 1 , b 1 ] (таким образом, превращая этот листовой узел во внутренний узел и вводя двух дочерних элементов).[a1,b1]bb<b1[a1,b][b+1,b1]
На этом этапе я утверждаю, что интервал может быть выражен как непересекающееся объединение O ( lg n ) интервалов, соответствующих некоторому подмножеству O ( lg n ) узлов в дереве. Итак, удалите всех потомков этих узлов (превратив их в листья) и установите значение, хранящееся в этих узлах, на y .[a,b]O(lgn)O(lgn)y
Наконец, поскольку мы изменили форму дерева, мы выполним любые необходимые повороты, чтобы перебалансировать дерево (используя любую стандартную технику для поддержания баланса дерева).
Поскольку эта операция включает в себя несколько простых операций на узлах (и этот набор узлов можно легко найти за O ( lg n ) времени), общее время для этой операции составляет O ( lg n ) .O(lgn)O(lgn)O(lgn)
Это показывает, что мы можем поддерживать операции get и set за времени на одну операцию. Фактически, время выполнения может быть показано как O ( lg min ( n , s ) ) , где s - количество операций установки, выполненных до настоящего времени.O(lgn)O(lgmin(n,s))s
Добавление поддержки для добавления
Мы можем изменить вышеуказанную структуру данных, чтобы она также могла поддерживать операцию добавления. В частности, вместо сохранения значения функции в листьях, оно будет представлено как сумма чисел, хранящихся в наборе узлов.
Более точно, значение функции на входе я будет возмещен как сумма значений , хранящихся в узлах на пути от корня дерева вниз к листу которого интервал содержит I . В каждом узле V мы будем хранить значение V ( V ) ; если v 0 , v 1 , ... , v K представляют предков листа об к (включая сам лист), то значение функции при I ( об к ) будетf(i)iivV(v)v0,v1,…,vkvkI(vk) .V(v0)+⋯+V(vk)
Операции get и set легко поддерживать с помощью варианта методов, описанных выше. В основном, при обходе дерева вниз, мы отслеживаем бегущую сумму значений, так что для каждого узел , что обход визит, мы будем знать сумму значений узлов на пути от корня до х . После того, как мы делаем это, простые корректировки реализации получения и установки описанного выше будет достаточно.xx
И теперь мы можем эффективно поддерживать . Сначала мы выражаем интервал [ a , b ] как объединение O ( lg n ) интервалов, соответствующих некоторому набору O ( lg n ) узлов в дереве (при необходимости разделяя узел на левой конечной точке и правой конечной точке), точно так же, как в шагах 1-3 операции установки. Теперь мы просто добавляем δ к значению, хранящемуся в каждом из этих O ( LG N )add([a,b],δ)[a,b]O(lgn)O(lgn)δO(lgn)узлы. (Мы не удаляем их потомков.)
Это обеспечивает способ поддержки получения, установки и добавления за времени на операцию. Фактически, время выполнения каждой операции равно O ( lg min ( n , s ) ), где s подсчитывает количество операций над множествами плюс количество операций добавления.O(lgn)O(lgmin(n,s))s
Поддержка операции удара
Самый важный вопрос для поддержки. Основная идея состоит в том, чтобы изменить вышеуказанную структуру данных, чтобы сохранить следующий дополнительный инвариант:
(*) Интервал соответствующий каждому листу ℓ, является максимальным плоским интервалом.I(ℓ)ℓ
Здесь я говорю, что интервал является максимальным плоским интервалом, если (i) [ a , b ] плоский, и (ii) ни один интервал, содержащий [ a , b ], не является плоским (другими словами, для всех a ′ , Ь ' , удовлетворяющих 1 ≤ ' ≤ ≤ B ≤ B ' ≤ п , либо [ ' , Ь ' ] = [[a,b][a,b][a,b]a′,b′1≤a′≤a≤b≤b′≤n или [ a ′ , b ′ ] не плоские).[a′,b′]=[a,b][a′,b′]
Это позволяет легко реализовать операцию «Укол»:
- находит лист, интервал которого содержит i , а затем возвращает этот интервал.stab(i)i
Однако теперь нам нужно изменить операции set и add для поддержки инварианта (*). Каждый раз, когда мы разбиваем лист на два, мы можем нарушать инвариант, если некоторая смежная пара листовых интервалов имеет одинаковое значение функции . К счастью, каждая операция set / add добавляет максимум 4 новых конечных интервала. Кроме того, для каждого нового интервала легко найти листовой интервал непосредственно слева и справа от него. Следовательно, мы можем сказать, был ли нарушен инвариант; если это так, то мы объединяем смежные интервалы, где f имеет одинаковое значение. К счастью, объединение двух смежных интервалов не вызывает каскадных изменений (поэтому нам не нужно проверять, могло ли слияние внести дополнительные нарушения инварианта). В целом, это включает в себя изучение 12ff пары интервалов и, возможно, слияние их. Наконец, поскольку слияние меняет форму дерева, если это нарушает инварианты баланса, выполните любые необходимые повороты, чтобы сохранить баланс дерева (следуя стандартным методикам для поддержания баланса бинарных деревьев). В целом, это добавляет не более O ( lg n ) дополнительной работы к операциям set / add.12=O(1)O(lgn)
Таким образом, эта окончательная структура данных поддерживает все четыре операции, и время выполнения каждой операции равно . Более точная оценка - время O ( lg min ( n , s ) ) на операцию, где s подсчитывает количество операций установки и добавления.O(lgn)O(lgmin(n,s))s
Прощальные мысли
Фу, это была довольно сложная схема. Надеюсь, я не допустил ошибок. Пожалуйста, внимательно проверьте мою работу, прежде чем полагаться на это решение.
add
будет линейным по числу подинтервалов в ; Вы думали о дереве сплайнов с дополнительными унарными узлами « + δ », лениво уплотненными?