Как эффективно найти непрерывный диапазон используемых/свободных слотов из дерева Фенвика

Предположим, что я отслеживаю использование слотов в дереве Фенвика. В качестве примера давайте рассмотрим отслеживание 32 слотов, что приведет к макету дерева Фенвика, как показано на изображении ниже, где числа в сетке указывают индекс в базовом массиве с подсчетами, управляемыми деревом Фенвика, где значение в каждой ячейке равно сумма «используемых» элементов в этом сегменте (т.е. ячейка массива 23 хранит количество используемых слотов в диапазоне [16-23]). Элементы на самом низком уровне (т. е. ячейки 0, 2, 4, ...) могут иметь значение только «1» (используемый слот) или «0» (свободный слот).

Пример расположения дерева Фенвика

Я ищу эффективный алгоритм для поиска первого диапазона заданного количества смежных свободных слотов.

Для иллюстрации предположим, что у меня есть дерево Фенвика, показанное на изображении ниже, в котором используется всего 9 слотов (обратите внимание, что светло-серые числа просто добавлены для ясности, а не хранятся в ячейках массива дерева).

Пример дерева

Теперь я хотел бы найти, например. первый непрерывный диапазон из 10 свободных слотов, который должен найти этот диапазон:

Пример результата поиска

Кажется, я не могу найти эффективный способ сделать это, и это вызывает у меня головную боль. Обратите внимание, что, поскольку требуемый объем памяти имеет решающее значение для моих целей, я не хочу расширять проект до дерева сегментов.

Любые мысли и предложения по решению типа O (log N) будут очень кстати.

ИЗМЕНИТЬ

Время для обновления после истечения периода вознаграждения. Спасибо за все комментарии, вопросы, предложения и ответы. Они заставили меня еще раз задуматься, многому меня научили и указали мне (еще раз; однажды я могу усвоить этот урок), что мне следует больше сосредоточиться на проблеме, которую я хочу решить, задавая вопросы.

Поскольку @Erik P был единственным, кто дал разумный ответ на вопрос который содержал запрошенный код/псевдокод, он получит вознаграждение.

Он также правильно указал, что поиск O(log N) с использованием этой структуры будет невозможен. Престижность @DanBjorge за предоставление доказательства, которое заставило меня задуматься о производительности в худшем случае.

Комментарий и ответ @EvgenyKluev заставили меня понять, что я должен был сформулировать свой вопрос по-другому. На самом деле я уже в значительной степени делал то, что он предлагал (см. https://gist.github.com/anonymous/7594508, который показывает, где я застрял, прежде чем опубликовать этот вопрос), и задал этот вопрос, надеясь, что будет эффективный способ поиска в смежных диапазонах, тем самым предотвратив изменение этого дизайна на дерево сегментов (что потребует дополнительные 1024 байта). Однако представляется, что такое изменение может быть разумным решением.

Для всех, кто интересуется, дерево Фенвика с двоичной кодировкой, соответствующее примеру, используемому в этом вопросе (дерево Фенвика с 32 слотами, закодированное в 64 бита), можно найти здесь: https://gist.github.com/anonymous/7594245.


person Alex    schedule 12.11.2013    source источник
comment
не очень понимаю, но если в ячейке 23 хранится #используется в 16-23, и вам нужно найти › 8, то вам нужно искать только подсказки (7,15,23 и т. д.). Отбросить, если › 0, проверить следующие 2, если равно 0? Таким образом, вы ищете только подсказки (лог) диапазонов, плюс небольшое постоянное увеличение в случае частичного успеха?   -  person im so confused    schedule 12.11.2013
comment
Уточните, пожалуйста, что вы подразумеваете под эффективностью? Вы ищете оптимизацию для наихудшего или среднего времени? Если в среднем, есть ли у вас информация об ожидаемом распределении входных данных?   -  person Dan Bjorge    schedule 19.11.2013
comment
@DanBjorge под эффективным я подразумеваю, что он предпочтительно должен быть амортизирован O (log N), т. Е. Оптимизирован для среднего времени. Для моего конкретного случая использования я ожидаю, что будут большие последовательные полностью используемые (1) области (обратите внимание, что дерево, которое я фактически использую, немного больше, чем в примере). Я ожидаю, что поиск обычно будет проводиться в диапазоне от 1 до 16 свободных слотов со средним значением (примерно здесь), вероятно, около 4.   -  person Alex    schedule 19.11.2013
comment
Важно ли найти первый такой диапазон, а не любой произвольный диапазон подходящего размера?   -  person Dan Bjorge    schedule 20.11.2013
comment
@DanBjorge не критично, но да, настоятельно предпочтительнее, поскольку позволяет несколько последовательно распределять свободные слоты при повторных запросах.   -  person Alex    schedule 20.11.2013
comment
Из вашего описания я предполагаю, что вы не можете позволить себе дополнительную память O (N) для какой-либо вспомогательной структуры, верно?   -  person Mikhail    schedule 20.11.2013
comment
@Mikhail правильно, пространство для хранения в настоящее время составляет N. Я не хочу иметь структуру, которая удваивает этот объем пространства.   -  person Alex    schedule 20.11.2013
comment
ИМХО, было бы намного проще найти ответ, если бы вопрос был переформулирован таким образом, что у него есть N элементов со значением 0 или 1 и N слов, реализовать структуру данных O (log N), чтобы получить как количество ненулевых элементов в некотором диапазоне, так и первый диапазон заданного количества смежных нулей. Затем вы просто помещаете все значения в битовый вектор (размер N/64), реализуете дерево Фенвика уменьшенной глубины (размер N/64) для подсчета элементов в диапазоне и реализуете дерево сегментов уменьшенной глубины (размер 2*N/64). найти серии нулей. Единственная проблема — решить, что делать с оставшимися неиспользованными 15*N/16 словами :)   -  person Evgeny Kluev    schedule 21.11.2013
comment
@EvgenyKluev правда, это было бы возможным решением этой проблемы. O (log N) обновлений, запросов точек и диапазонов важны, а также O (N) места для хранения. Не могли бы вы превратить это в ответ?   -  person Alex    schedule 21.11.2013


Ответы (4)


Я думаю, что самый простой способ реализовать все желаемые функции с временной сложностью O (log N) и в то же время минимизировать требования к памяти — это использовать битовый вектор для хранения всех значений 0/1 (свободно/используется). Битовый вектор может заменить 6 нижних уровней как дерева Фенвика, так и дерева сегментов (если они реализованы как 64-битные целые числа). Таким образом, высота этих деревьев может быть уменьшена в 6 раз, а требуемое пространство для каждого из этих деревьев будет в 64 (или 32) раза меньше, чем обычно.

Дерево сегментов может быть реализовано как неявное двоичное дерево, находящееся в массиве (точно так же, как известная реализация max-heap). Корневой узел с индексом 1, каждый левый потомок узла с индексом i помещается в 2*i, каждый правый потомок - в 2*i+1. Это означает, что требуется в два раза больше места по сравнению с деревом Фенвика, но поскольку высота дерева урезана на 6 уровней, это не является большой проблемой.

Каждый узел дерева отрезков должен хранить единственное значение — длину самой длинной непрерывной последовательности «свободных» слотов, начинающихся в точке, охватываемой этим узлом (или ноль, если такой начальной точки нет). Это делает поиск первого диапазона заданного числа смежных нулей очень простым: начните с корня, затем выберите левого потомка, если он содержит значение, большее или равное требуемому, в противном случае выберите правого потомка. Придя к какому-то листовому узлу, проверить соответствующее слово битового вектора (на наличие нулей в середине слова).

Операции обновления более сложны. При изменении значения на «используемое» проверьте соответствующее слово битового вектора, если оно пусто, поднимитесь по дереву сегментов, чтобы найти ненулевое значение для некоторого левого потомка, затем спуститесь по дереву, чтобы добраться до самого правого листа с этим значением, затем определите, как вновь добавленный слот разбивает «свободный» интервал на две половины, затем обновляет все родительские узлы как для добавленного слота, так и для начального узла разбиваемого интервала, а также устанавливает бит в битовом векторе. Изменение значения на «бесплатно» может быть реализовано аналогичным образом.

Если также необходимо получить количество ненулевых элементов в некотором диапазоне, реализуйте дерево Фенвика над тем же битовым вектором (но отдельно от дерева сегментов). В реализации дерева Фенвика нет ничего особенного, за исключением того, что сложение 6 нижних узлов заменяется операцией «подсчет населения» для некоторого слова битового вектора. Пример использования дерева Фенвика вместе с битовым вектором см. в первом решении для Magic Board на CodeChef.

Все необходимые операции над битовым вектором можно довольно эффективно реализовать с помощью различных побитовых трюков. Для некоторых из них (число начальных/конечных нулей и количество населения) вы можете использовать либо встроенные функции компилятора, либо инструкции ассемблера (в зависимости от целевой архитектуры).

Если битовый вектор реализован с 64-битными словами, а узлы дерева - с 32-битными словами, то оба дерева занимают 150% пространства в дополнение к битовому вектору. Это может быть значительно уменьшено, если каждый листовой узел будет соответствовать не одному слову битового вектора, а небольшому диапазону (4 или 8 слов). Для 8 слов дополнительное пространство, необходимое для деревьев, составит всего 20% от размера битового вектора. Это несколько усложняет реализацию. При правильной оптимизации производительность должна быть примерно такой же, как и в варианте для одного слова на листовой узел. Для очень больших наборов данных производительность, вероятно, будет лучше (поскольку вычисления с битовыми векторами более удобны для кэширования, чем обход деревьев).

person Evgeny Kluev    schedule 21.11.2013
comment
Спасибо за ваши дополнительные мысли. Мне нравится идея, и я уже немного поиграл с ней. Я согласен, где-то должен быть достигнут компромисс в размере битового вектора для листьев, чтобы сбалансировать удобство кеша по сравнению с кешем. ограничение количества шагов поиска и места для хранения. Вы, возможно, заметили из моего редактирования выше, что я начал даже с кодирования 32 листовых слотов в 64-битном слове в виде 5-уровневого дерева, что было хорошим упражнением, но не тем путем, как я быстро понял. - person Alex; 22.11.2013
comment
@Alex: Да, я понял часть вашего кода (к сожалению, мне не хватает знаний C#, чтобы понять его полностью, кроме того, вы реализуете все в одном классе, что усложняет ситуацию). Ваш код для подсчета занятых слотов в диапазоне выглядит нормально. Но код для поиска непрерывного блока использует то же дерево Фенвика, где я ожидал увидеть дерево сегментов... - person Evgeny Kluev; 22.11.2013
comment
Если предложенная здесь идея использования дерева сегментов кажется вам слишком сложной, вы можете пойти более простым путем: создать одно дерево сегментов для максимальной длины непрерывного блока, как это предлагается, и создать другое дерево сегментов, сохраняющее крайнюю правую начальную позицию для некоторого непрерывного блока. Это занимает больше памяти, но вы можете использовать простой поиск сверху вниз в этом дополнительном дереве вместо более сложного поиска, предложенного в ответе, это упрощает операции обновления. - person Evgeny Kluev; 22.11.2013
comment
Евгений, это потому, что код, который я разместил, на самом деле не решает проблему поиска смежных диапазонов. Он просто делает несколько маленьких шагов в этом направлении и показывает, где я застрял, прежде чем я разместил этот вопрос на SO. - person Alex; 22.11.2013

Как предлагает mcdowella в их ответе, пусть K2 = K/2, округляя вверх, и пусть M будет наименьшей степенью числа 2. то есть >= К2. Многообещающим подходом был бы поиск смежных блоков K2 нулей, полностью содержащихся в одном блоке размера M, и, как только мы их нашли, проверка соседних блоков размера M, чтобы увидеть, содержат ли они достаточное количество смежных нулей. Для начального сканирования, если количество нулей в блоке ‹ K2, очевидно, мы можем его пропустить, а если количество нулей >= K2 и размер блока >= 2*M, мы можем посмотреть на оба подблока.

Это предполагает следующий код. Ниже A[0 .. N-1] — массив дерева Фенвика; Предполагается, что N является степенью числа 2. Я предполагаю, что вы считаете пустые слоты, а не непустые; если вы предпочитаете считать пустые слоты, достаточно легко перейти от одного к другому.

initialize q as a stack data structure of triples of integers
push (N-1, N, A[n-1]) onto q
# An entry (i, j, z) represents the block [i-j+1 .. i] of length j, which
# contains z zeroes; we start with one block representing the whole array.
# We maintain the invariant that i always has at least as many trailing ones
# in its binary representation as j has trailing zeroes. (**)
initialize r as an empty list of pairs of integers
while q is not empty:
    pop an entry (i,j,z) off q
    if z < K2:
        next

    if FW(i) >= K:
        first_half := i - j/2
        # change this if you want to count nonempty slots:
        first_half_zeroes := A[first_half]
        # Because of invariant (**) above, first_half always has exactly
        # the right number of trailing 1 bits in its binary representation
        # that A[first_half] counts elements of the interval
        # [i-j+1 .. first_half].

        push (i, j/2, z - first_half_zeroes) onto q
        push (first_half, j/2, first_half_zeroes) onto q
    else:
        process_block(i, j, z)

Это позволяет нам обрабатывать по порядку все блоки размера M, содержащие не менее K/2 нулей. Вы даже можете рандомизировать порядок, в котором вы вставляете первую и вторую половину в q, чтобы получить блоки в случайном порядке, что может быть полезно для борьбы с ситуацией, когда первая половина вашего массива заполняется гораздо быстрее, чем последняя половина.

Теперь нам нужно обсудить, как обрабатывать отдельный блок. Если z = j, то блок полностью заполнен нулями, и мы можем смотреть как влево, так и вправо, чтобы добавить нули. В противном случае нам нужно выяснить, начинается ли он с >= K/2 смежных нулей, и если да, то с какого именно, а затем проверить, заканчивается ли предыдущий блок подходящим количеством нулей. Точно так же мы проверяем, заканчивается ли блок >= K/2 непрерывных нулей, и если да, то сколько именно, а затем проверяем, начинается ли следующий блок с подходящего количества нулей. Поэтому нам понадобится процедура для определения количества нулей, с которых начинается или заканчивается блок, возможно, с сокращением, если это не меньше a или не больше b. Чтобы быть точным: пусть end_with_zeroes(i, j, min, max) будет процедурой, которая возвращает количество нулей, которыми заканчивается блок из [i-j+1 .. j], с ярлыком для возврата max, если результат будет больше max и min, если результат будет меньше min. Аналогично для start_with_zeroes(i, j, min, max).

def process_block(i, j, z):
    if j == z:
        if i > j:
            a := ends_with_zeroes(i-j, j, 0, K-z)
        else:
            a := 0

        if i < N-1:
            b := starts_with_zeroes(i+j, j, K-z-a-1, K-z-a)
        else:
            b := 0

        if b >= K-z-a:
            print "Found: starting at ", i - j - a + 1
        return

    # If the block doesn't start or end with K2 zeroes but overlaps with a
    # correct solution anyway, we don't need to find it here -- we'll find it
    # starting from the adjacent block.
    a := starts_with_zeroes(i, j, K2-1, j)
    if i > j and a >= K2:
        b := ends_with_zeroes(i-j, j, K-a-1, K-a)
        if b >= K-a:
            print "Found: starting at ", i - j - a + 1
        # Since z < 2*K2, and j != z, we know this block doesn't end with K2
        # zeroes, so we can safely return.
        return

    a := ends_with_zeroes(i, j, K2-1, j)
    if i < N-1 and a >= K2:
        b := starts_with_zeroes(i+j, K-a-1, K-a)
        if b >= K-a:
            print "Found: starting at ", i - a + 1

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

Теперь все, что осталось, это реализовать start_with_zeroes и end_with_zeroes. Чтобы убедиться, что блок начинается как минимум с min нулей, мы можем проверить, что он начинается с 2 ^ h нулей (где 2 ^ h ‹ = min), проверив соответствующую запись Фенвика; затем аналогичным образом проверьте, начинается ли он с 2 ^ H нулей, где 2 ^ H >= max, чтобы сократить путь в другую сторону (за исключением случаев, когда max = j, сложнее найти правильный счет из дерева Фенвика); затем найдите точное число.

def starts_with_zeroes(i, j, min, max):
    start := i-j

    h2 := 1
    while h2 * 2 <= min:
        h2 := h2 * 2
        if A[start + h2] < h2:
            return min
    # Now h2 = 2^h in the text.
    # If you insist, you can do the above operation faster with bit twiddling
    # to get the 2log of min (in which case, for more info google it).

    while h2 < max and A[start + 2*h2] == 2*h2:
        h2 := 2*h2
    if h2 == j:
        # Walk up the Fenwick tree to determine the exact number of zeroes
        # in interval [start+1 .. i]. (Not implemented, but easy.) Let this
        # number be z.

        if z < j:
            h2 := h2 / 2

    if h2 >= max:
        return max

    # Now we know that [start+1 .. start+h2] is all zeroes, but somewhere in 
    # [start+h2+1 .. start+2*h2] there is a one.
    # Maintain invariant: the interval [start+1 .. start+h2] is all zeroes,
    # and there is a one in [start+h2+1 .. start+h2+step].
    step := h2;
    while step > 1:
        step := step / 2
        if A[start + h2 + step] == step:
             h2 := h2 + step
    return h2

Как видите, start_with_zeroes довольно восходящий. Я думаю, что для end_with_zeroes вам следует использовать более нисходящий подход, поскольку изучение второй половины чего-либо в дереве Фенвика немного сложнее. Вы должны быть в состоянии сделать аналогичный тип итерации в стиле бинарного поиска.

Этот алгоритм определенно не O(log(N)), и у меня есть подозрение, что это неизбежно. Дерево Фенвика просто не дает информации, подходящей для вашего вопроса. Однако я думаю, что этот алгоритм будет работать достаточно хорошо на практике, если подходящие интервалы достаточно распространены.

person Erik P.    schedule 19.11.2013

Одна быстрая проверка при поиске диапазона K смежных слотов состоит в том, чтобы найти наибольшую степень двойки, меньшую или равную K/2. Любые K слотов с непрерывными нулями должны содержать по крайней мере один выровненный по Фенвику диапазон слотов размером ‹= K/2, полностью заполненный нулями. Вы можете искать в дереве Фенвика сверху такие куски выровненных нулей, а затем искать первый, который можно расширить, чтобы получить диапазон K смежных нулей.

В вашем примере самый низкий уровень содержит 0 или 1, а верхний уровень содержит суммы потомков. Нахождение отрезков нулей было бы проще, если бы самый нижний уровень содержал 0, где вы в настоящее время пишете 1, и количество смежных нулей слева, где вы в настоящее время пишете нули, а верхние уровни содержали максимальное значение любого потомка. Обновление потребовало бы дополнительной работы, особенно если у вас создавались и уничтожались длинные строки нулей, но вы могли бы найти самую левую строку нулей длины не менее K с помощью одного поиска слева, ветвление слева, где максимальное значение было не менее K , На самом деле здесь большая часть работы по обновлению выполняется созданием и уничтожением серий 1,2,3,4... на самом низком уровне. Возможно, если вы оставите нижний уровень таким, каким он был изначально определен, и проведете индивидуальный анализ эффектов модификаций, вы могли бы иметь верхние уровни, отображающие самую длинную полосу нулей, начинающуюся с любого потомка данного узла — для быстрого поиска — и получить разумные результаты. стоимость обновления.

person mcdowella    schedule 12.11.2013
comment
да, но есть информация, доступная с более высоких уровней, которую можно использовать, чтобы пропустить поиск на нескольких уровнях K/2. В этом примере после проверки ячейки № 15 становится ясно, что в [0-15] нельзя найти 10 смежных слотов и что любые более низкие уровни в этом сегменте нужно исследовать только в том случае, если ячейка № 16 равна 0. Возможно, это хорошее решение. можно найти, выполнив нижнюю границу сверху вниз и проверив верхнюю границу с помощью индексации снизу вверх. - person Alex; 13.11.2013
comment
Да, есть много вещей, которые вы могли бы принять во внимание, но я не видел способа аккуратно учесть их все, поэтому я просто выбрал одну, которую я мог легко сделать. Интересно, является ли это правильной структурой данных для использования, поэтому я отредактировал свой ответ, чтобы предложить структуру, подобную Фенвику, которая упрощает поиск, но обходится дороже в обновлении. - person mcdowella; 13.11.2013
comment
Поиск смежных диапазонов используемых или неиспользуемых слотов — не единственный вариант использования текущей структуры. Если я правильно понял предложенную вами альтернативную структуру, то ячейка на самом низком уровне со значением 0 означает, что используется, а значение › 0 означает, что столько слотов непрерывно свободны слева, включая меня. Где верхние уровни содержат либо максимум сохраненного потомка, либо предполагаемого потомка? У меня возникли проблемы с визуализацией этого, поэтому я расшифровал исходный пример: cubeupload.com/im/b3NGNR.png< /а>. Это то, что вы предлагаете? - person Alex; 13.11.2013
comment
Я сам немного смутно разбираюсь в деталях деревьев Фенвика, но, судя по вашей диаграмме, я уверен, что вы уловили мою идею такой, какая она есть. - person mcdowella; 13.11.2013

@Erik рассказал о разумном алгоритме звучания. Однако обратите внимание, что эта задача имеет нижнюю границу сложности Ω(N/K) в худшем случае.

Доказательство:

Рассмотрим сокращенную версию задачи, где:

  • N и K - обе степени числа 2.
  • N > 2K >= 4

Предположим, ваш входной массив состоит из (N/2K) кусков размером 2K. Один фрагмент имеет форму K 0, за которым следуют K 1, каждый второй фрагмент представляет собой строку, повторенную 10 K раз. Есть (N/2K) таких массивов, каждый из которых имеет ровно одно решение задачи (начало одного специального фрагмента).

Пусть n = log2(N), k = log2(K). Давайте также определим корневой узел дерева как находящийся на уровне 0, а конечные узлы как находящиеся на уровне n дерева.

Обратите внимание, что из-за того, что наш массив состоит из выровненных фрагментов размером 2 КБ, уровень n-k дерева будет просто состоять из количества единиц в каждом фрагменте. Однако в каждом из наших кусков одинаковое количество единиц. Это означает, что каждый узел на уровне n-k будет идентичным, что, в свою очередь, означает, что каждый узел на уровне ‹= n-k также будет идентичным.

Это означает, что дерево не содержит информации, которая могла бы устранить неоднозначность специального фрагмента, пока вы не начнете анализировать уровень n-k+1 и ниже. Но поскольку все узлы (N/K) на этом уровне, кроме двух, идентичны, это означает, что в худшем случае вам придется проверить O(N/K) узлов, чтобы отличить решение от остальных. узлы.

person Dan Bjorge    schedule 20.11.2013
comment
Очень интересно. Некоторые фотографии (например, ОП) будут оценены. - person Mikhail; 20.11.2013
comment
Спасибо, я бы предположил, что в худшем случае ситуация состоит в том, что самый низкий уровень состоит только из фрагментов 1 + (nk) нулей или (nk) нулей + 1. В этом наихудшем случае (если я правильно понял) search должен был бы проверить общее количество узлов N/K + 2 * (N/K - 1). Чтобы проиллюстрировать свое понимание (и для @Mikhail), я загрузил изображение (желтый = проверенные узлы) здесь: cubeupload. com/im/wPHXyv.png - person Alex; 20.11.2013
comment
И поправка: в худшем случае будет N/K + k * (N/K - 1), если на этот раз я все правильно понял :D - person Alex; 21.11.2013